Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 66 KiB |
Before Width: | Height: | Size: 64 KiB |
Before Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 57 KiB |
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 60 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 30 KiB |
@ -1,142 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.common.views;
|
||||
|
||||
import androidx.test.ext.junit.runners.*;
|
||||
import androidx.test.filters.*;
|
||||
|
||||
import org.apache.commons.lang3.*;
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.core.models.*;
|
||||
import org.isoron.uhabits.core.ui.callbacks.*;
|
||||
import org.isoron.uhabits.core.utils.*;
|
||||
import org.isoron.uhabits.utils.*;
|
||||
import org.junit.*;
|
||||
import org.junit.runner.*;
|
||||
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@MediumTest
|
||||
public class HistoryChartTest extends BaseViewTest
|
||||
{
|
||||
private static final String BASE_PATH = "common/HistoryChart/";
|
||||
|
||||
private HistoryChart chart;
|
||||
|
||||
private Habit habit;
|
||||
|
||||
Timestamp today;
|
||||
|
||||
private OnToggleCheckmarkListener onToggleEntryListener;
|
||||
|
||||
@Override
|
||||
@Before
|
||||
public void setUp()
|
||||
{
|
||||
super.setUp();
|
||||
|
||||
fixtures.purgeHabits(habitList);
|
||||
habit = fixtures.createLongHabit();
|
||||
today = new Timestamp(DateUtils.getStartOfToday());
|
||||
|
||||
Integer[] entries = habit
|
||||
.getComputedEntries()
|
||||
.getByInterval(today.minus(300), today)
|
||||
.stream()
|
||||
.map(Entry::getValue)
|
||||
.toArray(Integer[]::new);
|
||||
|
||||
chart = new HistoryChart(targetContext);
|
||||
chart.setSkipEnabled(true);
|
||||
chart.setEntries(ArrayUtils.toPrimitive(entries));
|
||||
chart.setColor(PaletteUtilsKt.toFixedAndroidColor(habit.getColor()));
|
||||
measureView(chart, dpToPixels(400), dpToPixels(200));
|
||||
|
||||
onToggleEntryListener = mock(OnToggleCheckmarkListener.class);
|
||||
chart.setOnToggleCheckmarkListener(onToggleEntryListener);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tapDate_atInvalidLocations() throws Throwable
|
||||
{
|
||||
chart.setIsEditable(true);
|
||||
chart.tap(dpToPixels(118), dpToPixels(13)); // header
|
||||
chart.tap(dpToPixels(336), dpToPixels(60)); // tomorrow's square
|
||||
chart.tap(dpToPixels(370), dpToPixels(60)); // right axis
|
||||
verifyNoMoreInteractions(onToggleEntryListener);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tapDate_withEditableView() throws Throwable
|
||||
{
|
||||
chart.setIsEditable(true);
|
||||
chart.tap(dpToPixels(340), dpToPixels(40));
|
||||
verify(onToggleEntryListener).onToggleEntry(today, Entry.SKIP);
|
||||
verifyNoMoreInteractions(onToggleEntryListener);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tapDate_withEmptyHabit()
|
||||
{
|
||||
chart.setIsEditable(true);
|
||||
chart.setEntries(new int[]{});
|
||||
chart.tap(dpToPixels(340), dpToPixels(40));
|
||||
verify(onToggleEntryListener).onToggleEntry(today, Entry.YES_MANUAL);
|
||||
verifyNoMoreInteractions(onToggleEntryListener);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tapDate_withReadOnlyView() throws Throwable
|
||||
{
|
||||
chart.setIsEditable(false);
|
||||
chart.tap(dpToPixels(340), dpToPixels(40));
|
||||
verifyNoMoreInteractions(onToggleEntryListener);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRender() throws Throwable
|
||||
{
|
||||
assertRenders(chart, BASE_PATH + "render.png");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRender_withDataOffset() throws Throwable
|
||||
{
|
||||
chart.onScroll(null, null, -dpToPixels(150), 0);
|
||||
chart.invalidate();
|
||||
|
||||
assertRenders(chart, BASE_PATH + "renderDataOffset.png");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRender_withDifferentSize() throws Throwable
|
||||
{
|
||||
measureView(chart, dpToPixels(200), dpToPixels(200));
|
||||
assertRenders(chart, BASE_PATH + "renderDifferentSize.png");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRender_withTransparentBackground() throws Throwable
|
||||
{
|
||||
chart.setIsBackgroundTransparent(true);
|
||||
assertRenders(chart, BASE_PATH + "renderTransparent.png");
|
||||
}
|
||||
}
|
@ -1,553 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.common.views;
|
||||
|
||||
import android.content.*;
|
||||
import android.graphics.*;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint.*;
|
||||
import android.util.*;
|
||||
import android.view.*;
|
||||
|
||||
import androidx.annotation.*;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.core.models.*;
|
||||
import org.isoron.uhabits.core.ui.callbacks.*;
|
||||
import org.isoron.uhabits.core.utils.*;
|
||||
import org.isoron.uhabits.utils.*;
|
||||
import org.jetbrains.annotations.*;
|
||||
|
||||
import java.text.*;
|
||||
import java.util.*;
|
||||
|
||||
import static org.isoron.uhabits.utils.InterfaceUtils.*;
|
||||
import static org.isoron.uhabits.core.models.Entry.*;
|
||||
|
||||
public class HistoryChart extends ScrollableChart
|
||||
{
|
||||
private int[] checkmarks;
|
||||
|
||||
private Paint pSquareBg, pSquareFg, pTextHeader;
|
||||
|
||||
private float squareSpacing;
|
||||
|
||||
private float squareTextOffset;
|
||||
|
||||
private float headerTextOffset;
|
||||
|
||||
private float columnWidth;
|
||||
|
||||
private float columnHeight;
|
||||
|
||||
private int nColumns;
|
||||
|
||||
private SimpleDateFormat dfMonth;
|
||||
|
||||
private SimpleDateFormat dfYear;
|
||||
|
||||
private Calendar baseDate;
|
||||
|
||||
private int nDays;
|
||||
|
||||
/**
|
||||
* 0-based-position of today in the column
|
||||
*/
|
||||
private int todayPositionInColumn;
|
||||
|
||||
private int colors[];
|
||||
|
||||
private int textColors[];
|
||||
|
||||
private RectF baseLocation;
|
||||
|
||||
private int primaryColor;
|
||||
|
||||
private boolean isBackgroundTransparent;
|
||||
|
||||
private int reverseTextColor;
|
||||
|
||||
private int backgroundColor;
|
||||
|
||||
private boolean isEditable;
|
||||
|
||||
private String previousMonth;
|
||||
|
||||
private String previousYear;
|
||||
|
||||
private float headerOverflow = 0;
|
||||
|
||||
private boolean isNumerical = false;
|
||||
|
||||
private int firstWeekday = Calendar.SUNDAY;
|
||||
|
||||
@NonNull
|
||||
private OnToggleCheckmarkListener onToggleCheckmarkListener;
|
||||
|
||||
private boolean skipsEnabled;
|
||||
|
||||
public HistoryChart(Context context)
|
||||
{
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public HistoryChart(Context context, AttributeSet attrs)
|
||||
{
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLongPress(MotionEvent e)
|
||||
{
|
||||
onSingleTapUp(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSingleTapUp(MotionEvent e)
|
||||
{
|
||||
float x, y;
|
||||
try
|
||||
{
|
||||
int pointerId = e.getPointerId(0);
|
||||
x = e.getX(pointerId);
|
||||
y = e.getY(pointerId);
|
||||
}
|
||||
catch (RuntimeException ex)
|
||||
{
|
||||
// Android often throws IllegalArgumentException here. Apparently,
|
||||
// the pointer id may become invalid shortly after calling
|
||||
// e.getPointerId.
|
||||
return false;
|
||||
}
|
||||
return tap(x, y);
|
||||
}
|
||||
|
||||
public boolean tap(float x, float y)
|
||||
{
|
||||
if (!isEditable) return false;
|
||||
performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
|
||||
|
||||
final Timestamp timestamp = positionToTimestamp(x, y);
|
||||
if (timestamp == null) return false;
|
||||
|
||||
Timestamp today = DateUtils.getTodayWithOffset();
|
||||
int newValue = YES_MANUAL;
|
||||
int offset = timestamp.daysUntil(today);
|
||||
if (offset < checkmarks.length)
|
||||
{
|
||||
if(skipsEnabled)
|
||||
newValue = Entry.Companion.nextToggleValueWithSkip(checkmarks[offset]);
|
||||
else
|
||||
newValue = Entry.Companion.nextToggleValueWithoutSkip(checkmarks[offset]);
|
||||
}
|
||||
|
||||
onToggleCheckmarkListener.onToggleEntry(timestamp, newValue);
|
||||
postInvalidate();
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
public void populateWithRandomData()
|
||||
{
|
||||
Random random = new Random();
|
||||
checkmarks = new int[100];
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
if (random.nextFloat() < 0.3) checkmarks[i] = 2;
|
||||
|
||||
for (int i = 0; i < 100 - 7; i++)
|
||||
{
|
||||
int count = 0;
|
||||
for (int j = 0; j < 7; j++)
|
||||
if (checkmarks[i + j] != 0) count++;
|
||||
|
||||
if (count >= 3) checkmarks[i] = Math.max(checkmarks[i], 1);
|
||||
}
|
||||
}
|
||||
|
||||
public void setEntries(int[] checkmarks)
|
||||
{
|
||||
this.checkmarks = checkmarks;
|
||||
postInvalidate();
|
||||
}
|
||||
|
||||
public void setColor(int color)
|
||||
{
|
||||
this.primaryColor = color;
|
||||
initColors();
|
||||
postInvalidate();
|
||||
}
|
||||
|
||||
public void setOnToggleCheckmarkListener(@NonNull OnToggleCheckmarkListener onToggleCheckmarkListener)
|
||||
{
|
||||
this.onToggleCheckmarkListener = onToggleCheckmarkListener;
|
||||
}
|
||||
|
||||
public void setNumerical(boolean numerical)
|
||||
{
|
||||
isNumerical = numerical;
|
||||
postInvalidate();
|
||||
}
|
||||
|
||||
public void setIsBackgroundTransparent(boolean isBackgroundTransparent)
|
||||
{
|
||||
this.isBackgroundTransparent = isBackgroundTransparent;
|
||||
initColors();
|
||||
}
|
||||
|
||||
public void setSkipEnabled(boolean value)
|
||||
{
|
||||
this.skipsEnabled = value;
|
||||
}
|
||||
|
||||
public void setIsEditable(boolean isEditable)
|
||||
{
|
||||
this.isEditable = isEditable;
|
||||
}
|
||||
|
||||
public void setFirstWeekday(int firstWeekday)
|
||||
{
|
||||
this.firstWeekday = firstWeekday;
|
||||
postInvalidate();
|
||||
}
|
||||
|
||||
protected void initPaints()
|
||||
{
|
||||
pTextHeader = new Paint();
|
||||
pTextHeader.setTextAlign(Align.LEFT);
|
||||
pTextHeader.setAntiAlias(true);
|
||||
|
||||
pSquareBg = new Paint();
|
||||
pSquareBg.setAntiAlias(true);
|
||||
|
||||
pSquareFg = new Paint();
|
||||
pSquareFg.setAntiAlias(true);
|
||||
pSquareFg.setTextAlign(Align.CENTER);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas)
|
||||
{
|
||||
super.onDraw(canvas);
|
||||
|
||||
baseLocation.set(0, 0, columnWidth - squareSpacing,
|
||||
columnWidth - squareSpacing);
|
||||
baseLocation.offset(getPaddingLeft(), getPaddingTop());
|
||||
|
||||
headerOverflow = 0;
|
||||
previousMonth = "";
|
||||
previousYear = "";
|
||||
pTextHeader.setColor(textColors[1]);
|
||||
|
||||
updateDate();
|
||||
GregorianCalendar currentDate = (GregorianCalendar) baseDate.clone();
|
||||
|
||||
for (int column = 0; column < nColumns - 1; column++)
|
||||
{
|
||||
drawColumn(canvas, baseLocation, currentDate, column);
|
||||
baseLocation.offset(columnWidth, -columnHeight);
|
||||
}
|
||||
|
||||
drawAxis(canvas, baseLocation);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
|
||||
{
|
||||
int width = MeasureSpec.getSize(widthMeasureSpec);
|
||||
int height = MeasureSpec.getSize(heightMeasureSpec);
|
||||
setMeasuredDimension(width, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int width,
|
||||
int height,
|
||||
int oldWidth,
|
||||
int oldHeight)
|
||||
{
|
||||
if (height < 8) height = 200;
|
||||
float baseSize = height / 8.0f;
|
||||
setScrollerBucketSize((int) baseSize);
|
||||
|
||||
squareSpacing = dpToPixels(getContext(), 1.0f);
|
||||
float maxTextSize = getDimension(getContext(), R.dimen.regularTextSize);
|
||||
float textSize = height * 0.06f;
|
||||
textSize = Math.min(textSize, maxTextSize);
|
||||
|
||||
pSquareFg.setTextSize(textSize);
|
||||
pTextHeader.setTextSize(textSize);
|
||||
squareTextOffset = pSquareFg.getFontSpacing() * 0.4f;
|
||||
headerTextOffset = pTextHeader.getFontSpacing() * 0.3f;
|
||||
|
||||
float rightLabelWidth = getWeekdayLabelWidth() + headerTextOffset;
|
||||
float horizontalPadding = getPaddingRight() + getPaddingLeft();
|
||||
|
||||
columnWidth = baseSize;
|
||||
columnHeight = 8 * baseSize;
|
||||
nColumns =
|
||||
(int) ((width - rightLabelWidth - horizontalPadding) / baseSize) +
|
||||
1;
|
||||
|
||||
updateDate();
|
||||
}
|
||||
|
||||
private void drawAxis(Canvas canvas, RectF location)
|
||||
{
|
||||
float verticalOffset = pTextHeader.getFontSpacing() * 0.4f;
|
||||
|
||||
for (String day : DateUtils.getShortWeekdayNames(firstWeekday))
|
||||
{
|
||||
location.offset(0, columnWidth);
|
||||
canvas.drawText(day, location.left + headerTextOffset,
|
||||
location.centerY() + verticalOffset, pTextHeader);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawColumn(Canvas canvas,
|
||||
RectF location,
|
||||
GregorianCalendar date,
|
||||
int column)
|
||||
{
|
||||
drawColumnHeader(canvas, location, date);
|
||||
location.offset(0, columnWidth);
|
||||
|
||||
for (int j = 0; j < 7; j++)
|
||||
{
|
||||
if (!(column == nColumns - 2 && getDataOffset() == 0 &&
|
||||
j > todayPositionInColumn))
|
||||
{
|
||||
int checkmarkOffset =
|
||||
getDataOffset() * 7 + nDays - 7 * (column + 1) +
|
||||
todayPositionInColumn - j;
|
||||
drawSquare(canvas, location, date, checkmarkOffset);
|
||||
}
|
||||
|
||||
date.add(Calendar.DAY_OF_MONTH, 1);
|
||||
location.offset(0, columnWidth);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawColumnHeader(Canvas canvas,
|
||||
RectF location,
|
||||
GregorianCalendar date)
|
||||
{
|
||||
String month = dfMonth.format(date.getTime());
|
||||
String year = dfYear.format(date.getTime());
|
||||
|
||||
String text = null;
|
||||
if (!month.equals(previousMonth)) text = previousMonth = month;
|
||||
else if (!year.equals(previousYear)) text = previousYear = year;
|
||||
|
||||
if (text != null)
|
||||
{
|
||||
canvas.drawText(text, location.left + headerOverflow,
|
||||
location.bottom - headerTextOffset, pTextHeader);
|
||||
headerOverflow +=
|
||||
pTextHeader.measureText(text) + columnWidth * 0.2f;
|
||||
}
|
||||
|
||||
headerOverflow = Math.max(0, headerOverflow - columnWidth);
|
||||
}
|
||||
|
||||
private void drawSquare(Canvas canvas,
|
||||
RectF location,
|
||||
GregorianCalendar date,
|
||||
int checkmarkOffset)
|
||||
{
|
||||
|
||||
int checkmark = 0;
|
||||
if (checkmarkOffset >= checkmarks.length)
|
||||
{
|
||||
pSquareBg.setColor(colors[0]);
|
||||
pSquareFg.setColor(textColors[1]);
|
||||
}
|
||||
else
|
||||
{
|
||||
checkmark = checkmarks[checkmarkOffset];
|
||||
if(checkmark <= 0)
|
||||
{
|
||||
pSquareBg.setColor(colors[0]);
|
||||
pSquareFg.setColor(textColors[1]);
|
||||
}
|
||||
else if (isNumerical || checkmark == YES_MANUAL)
|
||||
{
|
||||
pSquareBg.setColor(colors[2]);
|
||||
pSquareFg.setColor(textColors[2]);
|
||||
}
|
||||
else
|
||||
{
|
||||
pSquareBg.setColor(colors[1]);
|
||||
pSquareFg.setColor(textColors[2]);
|
||||
}
|
||||
}
|
||||
|
||||
float round = dpToPixels(getContext(), 2);
|
||||
canvas.drawRoundRect(location, round, round, pSquareBg);
|
||||
|
||||
if (!isNumerical && checkmark == SKIP)
|
||||
{
|
||||
pSquareBg.setColor(backgroundColor);
|
||||
pSquareBg.setStrokeWidth(columnWidth * 0.025f);
|
||||
|
||||
canvas.save();
|
||||
canvas.clipRect(location);
|
||||
float offset = - columnWidth;
|
||||
for (int k = 0; k < 10; k++)
|
||||
{
|
||||
offset += columnWidth / 5;
|
||||
canvas.drawLine(location.left + offset,
|
||||
location.bottom,
|
||||
location.right + offset,
|
||||
location.top,
|
||||
pSquareBg);
|
||||
}
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
String text = Integer.toString(date.get(Calendar.DAY_OF_MONTH));
|
||||
canvas.drawText(text, location.centerX(),
|
||||
location.centerY() + squareTextOffset, pSquareFg);
|
||||
}
|
||||
|
||||
private float getWeekdayLabelWidth()
|
||||
{
|
||||
float width = 0;
|
||||
|
||||
for (String w : DateUtils.getShortWeekdayNames(firstWeekday))
|
||||
width = Math.max(width, pSquareFg.measureText(w));
|
||||
|
||||
return width;
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
isEditable = false;
|
||||
checkmarks = new int[0];
|
||||
onToggleCheckmarkListener = new OnToggleCheckmarkListener()
|
||||
{
|
||||
@Override
|
||||
public void onToggleEntry(@NotNull Timestamp timestamp, int value)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
initColors();
|
||||
initPaints();
|
||||
initDateFormats();
|
||||
initRects();
|
||||
}
|
||||
|
||||
private void initColors()
|
||||
{
|
||||
StyledResources res = new StyledResources(getContext());
|
||||
|
||||
if (isBackgroundTransparent)
|
||||
primaryColor = ColorUtils.setMinValue(primaryColor, 0.75f);
|
||||
|
||||
int red = Color.red(primaryColor);
|
||||
int green = Color.green(primaryColor);
|
||||
int blue = Color.blue(primaryColor);
|
||||
|
||||
backgroundColor = res.getColor(R.attr.cardBgColor);
|
||||
|
||||
if (isBackgroundTransparent)
|
||||
{
|
||||
colors = new int[3];
|
||||
colors[0] = Color.argb(16, 255, 255, 255);
|
||||
colors[1] = Color.argb(128, red, green, blue);
|
||||
colors[2] = primaryColor;
|
||||
|
||||
textColors = new int[3];
|
||||
textColors[0] = Color.WHITE;
|
||||
textColors[1] = Color.WHITE;
|
||||
textColors[2] = Color.WHITE;
|
||||
reverseTextColor = Color.WHITE;
|
||||
}
|
||||
else
|
||||
{
|
||||
colors = new int[3];
|
||||
colors[0] = res.getColor(R.attr.lowContrastTextColor);
|
||||
colors[1] = Color.argb(127, red, green, blue);
|
||||
colors[2] = primaryColor;
|
||||
|
||||
textColors = new int[3];
|
||||
textColors[0] = res.getColor(R.attr.lowContrastReverseTextColor);
|
||||
textColors[1] = res.getColor(R.attr.mediumContrastTextColor);
|
||||
textColors[2] = res.getColor(R.attr.highContrastReverseTextColor);
|
||||
reverseTextColor = res.getColor(R.attr.highContrastReverseTextColor);
|
||||
}
|
||||
}
|
||||
|
||||
private void initDateFormats()
|
||||
{
|
||||
if (isInEditMode())
|
||||
{
|
||||
dfMonth = new SimpleDateFormat("MMM", Locale.getDefault());
|
||||
dfYear = new SimpleDateFormat("yyyy", Locale.getDefault());
|
||||
}
|
||||
else
|
||||
{
|
||||
dfMonth = DateExtensionsKt.toSimpleDataFormat("MMM");
|
||||
dfYear = DateExtensionsKt.toSimpleDataFormat("yyyy");
|
||||
}
|
||||
}
|
||||
|
||||
private void initRects()
|
||||
{
|
||||
baseLocation = new RectF();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Timestamp positionToTimestamp(float x, float y)
|
||||
{
|
||||
int col = (int) (x / columnWidth);
|
||||
int row = (int) (y / columnWidth);
|
||||
|
||||
if (row == 0) return null;
|
||||
if (col == nColumns - 1) return null;
|
||||
|
||||
int offset = col * 7 + (row - 1);
|
||||
Calendar date = (Calendar) baseDate.clone();
|
||||
date.add(Calendar.DAY_OF_YEAR, offset);
|
||||
|
||||
if (DateUtils.getStartOfDayWithOffset(date.getTimeInMillis()) >
|
||||
DateUtils.getStartOfTodayWithOffset()) return null;
|
||||
|
||||
return new Timestamp(date.getTimeInMillis());
|
||||
}
|
||||
|
||||
private void updateDate()
|
||||
{
|
||||
baseDate = DateUtils.getStartOfTodayCalendarWithOffset();
|
||||
baseDate.add(Calendar.DAY_OF_YEAR, -(getDataOffset() - 1) * 7);
|
||||
|
||||
nDays = (nColumns - 1) * 7;
|
||||
int realWeekday =
|
||||
DateUtils.getStartOfTodayCalendarWithOffset().get(Calendar.DAY_OF_WEEK);
|
||||
todayPositionInColumn =
|
||||
(7 + realWeekday - firstWeekday) % 7;
|
||||
|
||||
baseDate.add(Calendar.DAY_OF_YEAR, -nDays);
|
||||
baseDate.add(Calendar.DAY_OF_YEAR, -todayPositionInColumn);
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 7.9 KiB |
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 350 B |
After Width: | Height: | Size: 345 B |
After Width: | Height: | Size: 424 B |
After Width: | Height: | Size: 4.2 KiB |
After Width: | Height: | Size: 42 KiB |
After Width: | Height: | Size: 43 KiB |
After Width: | Height: | Size: 44 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 3.5 KiB |
@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.components
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.isoron.platform.gui.assertRenders
|
||||
import org.isoron.platform.time.JavaLocalDateFormatter
|
||||
import org.isoron.platform.time.LocalDate
|
||||
import org.isoron.uhabits.core.models.PaletteColor
|
||||
import org.isoron.uhabits.core.ui.views.DarkTheme
|
||||
import org.isoron.uhabits.core.ui.views.HistoryChart
|
||||
import org.isoron.uhabits.core.ui.views.HistoryChart.Square.DIMMED
|
||||
import org.isoron.uhabits.core.ui.views.HistoryChart.Square.HATCHED
|
||||
import org.isoron.uhabits.core.ui.views.HistoryChart.Square.OFF
|
||||
import org.isoron.uhabits.core.ui.views.HistoryChart.Square.ON
|
||||
import org.isoron.uhabits.core.ui.views.LightTheme
|
||||
import org.junit.Test
|
||||
import java.util.Locale
|
||||
|
||||
class HistoryChartTest {
|
||||
val base = "views/HistoryChart"
|
||||
val fmt = JavaLocalDateFormatter(Locale.US)
|
||||
val theme = LightTheme()
|
||||
val view = HistoryChart(
|
||||
LocalDate(2015, 1, 25),
|
||||
PaletteColor(7),
|
||||
theme,
|
||||
fmt,
|
||||
).apply {
|
||||
series = listOf(
|
||||
2, // today
|
||||
2, 1, 2, 1, 2, 1, 2,
|
||||
2, 3, 3, 3, 3, 1, 2,
|
||||
2, 1, 2, 1, 2, 2, 1,
|
||||
1, 1, 1, 1, 2, 2, 2,
|
||||
1, 3, 3, 3, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 1, 1, 1, 1,
|
||||
2, 2, 2, 3, 3, 3, 1,
|
||||
1, 2, 1, 2, 1, 1, 2,
|
||||
1, 2, 1, 1, 1, 1, 2,
|
||||
2, 2, 2, 2, 2, 1, 1,
|
||||
1, 1, 2, 2, 1, 2, 1,
|
||||
1, 1, 1, 1, 2, 2, 2,
|
||||
).map {
|
||||
when (it) {
|
||||
3 -> HATCHED
|
||||
2 -> ON
|
||||
1 -> DIMMED
|
||||
else -> OFF
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Label overflow
|
||||
// TODO: Transparent
|
||||
// TODO: onClick
|
||||
// TODO: HistoryEditorDialog
|
||||
// TODO: Remove excessive padding on widgets
|
||||
// TODO: First day of the week
|
||||
|
||||
@Test
|
||||
fun testDraw() = runBlocking {
|
||||
assertRenders(400, 200, "$base/base.png", view)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDrawDifferentSize() = runBlocking {
|
||||
assertRenders(200, 200, "$base/small.png", view)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDrawDarkTheme() = runBlocking {
|
||||
view.theme = DarkTheme()
|
||||
assertRenders(400, 200, "$base/dark.png", view)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDrawOffset() = runBlocking {
|
||||
view.dataOffset = 2
|
||||
assertRenders(400, 200, "$base/scroll.png", view)
|
||||
}
|
||||
}
|