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)
|
||||||
|
}
|
||||||
|
}
|