Replace HistoryChart by new Kotlin implementation

pull/707/head
Alinson S. Xavier 5 years ago
parent 354c930d85
commit 93a2ec3186

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 30 KiB

@ -30,7 +30,7 @@ class AndroidCanvasTest : BaseViewTest() {
val bmp = Bitmap.createBitmap(1000, 800, Bitmap.Config.ARGB_8888) val bmp = Bitmap.createBitmap(1000, 800, Bitmap.Config.ARGB_8888)
val canvas = AndroidCanvas() val canvas = AndroidCanvas()
canvas.context = testContext canvas.context = testContext
canvas.density = 2.0 canvas.innerDensity = 2.0
canvas.innerCanvas = android.graphics.Canvas(bmp) canvas.innerCanvas = android.graphics.Canvas(bmp)
canvas.innerBitmap = bmp canvas.innerBitmap = bmp
canvas.drawTestImage() canvas.drawTestImage()

@ -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");
}
}

@ -25,6 +25,7 @@ import androidx.test.filters.MediumTest
import org.isoron.uhabits.BaseViewTest import org.isoron.uhabits.BaseViewTest
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.core.ui.screens.habits.show.views.HistoryCardPresenter import org.isoron.uhabits.core.ui.screens.habits.show.views.HistoryCardPresenter
import org.isoron.uhabits.core.ui.views.LightTheme
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@ -47,7 +48,8 @@ class HistoryCardViewTest : BaseViewTest() {
HistoryCardPresenter().present( HistoryCardPresenter().present(
habit = habit, habit = habit,
firstWeekday = 1, firstWeekday = 1,
isSkipEnabled = false isSkipEnabled = false,
theme = LightTheme(),
) )
) )
measureView(view, 800f, 600f) measureView(view, 800f, 600f)

@ -29,19 +29,24 @@ import org.isoron.uhabits.utils.InterfaceUtils.getFontAwesome
class AndroidCanvas : Canvas { class AndroidCanvas : Canvas {
lateinit var innerCanvas: android.graphics.Canvas
lateinit var context: Context lateinit var context: Context
lateinit var innerCanvas: android.graphics.Canvas
var innerBitmap: Bitmap? = null var innerBitmap: Bitmap? = null
var density = 1.0 var innerDensity = 1.0
var innerWidth = 0
var innerHeight = 0
var paint = Paint().apply { var paint = Paint().apply {
isAntiAlias = true isAntiAlias = true
} }
var textPaint = TextPaint().apply { var textPaint = TextPaint().apply {
isAntiAlias = true isAntiAlias = true
textAlign = Paint.Align.CENTER
} }
var textBounds = Rect() var textBounds = Rect()
private fun Double.toDp() = (this * density).toFloat() private fun Double.toDp() = (this * innerDensity).toFloat()
override fun setColor(color: Color) { override fun setColor(color: Color) {
paint.color = color.toInt() paint.color = color.toInt()
@ -73,6 +78,25 @@ class AndroidCanvas : Canvas {
rect(x, y, width, height) rect(x, y, width, height)
} }
override fun fillRoundRect(
x: Double,
y: Double,
width: Double,
height: Double,
cornerRadius: Double,
) {
paint.style = Paint.Style.FILL
innerCanvas.drawRoundRect(
x.toDp(),
y.toDp(),
(x + width).toDp(),
(y + height).toDp(),
cornerRadius.toDp(),
cornerRadius.toDp(),
paint,
)
}
override fun drawRect(x: Double, y: Double, width: Double, height: Double) { override fun drawRect(x: Double, y: Double, width: Double, height: Double) {
paint.style = Paint.Style.STROKE paint.style = Paint.Style.STROKE
rect(x, y, width, height) rect(x, y, width, height)
@ -89,11 +113,11 @@ class AndroidCanvas : Canvas {
} }
override fun getHeight(): Double { override fun getHeight(): Double {
return innerCanvas.height / density return innerHeight / innerDensity
} }
override fun getWidth(): Double { override fun getWidth(): Double {
return innerCanvas.width / density return innerWidth / innerDensity
} }
override fun setFont(font: Font) { override fun setFont(font: Font) {

@ -31,7 +31,7 @@ import android.widget.Scroller
*/ */
class AndroidDataView( class AndroidDataView(
context: Context, context: Context,
attrs: AttributeSet, attrs: AttributeSet? = null,
) : AndroidView<DataView>(context, attrs), ) : AndroidView<DataView>(context, attrs),
GestureDetector.OnGestureListener, GestureDetector.OnGestureListener,
ValueAnimator.AnimatorUpdateListener { ValueAnimator.AnimatorUpdateListener {
@ -99,7 +99,7 @@ class AndroidDataView(
} }
private fun updateDataOffset() { private fun updateDataOffset() {
var newDataOffset: Int = scroller.currX / (view.dataColumnWidth * canvas.density).toInt() var newDataOffset: Int = scroller.currX / (view.dataColumnWidth * canvas.innerDensity).toInt()
newDataOffset = Math.max(0, newDataOffset) newDataOffset = Math.max(0, newDataOffset)
if (newDataOffset != view.dataOffset) { if (newDataOffset != view.dataOffset) {
view.dataOffset = newDataOffset view.dataOffset = newDataOffset

@ -28,7 +28,7 @@ class AndroidTestView(context: Context, attrs: AttributeSet) : android.view.View
override fun onDraw(canvas: android.graphics.Canvas) { override fun onDraw(canvas: android.graphics.Canvas) {
this.canvas.context = context this.canvas.context = context
this.canvas.innerCanvas = canvas this.canvas.innerCanvas = canvas
this.canvas.density = resources.displayMetrics.density.toDouble() this.canvas.innerDensity = resources.displayMetrics.density.toDouble()
this.canvas.drawTestImage() this.canvas.drawTestImage()
} }
} }

@ -24,7 +24,7 @@ import android.util.AttributeSet
open class AndroidView<T : View>( open class AndroidView<T : View>(
context: Context, context: Context,
attrs: AttributeSet, attrs: AttributeSet? = null,
) : android.view.View(context, attrs) { ) : android.view.View(context, attrs) {
lateinit var view: T lateinit var view: T
@ -33,7 +33,9 @@ open class AndroidView<T : View>(
override fun onDraw(canvas: android.graphics.Canvas) { override fun onDraw(canvas: android.graphics.Canvas) {
this.canvas.context = context this.canvas.context = context
this.canvas.innerCanvas = canvas this.canvas.innerCanvas = canvas
this.canvas.density = resources.displayMetrics.density.toDouble() this.canvas.innerWidth = width
this.canvas.innerHeight = height
this.canvas.innerDensity = resources.displayMetrics.density.toDouble()
view.draw(this.canvas) view.draw(this.canvas)
} }
} }

@ -30,17 +30,17 @@ import androidx.appcompat.app.*;
import android.util.*; import android.util.*;
import org.isoron.uhabits.*; import org.isoron.uhabits.*;
import org.isoron.uhabits.activities.common.views.*;
import org.isoron.uhabits.core.commands.*; import org.isoron.uhabits.core.commands.*;
import org.isoron.uhabits.core.models.*; import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.preferences.*; import org.isoron.uhabits.core.preferences.*;
import org.isoron.uhabits.core.tasks.*; import org.isoron.uhabits.core.tasks.*;
import org.isoron.uhabits.core.ui.callbacks.*; import org.isoron.uhabits.core.ui.callbacks.*;
import org.isoron.uhabits.core.ui.screens.habits.show.views.*; import org.isoron.uhabits.core.ui.screens.habits.show.views.*;
import org.isoron.uhabits.core.ui.views.*;
import org.isoron.uhabits.utils.*; import org.isoron.uhabits.utils.*;
import org.jetbrains.annotations.*; import org.jetbrains.annotations.*;
import static org.isoron.uhabits.utils.InterfaceUtils.*; import java.util.*;
public class HistoryEditorDialog extends AppCompatDialogFragment public class HistoryEditorDialog extends AppCompatDialogFragment
implements DialogInterface.OnClickListener, CommandRunner.Listener implements DialogInterface.OnClickListener, CommandRunner.Listener
@ -92,31 +92,31 @@ public class HistoryEditorDialog extends AppCompatDialogFragment
commandRunner = app.getComponent().getCommandRunner(); commandRunner = app.getComponent().getCommandRunner();
prefs = app.getComponent().getPreferences(); prefs = app.getComponent().getPreferences();
historyChart = new HistoryChart(context); // historyChart = new HistoryChart(context);
historyChart.setOnToggleCheckmarkListener(onToggleCheckmarkListener); // historyChart.setOnToggleCheckmarkListener(onToggleCheckmarkListener);
historyChart.setFirstWeekday(prefs.getFirstWeekday()); // historyChart.setFirstWeekday(prefs.getFirstWeekday());
historyChart.setSkipEnabled(prefs.isSkipEnabled()); // historyChart.setSkipEnabled(prefs.isSkipEnabled());
if (savedInstanceState != null) // if (savedInstanceState != null)
{ // {
long id = savedInstanceState.getLong("habit", -1); // long id = savedInstanceState.getLong("habit", -1);
if (id > 0) this.habit = habitList.getById(id); // if (id > 0) this.habit = habitList.getById(id);
historyChart.onRestoreInstanceState( // historyChart.onRestoreInstanceState(
savedInstanceState.getParcelable("historyChart")); // savedInstanceState.getParcelable("historyChart"));
} // }
//
int padding = // int padding =
(int) getDimension(getContext(), R.dimen.history_editor_padding); // (int) getDimension(getContext(), R.dimen.history_editor_padding);
//
historyChart.setPadding(padding, 0, padding, 0); // historyChart.setPadding(padding, 0, padding, 0);
historyChart.setIsEditable(true); // historyChart.setIsEditable(true);
//
AlertDialog.Builder builder = new AlertDialog.Builder(context); AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder builder
.setTitle(R.string.history) .setTitle(R.string.history)
.setView(historyChart) // .setView(historyChart)
.setPositiveButton(android.R.string.ok, this); .setPositiveButton(android.R.string.ok, this);
//
return builder.create(); return builder.create();
} }
@ -146,8 +146,8 @@ public class HistoryEditorDialog extends AppCompatDialogFragment
@Override @Override
public void onSaveInstanceState(Bundle outState) public void onSaveInstanceState(Bundle outState)
{ {
outState.putLong("habit", habit.getId()); // outState.putLong("habit", habit.getId());
outState.putParcelable("historyChart", historyChart.onSaveInstanceState()); // outState.putParcelable("historyChart", historyChart.onSaveInstanceState());
} }
public void setOnToggleCheckmarkListener(@NonNull OnToggleCheckmarkListener onToggleCheckmarkListener) public void setOnToggleCheckmarkListener(@NonNull OnToggleCheckmarkListener onToggleCheckmarkListener)
@ -174,7 +174,7 @@ public class HistoryEditorDialog extends AppCompatDialogFragment
private class RefreshTask implements Task private class RefreshTask implements Task
{ {
public int[] checkmarks; public List<HistoryChart.Square> checkmarks;
@Override @Override
public void doInBackground() public void doInBackground()
@ -182,9 +182,10 @@ public class HistoryEditorDialog extends AppCompatDialogFragment
HistoryCardViewModel model = new HistoryCardPresenter().present( HistoryCardViewModel model = new HistoryCardPresenter().present(
habit, habit,
prefs.getFirstWeekday(), prefs.getFirstWeekday(),
prefs.isSkipEnabled() prefs.isSkipEnabled(),
new LightTheme()
); );
checkmarks = model.getEntries(); checkmarks = model.getSeries();
} }
@Override @Override
@ -194,9 +195,9 @@ public class HistoryEditorDialog extends AppCompatDialogFragment
return; return;
int color = PaletteUtilsKt.toThemedAndroidColor(habit.getColor(), getContext()); int color = PaletteUtilsKt.toThemedAndroidColor(habit.getColor(), getContext());
historyChart.setColor(color); // historyChart.setColor(color);
historyChart.setEntries(checkmarks); // historyChart.setEntries(checkmarks);
historyChart.setNumerical(habit.isNumerical()); // historyChart.setNumerical(habit.isNumerical());
} }
} }
} }

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

@ -22,9 +22,12 @@ import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.LinearLayout import android.widget.LinearLayout
import org.isoron.platform.time.JavaLocalDateFormatter
import org.isoron.uhabits.core.ui.screens.habits.show.views.HistoryCardViewModel import org.isoron.uhabits.core.ui.screens.habits.show.views.HistoryCardViewModel
import org.isoron.uhabits.core.ui.views.HistoryChart
import org.isoron.uhabits.databinding.ShowHabitHistoryBinding import org.isoron.uhabits.databinding.ShowHabitHistoryBinding
import org.isoron.uhabits.utils.toThemedAndroidColor import org.isoron.uhabits.utils.toThemedAndroidColor
import java.util.Locale
class HistoryCardView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) { class HistoryCardView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
@ -37,14 +40,24 @@ class HistoryCardView(context: Context, attrs: AttributeSet) : LinearLayout(cont
} }
fun update(data: HistoryCardViewModel) { fun update(data: HistoryCardViewModel) {
binding.historyChart.setFirstWeekday(data.firstWeekday)
binding.historyChart.setSkipEnabled(data.isSkipEnabled)
binding.historyChart.setEntries(data.entries)
val androidColor = data.color.toThemedAndroidColor(context) val androidColor = data.color.toThemedAndroidColor(context)
binding.title.setTextColor(androidColor) binding.title.setTextColor(androidColor)
binding.historyChart.setColor(androidColor) binding.chart.view = HistoryChart(
if (data.isNumerical) { today = data.today,
binding.historyChart.setNumerical(true) paletteColor = data.color,
theme = data.theme,
dateFormatter = JavaLocalDateFormatter(Locale.getDefault())
).apply {
series = data.series
} }
// binding.historyChart.setFirstWeekday(data.firstWeekday)
// binding.historyChart.setSkipEnabled(data.isSkipEnabled)
// binding.historyChart.setEntries(data.entries)
// binding.historyChart.setColor(androidColor)
// if (data.isNumerical) {
// binding.historyChart.setNumerical(true)
// }
} }
} }

@ -22,11 +22,15 @@ package org.isoron.uhabits.widgets
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.view.View import android.view.View
import org.isoron.uhabits.activities.common.views.HistoryChart import org.isoron.platform.gui.AndroidDataView
import org.isoron.platform.time.JavaLocalDateFormatter
import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.ui.screens.habits.show.views.HistoryCardPresenter import org.isoron.uhabits.core.ui.screens.habits.show.views.HistoryCardPresenter
import org.isoron.uhabits.utils.toThemedAndroidColor import org.isoron.uhabits.core.ui.views.DarkTheme
import org.isoron.uhabits.core.ui.views.HistoryChart
import org.isoron.uhabits.core.utils.DateUtils
import org.isoron.uhabits.widgets.views.GraphWidgetView import org.isoron.uhabits.widgets.views.GraphWidgetView
import java.util.Locale
class HistoryWidget( class HistoryWidget(
context: Context, context: Context,
@ -47,18 +51,25 @@ class HistoryWidget(
habit = habit, habit = habit,
isSkipEnabled = prefs.isSkipEnabled, isSkipEnabled = prefs.isSkipEnabled,
firstWeekday = prefs.firstWeekday, firstWeekday = prefs.firstWeekday,
theme = DarkTheme(),
) )
(widgetView.dataView as HistoryChart).apply { (widgetView.dataView as AndroidDataView).apply {
setFirstWeekday(model.firstWeekday) (this.view as HistoryChart).series = model.series
setSkipEnabled(model.isSkipEnabled)
setColor(model.color.toThemedAndroidColor(context))
setEntries(model.entries)
setNumerical(model.isNumerical)
} }
} }
override fun buildView() = override fun buildView() =
GraphWidgetView(context, HistoryChart(context)).apply { GraphWidgetView(
context,
AndroidDataView(context).apply {
view = HistoryChart(
today = DateUtils.getTodayWithOffset().toLocalDate(),
paletteColor = habit.color,
theme = DarkTheme(),
dateFormatter = JavaLocalDateFormatter(Locale.getDefault())
)
}
).apply {
setTitle(habit.name) setTitle(habit.name)
} }

@ -29,8 +29,8 @@
style="@style/CardHeader" style="@style/CardHeader"
android:text="@string/calendar"/> android:text="@string/calendar"/>
<org.isoron.uhabits.activities.common.views.HistoryChart <org.isoron.platform.gui.AndroidDataView
android:id="@+id/historyChart" android:id="@+id/chart"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="160dp"/> android:layout_height="160dp"/>

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 350 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

@ -34,6 +34,7 @@ interface Canvas {
fun drawLine(x1: Double, y1: Double, x2: Double, y2: Double) fun drawLine(x1: Double, y1: Double, x2: Double, y2: Double)
fun drawText(text: String, x: Double, y: Double) fun drawText(text: String, x: Double, y: Double)
fun fillRect(x: Double, y: Double, width: Double, height: Double) fun fillRect(x: Double, y: Double, width: Double, height: Double)
fun fillRoundRect(x: Double, y: Double, width: Double, height: Double, cornerRadius: Double)
fun drawRect(x: Double, y: Double, width: Double, height: Double) fun drawRect(x: Double, y: Double, width: Double, height: Double)
fun getHeight(): Double fun getHeight(): Double
fun getWidth(): Double fun getWidth(): Double

@ -19,8 +19,6 @@
package org.isoron.platform.gui package org.isoron.platform.gui
data class PaletteColor(val index: Int)
data class Color( data class Color(
val red: Double, val red: Double,
val green: Double, val green: Double,
@ -48,4 +46,11 @@ data class Color(
alpha * (1 - weight) + other.alpha * weight alpha * (1 - weight) + other.alpha * weight
) )
} }
fun contrast(other: Color): Double {
val l1 = this.luminosity
val l2 = other.luminosity
val relativeLuminosity = (l1 + 0.05) / (l2 + 0.05)
return if (relativeLuminosity >= 1) relativeLuminosity else 1 / relativeLuminosity
}
} }

@ -30,6 +30,7 @@ import java.awt.RenderingHints.VALUE_ANTIALIAS_ON
import java.awt.RenderingHints.VALUE_FRACTIONALMETRICS_ON import java.awt.RenderingHints.VALUE_FRACTIONALMETRICS_ON
import java.awt.RenderingHints.VALUE_TEXT_ANTIALIAS_ON import java.awt.RenderingHints.VALUE_TEXT_ANTIALIAS_ON
import java.awt.font.FontRenderContext import java.awt.font.FontRenderContext
import java.awt.geom.RoundRectangle2D
import java.awt.image.BufferedImage import java.awt.image.BufferedImage
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -115,6 +116,25 @@ class JavaCanvas(
g2d.fillRect(toPixel(x), toPixel(y), toPixel(width), toPixel(height)) g2d.fillRect(toPixel(x), toPixel(y), toPixel(width), toPixel(height))
} }
override fun fillRoundRect(
x: Double,
y: Double,
width: Double,
height: Double,
cornerRadius: Double
) {
g2d.fill(
RoundRectangle2D.Double(
toPixel(x).toDouble(),
toPixel(y).toDouble(),
toPixel(width).toDouble(),
toPixel(height).toDouble(),
toPixel(cornerRadius).toDouble(),
toPixel(cornerRadius).toDouble(),
)
)
}
override fun drawRect(x: Double, y: Double, width: Double, height: Double) { override fun drawRect(x: Double, y: Double, width: Double, height: Double) {
g2d.drawRect(toPixel(x), toPixel(y), toPixel(width), toPixel(height)) g2d.drawRect(toPixel(x), toPixel(y), toPixel(width), toPixel(height))
} }

@ -96,6 +96,7 @@ class ShowHabitPresenter {
habit = habit, habit = habit,
firstWeekday = preferences.firstWeekday, firstWeekday = preferences.firstWeekday,
isSkipEnabled = preferences.isSkipEnabled, isSkipEnabled = preferences.isSkipEnabled,
theme = theme,
), ),
bar = BarCardPresenter().present( bar = BarCardPresenter().present(
habit = habit, habit = habit,

@ -19,16 +19,24 @@
package org.isoron.uhabits.core.ui.screens.habits.show.views package org.isoron.uhabits.core.ui.screens.habits.show.views
import org.isoron.platform.time.LocalDate
import org.isoron.uhabits.core.models.Entry
import org.isoron.uhabits.core.models.Entry.Companion.SKIP
import org.isoron.uhabits.core.models.Entry.Companion.YES_AUTO
import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL
import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.ui.views.HistoryChart
import org.isoron.uhabits.core.ui.views.Theme
import org.isoron.uhabits.core.utils.DateUtils import org.isoron.uhabits.core.utils.DateUtils
import kotlin.math.max
data class HistoryCardViewModel( data class HistoryCardViewModel(
val color: PaletteColor, val color: PaletteColor,
val entries: IntArray,
val firstWeekday: Int, val firstWeekday: Int,
val isNumerical: Boolean, val series: List<HistoryChart.Square>,
val isSkipEnabled: Boolean, val theme: Theme,
val today: LocalDate,
) )
class HistoryCardPresenter { class HistoryCardPresenter {
@ -36,18 +44,37 @@ class HistoryCardPresenter {
habit: Habit, habit: Habit,
firstWeekday: Int, firstWeekday: Int,
isSkipEnabled: Boolean, isSkipEnabled: Boolean,
theme: Theme,
): HistoryCardViewModel { ): HistoryCardViewModel {
val today = DateUtils.getTodayWithOffset() val today = DateUtils.getTodayWithOffset()
val oldest = habit.computedEntries.getKnown().lastOrNull()?.timestamp ?: today val oldest = habit.computedEntries.getKnown().lastOrNull()?.timestamp ?: today
val entries = val entries = habit.computedEntries.getByInterval(oldest, today)
habit.computedEntries.getByInterval(oldest, today).map { it.value }.toIntArray() val series = if (habit.isNumerical) {
entries.map {
Entry(it.timestamp, max(0, it.value))
}.map {
when (it.value) {
0 -> HistoryChart.Square.OFF
else -> HistoryChart.Square.ON
}
}
} else {
entries.map {
when (it.value) {
YES_MANUAL -> HistoryChart.Square.ON
YES_AUTO -> HistoryChart.Square.DIMMED
SKIP -> HistoryChart.Square.HATCHED
else -> HistoryChart.Square.OFF
}
}
}
return HistoryCardViewModel( return HistoryCardViewModel(
entries = entries,
color = habit.color, color = habit.color,
firstWeekday = firstWeekday, firstWeekday = firstWeekday,
isNumerical = habit.isNumerical, today = today.toLocalDate(),
isSkipEnabled = isSkipEnabled, theme = theme,
series = series,
) )
} }
} }

@ -21,53 +21,74 @@ package org.isoron.uhabits.core.ui.views
import org.isoron.platform.gui.Canvas import org.isoron.platform.gui.Canvas
import org.isoron.platform.gui.Color import org.isoron.platform.gui.Color
import org.isoron.platform.gui.DataView
import org.isoron.platform.gui.TextAlign import org.isoron.platform.gui.TextAlign
import org.isoron.platform.gui.View
import org.isoron.platform.time.LocalDate import org.isoron.platform.time.LocalDate
import org.isoron.platform.time.LocalDateFormatter import org.isoron.platform.time.LocalDateFormatter
import org.isoron.uhabits.core.models.PaletteColor
import kotlin.math.floor import kotlin.math.floor
import kotlin.math.round import kotlin.math.round
class CalendarChart( class HistoryChart(
var today: LocalDate, var today: LocalDate,
var color: Color, var paletteColor: PaletteColor,
var theme: Theme, var theme: Theme,
var dateFormatter: LocalDateFormatter var dateFormatter: LocalDateFormatter
) : View { ) : DataView {
var padding = 5.0 enum class Square {
var backgroundColor = Color(0xFFFFFF) ON,
OFF,
DIMMED,
HATCHED,
}
// Data
var series = listOf<Square>()
// Style
var padding = 0.0
var squareSpacing = 1.0 var squareSpacing = 1.0
var series = listOf<Double>() override var dataOffset = 0
var scrollPosition = 0
private var squareSize = 0.0 private var squareSize = 0.0
var lastPrintedMonth = ""
var lastPrintedYear = ""
override val dataColumnWidth: Double
get() = squareSpacing + squareSize
override fun draw(canvas: Canvas) { override fun draw(canvas: Canvas) {
val width = canvas.getWidth() val width = canvas.getWidth()
val height = canvas.getHeight() val height = canvas.getHeight()
canvas.setColor(backgroundColor) canvas.setColor(theme.cardBackgroundColor)
canvas.fillRect(0.0, 0.0, width, height) canvas.fillRect(0.0, 0.0, width, height)
squareSize = round((height - 2 * padding) / 8.0) squareSize = round((height - 2 * padding) / 8.0)
canvas.setFontSize(height * 0.06) canvas.setFontSize(height * 0.06)
val nColumns = floor((width - 2 * padding) / squareSize).toInt() - 2 val nColumns = floor((width - 2 * padding) / squareSize).toInt() - 2
val todayWeekday = today.dayOfWeek val todayWeekday = today.dayOfWeek
val topLeftOffset = (nColumns - 1 + scrollPosition) * 7 + todayWeekday.index val topLeftOffset = (nColumns - 1 + dataOffset) * 7 + todayWeekday.index
val topLeftDate = today.minus(topLeftOffset) val topLeftDate = today.minus(topLeftOffset)
lastPrintedYear = ""
lastPrintedMonth = ""
// Draw main columns
repeat(nColumns) { column -> repeat(nColumns) { column ->
val topOffset = topLeftOffset - 7 * column val topOffset = topLeftOffset - 7 * column
val topDate = topLeftDate.plus(7 * column) val topDate = topLeftDate.plus(7 * column)
drawColumn(canvas, column, topDate, topOffset) drawColumn(canvas, column, topDate, topOffset)
} }
// Draw week day names
canvas.setColor(theme.mediumContrastTextColor) canvas.setColor(theme.mediumContrastTextColor)
repeat(7) { row -> repeat(7) { row ->
val date = topLeftDate.plus(row) val date = topLeftDate.plus(row)
canvas.setTextAlign(TextAlign.LEFT) canvas.setTextAlign(TextAlign.LEFT)
canvas.drawText( canvas.drawText(
dateFormatter.shortWeekdayName(date), dateFormatter.shortWeekdayName(date),
padding + nColumns * squareSize + padding, padding + nColumns * squareSize + squareSpacing * 3,
padding + squareSize * (row + 1) + squareSize / 2 padding + squareSize * (row + 1) + squareSize / 2
) )
} }
@ -97,23 +118,30 @@ class CalendarChart(
} }
private fun drawHeader(canvas: Canvas, column: Int, date: LocalDate) { private fun drawHeader(canvas: Canvas, column: Int, date: LocalDate) {
if (date.day >= 8) return
canvas.setColor(theme.mediumContrastTextColor) canvas.setColor(theme.mediumContrastTextColor)
if (date.month == 1) { val monthText = dateFormatter.shortMonthName(date)
canvas.drawText( val yearText = date.year.toString()
date.year.toString(), val headerText: String
padding + column * squareSize + squareSize / 2, when {
padding + squareSize / 2 monthText != lastPrintedMonth -> {
) headerText = monthText
} else { lastPrintedMonth = monthText
}
yearText != lastPrintedYear -> {
headerText = yearText
lastPrintedYear = headerText
}
else -> {
headerText = ""
}
}
canvas.setTextAlign(TextAlign.LEFT)
canvas.drawText( canvas.drawText(
dateFormatter.shortMonthName(date), headerText,
padding + column * squareSize + squareSize / 2, padding + column * squareSize,
padding + squareSize / 2 padding + squareSize / 2
) )
} }
}
private fun drawSquare( private fun drawSquare(
canvas: Canvas, canvas: Canvas,
@ -125,19 +153,46 @@ class CalendarChart(
offset: Int offset: Int
) { ) {
var value = if (offset >= series.size) 0.0 else series[offset] val value = if (offset >= series.size) Square.OFF else series[offset]
value = round(value * 5.0) / 5.0 val squareColor: Color
val color = theme.color(paletteColor.paletteIndex)
when (value) {
Square.ON -> {
squareColor = color
}
Square.OFF -> {
squareColor = theme.lowContrastTextColor
}
Square.DIMMED, Square.HATCHED -> {
squareColor = color.blendWith(theme.cardBackgroundColor, 0.5)
}
}
var squareColor = color.blendWith(backgroundColor, 1 - value) canvas.setColor(squareColor)
var textColor = backgroundColor canvas.fillRoundRect(x, y, width, height, width * 0.15)
if (value == Square.HATCHED) {
canvas.setStrokeWidth(0.75)
canvas.setColor(theme.cardBackgroundColor)
var k = width / 10
repeat(5) {
canvas.drawLine(x + k, y, x, y + k)
canvas.drawLine(
x + width - k,
y + height,
x + width,
y + height - k
)
k += width / 5
}
}
if (value == 0.0) squareColor = theme.lowContrastTextColor val c1 = squareColor.contrast(theme.cardBackgroundColor)
if (squareColor.luminosity > 0.8) val c2 = squareColor.contrast(theme.mediumContrastTextColor)
textColor = squareColor.blendWith(theme.highContrastTextColor, 0.5) val textColor = if (c1 > c2) theme.cardBackgroundColor else theme.mediumContrastTextColor
canvas.setColor(squareColor)
canvas.fillRect(x, y, width, height)
canvas.setColor(textColor) canvas.setColor(textColor)
canvas.setTextAlign(TextAlign.CENTER)
canvas.drawText(date.day.toString(), x + width / 2, y + width / 2) canvas.drawText(date.day.toString(), x + width / 2, y + width / 2)
} }
} }

@ -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)
}
}
Loading…
Cancel
Save