diff --git a/android/uhabits-android/src/androidTest/assets/views/common/BarChart/render.png b/android/uhabits-android/src/androidTest/assets/views/common/BarChart/render.png deleted file mode 100644 index 0f09906be..000000000 Binary files a/android/uhabits-android/src/androidTest/assets/views/common/BarChart/render.png and /dev/null differ diff --git a/android/uhabits-android/src/androidTest/assets/views/common/BarChart/renderDataOffset.png b/android/uhabits-android/src/androidTest/assets/views/common/BarChart/renderDataOffset.png deleted file mode 100644 index efbbc9d92..000000000 Binary files a/android/uhabits-android/src/androidTest/assets/views/common/BarChart/renderDataOffset.png and /dev/null differ diff --git a/android/uhabits-android/src/androidTest/assets/views/common/BarChart/renderDifferentSize.png b/android/uhabits-android/src/androidTest/assets/views/common/BarChart/renderDifferentSize.png deleted file mode 100644 index b75b1adac..000000000 Binary files a/android/uhabits-android/src/androidTest/assets/views/common/BarChart/renderDifferentSize.png and /dev/null differ diff --git a/android/uhabits-android/src/androidTest/assets/views/common/BarChart/renderTransparent.png b/android/uhabits-android/src/androidTest/assets/views/common/BarChart/renderTransparent.png deleted file mode 100644 index 8cfb3ce13..000000000 Binary files a/android/uhabits-android/src/androidTest/assets/views/common/BarChart/renderTransparent.png and /dev/null differ diff --git a/android/uhabits-android/src/androidTest/assets/views/common/HistoryChart/render.png b/android/uhabits-android/src/androidTest/assets/views/common/HistoryChart/render.png deleted file mode 100644 index 95b59ae9b..000000000 Binary files a/android/uhabits-android/src/androidTest/assets/views/common/HistoryChart/render.png and /dev/null differ diff --git a/android/uhabits-android/src/androidTest/assets/views/common/HistoryChart/renderDataOffset.png b/android/uhabits-android/src/androidTest/assets/views/common/HistoryChart/renderDataOffset.png deleted file mode 100644 index 46fda1fec..000000000 Binary files a/android/uhabits-android/src/androidTest/assets/views/common/HistoryChart/renderDataOffset.png and /dev/null differ diff --git a/android/uhabits-android/src/androidTest/assets/views/common/HistoryChart/renderDifferentSize.png b/android/uhabits-android/src/androidTest/assets/views/common/HistoryChart/renderDifferentSize.png deleted file mode 100644 index 4507f30ba..000000000 Binary files a/android/uhabits-android/src/androidTest/assets/views/common/HistoryChart/renderDifferentSize.png and /dev/null differ diff --git a/android/uhabits-android/src/androidTest/assets/views/common/HistoryChart/renderTransparent.png b/android/uhabits-android/src/androidTest/assets/views/common/HistoryChart/renderTransparent.png deleted file mode 100644 index 5d997f32c..000000000 Binary files a/android/uhabits-android/src/androidTest/assets/views/common/HistoryChart/renderTransparent.png and /dev/null differ diff --git a/android/uhabits-android/src/androidTest/assets/views/habits/show/HistoryCard/render.png b/android/uhabits-android/src/androidTest/assets/views/habits/show/HistoryCard/render.png index 284fc59df..4eaeb75e5 100644 Binary files a/android/uhabits-android/src/androidTest/assets/views/habits/show/HistoryCard/render.png and b/android/uhabits-android/src/androidTest/assets/views/habits/show/HistoryCard/render.png differ diff --git a/android/uhabits-android/src/androidTest/assets/views/widgets/HistoryWidget/render.png b/android/uhabits-android/src/androidTest/assets/views/widgets/HistoryWidget/render.png index 4239c0ca3..a6ed0060b 100644 Binary files a/android/uhabits-android/src/androidTest/assets/views/widgets/HistoryWidget/render.png and b/android/uhabits-android/src/androidTest/assets/views/widgets/HistoryWidget/render.png differ diff --git a/android/uhabits-android/src/androidTest/java/org/isoron/platform/gui/AndroidCanvasTest.kt b/android/uhabits-android/src/androidTest/java/org/isoron/platform/gui/AndroidCanvasTest.kt index 4bbb59b87..e34e14ae2 100644 --- a/android/uhabits-android/src/androidTest/java/org/isoron/platform/gui/AndroidCanvasTest.kt +++ b/android/uhabits-android/src/androidTest/java/org/isoron/platform/gui/AndroidCanvasTest.kt @@ -30,7 +30,7 @@ class AndroidCanvasTest : BaseViewTest() { val bmp = Bitmap.createBitmap(1000, 800, Bitmap.Config.ARGB_8888) val canvas = AndroidCanvas() canvas.context = testContext - canvas.density = 2.0 + canvas.innerDensity = 2.0 canvas.innerCanvas = android.graphics.Canvas(bmp) canvas.innerBitmap = bmp canvas.drawTestImage() diff --git a/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/HistoryChartTest.java b/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/HistoryChartTest.java deleted file mode 100644 index ac16fa3a9..000000000 --- a/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/HistoryChartTest.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright (C) 2016 Álinson Santos Xavier - * - * 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 . - */ - -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"); - } -} diff --git a/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/show/views/HistoryCardViewTest.kt b/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/show/views/HistoryCardViewTest.kt index b39fb51dd..ffd2ac4db 100644 --- a/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/show/views/HistoryCardViewTest.kt +++ b/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/show/views/HistoryCardViewTest.kt @@ -25,6 +25,7 @@ import androidx.test.filters.MediumTest import org.isoron.uhabits.BaseViewTest import org.isoron.uhabits.R 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.Test import org.junit.runner.RunWith @@ -47,7 +48,8 @@ class HistoryCardViewTest : BaseViewTest() { HistoryCardPresenter().present( habit = habit, firstWeekday = 1, - isSkipEnabled = false + isSkipEnabled = false, + theme = LightTheme(), ) ) measureView(view, 800f, 600f) diff --git a/android/uhabits-android/src/main/java/org/isoron/platform/gui/AndroidCanvas.kt b/android/uhabits-android/src/main/java/org/isoron/platform/gui/AndroidCanvas.kt index 9d9c38743..472632690 100644 --- a/android/uhabits-android/src/main/java/org/isoron/platform/gui/AndroidCanvas.kt +++ b/android/uhabits-android/src/main/java/org/isoron/platform/gui/AndroidCanvas.kt @@ -29,19 +29,24 @@ import org.isoron.uhabits.utils.InterfaceUtils.getFontAwesome class AndroidCanvas : Canvas { - lateinit var innerCanvas: android.graphics.Canvas lateinit var context: Context + + lateinit var innerCanvas: android.graphics.Canvas var innerBitmap: Bitmap? = null - var density = 1.0 + var innerDensity = 1.0 + var innerWidth = 0 + var innerHeight = 0 + var paint = Paint().apply { isAntiAlias = true } var textPaint = TextPaint().apply { isAntiAlias = true + textAlign = Paint.Align.CENTER } var textBounds = Rect() - private fun Double.toDp() = (this * density).toFloat() + private fun Double.toDp() = (this * innerDensity).toFloat() override fun setColor(color: Color) { paint.color = color.toInt() @@ -73,6 +78,25 @@ class AndroidCanvas : Canvas { 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) { paint.style = Paint.Style.STROKE rect(x, y, width, height) @@ -89,11 +113,11 @@ class AndroidCanvas : Canvas { } override fun getHeight(): Double { - return innerCanvas.height / density + return innerHeight / innerDensity } override fun getWidth(): Double { - return innerCanvas.width / density + return innerWidth / innerDensity } override fun setFont(font: Font) { diff --git a/android/uhabits-android/src/main/java/org/isoron/platform/gui/AndroidDataView.kt b/android/uhabits-android/src/main/java/org/isoron/platform/gui/AndroidDataView.kt index ec4ba4dc7..b1cd1daf5 100644 --- a/android/uhabits-android/src/main/java/org/isoron/platform/gui/AndroidDataView.kt +++ b/android/uhabits-android/src/main/java/org/isoron/platform/gui/AndroidDataView.kt @@ -31,7 +31,7 @@ import android.widget.Scroller */ class AndroidDataView( context: Context, - attrs: AttributeSet, + attrs: AttributeSet? = null, ) : AndroidView(context, attrs), GestureDetector.OnGestureListener, ValueAnimator.AnimatorUpdateListener { @@ -99,7 +99,7 @@ class AndroidDataView( } 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) if (newDataOffset != view.dataOffset) { view.dataOffset = newDataOffset diff --git a/android/uhabits-android/src/main/java/org/isoron/platform/gui/AndroidTestView.kt b/android/uhabits-android/src/main/java/org/isoron/platform/gui/AndroidTestView.kt index 9f7a532de..36d665245 100644 --- a/android/uhabits-android/src/main/java/org/isoron/platform/gui/AndroidTestView.kt +++ b/android/uhabits-android/src/main/java/org/isoron/platform/gui/AndroidTestView.kt @@ -28,7 +28,7 @@ class AndroidTestView(context: Context, attrs: AttributeSet) : android.view.View override fun onDraw(canvas: android.graphics.Canvas) { this.canvas.context = context this.canvas.innerCanvas = canvas - this.canvas.density = resources.displayMetrics.density.toDouble() + this.canvas.innerDensity = resources.displayMetrics.density.toDouble() this.canvas.drawTestImage() } } diff --git a/android/uhabits-android/src/main/java/org/isoron/platform/gui/AndroidView.kt b/android/uhabits-android/src/main/java/org/isoron/platform/gui/AndroidView.kt index 9709e2fc1..32cc8cb26 100644 --- a/android/uhabits-android/src/main/java/org/isoron/platform/gui/AndroidView.kt +++ b/android/uhabits-android/src/main/java/org/isoron/platform/gui/AndroidView.kt @@ -24,7 +24,7 @@ import android.util.AttributeSet open class AndroidView( context: Context, - attrs: AttributeSet, + attrs: AttributeSet? = null, ) : android.view.View(context, attrs) { lateinit var view: T @@ -33,7 +33,9 @@ open class AndroidView( override fun onDraw(canvas: android.graphics.Canvas) { this.canvas.context = context 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) } } diff --git a/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/dialogs/HistoryEditorDialog.java b/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/dialogs/HistoryEditorDialog.java index 8996a9d9a..a5a0f7665 100644 --- a/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/dialogs/HistoryEditorDialog.java +++ b/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/dialogs/HistoryEditorDialog.java @@ -30,17 +30,17 @@ import androidx.appcompat.app.*; import android.util.*; import org.isoron.uhabits.*; -import org.isoron.uhabits.activities.common.views.*; import org.isoron.uhabits.core.commands.*; import org.isoron.uhabits.core.models.*; import org.isoron.uhabits.core.preferences.*; import org.isoron.uhabits.core.tasks.*; import org.isoron.uhabits.core.ui.callbacks.*; import org.isoron.uhabits.core.ui.screens.habits.show.views.*; +import org.isoron.uhabits.core.ui.views.*; import org.isoron.uhabits.utils.*; import org.jetbrains.annotations.*; -import static org.isoron.uhabits.utils.InterfaceUtils.*; +import java.util.*; public class HistoryEditorDialog extends AppCompatDialogFragment implements DialogInterface.OnClickListener, CommandRunner.Listener @@ -92,31 +92,31 @@ public class HistoryEditorDialog extends AppCompatDialogFragment commandRunner = app.getComponent().getCommandRunner(); prefs = app.getComponent().getPreferences(); - historyChart = new HistoryChart(context); - historyChart.setOnToggleCheckmarkListener(onToggleCheckmarkListener); - historyChart.setFirstWeekday(prefs.getFirstWeekday()); - historyChart.setSkipEnabled(prefs.isSkipEnabled()); - - if (savedInstanceState != null) - { - long id = savedInstanceState.getLong("habit", -1); - if (id > 0) this.habit = habitList.getById(id); - historyChart.onRestoreInstanceState( - savedInstanceState.getParcelable("historyChart")); - } - - int padding = - (int) getDimension(getContext(), R.dimen.history_editor_padding); - - historyChart.setPadding(padding, 0, padding, 0); - historyChart.setIsEditable(true); - +// historyChart = new HistoryChart(context); +// historyChart.setOnToggleCheckmarkListener(onToggleCheckmarkListener); +// historyChart.setFirstWeekday(prefs.getFirstWeekday()); +// historyChart.setSkipEnabled(prefs.isSkipEnabled()); + +// if (savedInstanceState != null) +// { +// long id = savedInstanceState.getLong("habit", -1); +// if (id > 0) this.habit = habitList.getById(id); +// historyChart.onRestoreInstanceState( +// savedInstanceState.getParcelable("historyChart")); +// } +// +// int padding = +// (int) getDimension(getContext(), R.dimen.history_editor_padding); +// +// historyChart.setPadding(padding, 0, padding, 0); +// historyChart.setIsEditable(true); +// AlertDialog.Builder builder = new AlertDialog.Builder(context); builder .setTitle(R.string.history) - .setView(historyChart) +// .setView(historyChart) .setPositiveButton(android.R.string.ok, this); - +// return builder.create(); } @@ -146,8 +146,8 @@ public class HistoryEditorDialog extends AppCompatDialogFragment @Override public void onSaveInstanceState(Bundle outState) { - outState.putLong("habit", habit.getId()); - outState.putParcelable("historyChart", historyChart.onSaveInstanceState()); +// outState.putLong("habit", habit.getId()); +// outState.putParcelable("historyChart", historyChart.onSaveInstanceState()); } public void setOnToggleCheckmarkListener(@NonNull OnToggleCheckmarkListener onToggleCheckmarkListener) @@ -174,7 +174,7 @@ public class HistoryEditorDialog extends AppCompatDialogFragment private class RefreshTask implements Task { - public int[] checkmarks; + public List checkmarks; @Override public void doInBackground() @@ -182,9 +182,10 @@ public class HistoryEditorDialog extends AppCompatDialogFragment HistoryCardViewModel model = new HistoryCardPresenter().present( habit, prefs.getFirstWeekday(), - prefs.isSkipEnabled() + prefs.isSkipEnabled(), + new LightTheme() ); - checkmarks = model.getEntries(); + checkmarks = model.getSeries(); } @Override @@ -194,9 +195,9 @@ public class HistoryEditorDialog extends AppCompatDialogFragment return; int color = PaletteUtilsKt.toThemedAndroidColor(habit.getColor(), getContext()); - historyChart.setColor(color); - historyChart.setEntries(checkmarks); - historyChart.setNumerical(habit.isNumerical()); +// historyChart.setColor(color); +// historyChart.setEntries(checkmarks); +// historyChart.setNumerical(habit.isNumerical()); } } } diff --git a/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/HistoryChart.java b/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/HistoryChart.java deleted file mode 100644 index 8a38bb038..000000000 --- a/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/HistoryChart.java +++ /dev/null @@ -1,553 +0,0 @@ -/* - * Copyright (C) 2016 Álinson Santos Xavier - * - * 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 . - */ - -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); - } -} diff --git a/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/HistoryCardView.kt b/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/HistoryCardView.kt index 8c6b55471..8e2533d74 100644 --- a/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/HistoryCardView.kt +++ b/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/HistoryCardView.kt @@ -22,9 +22,12 @@ import android.content.Context import android.util.AttributeSet import android.view.LayoutInflater 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.views.HistoryChart import org.isoron.uhabits.databinding.ShowHabitHistoryBinding import org.isoron.uhabits.utils.toThemedAndroidColor +import java.util.Locale 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) { - binding.historyChart.setFirstWeekday(data.firstWeekday) - binding.historyChart.setSkipEnabled(data.isSkipEnabled) - binding.historyChart.setEntries(data.entries) + val androidColor = data.color.toThemedAndroidColor(context) binding.title.setTextColor(androidColor) - binding.historyChart.setColor(androidColor) - if (data.isNumerical) { - binding.historyChart.setNumerical(true) + binding.chart.view = HistoryChart( + today = data.today, + 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) + // } } } diff --git a/android/uhabits-android/src/main/java/org/isoron/uhabits/widgets/HistoryWidget.kt b/android/uhabits-android/src/main/java/org/isoron/uhabits/widgets/HistoryWidget.kt index d1393d5be..5f69d9b53 100644 --- a/android/uhabits-android/src/main/java/org/isoron/uhabits/widgets/HistoryWidget.kt +++ b/android/uhabits-android/src/main/java/org/isoron/uhabits/widgets/HistoryWidget.kt @@ -22,11 +22,15 @@ package org.isoron.uhabits.widgets import android.app.PendingIntent import android.content.Context 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.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 java.util.Locale class HistoryWidget( context: Context, @@ -47,18 +51,25 @@ class HistoryWidget( habit = habit, isSkipEnabled = prefs.isSkipEnabled, firstWeekday = prefs.firstWeekday, + theme = DarkTheme(), ) - (widgetView.dataView as HistoryChart).apply { - setFirstWeekday(model.firstWeekday) - setSkipEnabled(model.isSkipEnabled) - setColor(model.color.toThemedAndroidColor(context)) - setEntries(model.entries) - setNumerical(model.isNumerical) + (widgetView.dataView as AndroidDataView).apply { + (this.view as HistoryChart).series = model.series } } 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) } diff --git a/android/uhabits-android/src/main/res/layout/show_habit_history.xml b/android/uhabits-android/src/main/res/layout/show_habit_history.xml index 974ddb850..fa4743e44 100644 --- a/android/uhabits-android/src/main/res/layout/show_habit_history.xml +++ b/android/uhabits-android/src/main/res/layout/show_habit_history.xml @@ -29,8 +29,8 @@ style="@style/CardHeader" android:text="@string/calendar"/> - diff --git a/android/uhabits-core/assets/test/views/BarChart/base.png b/android/uhabits-core/assets/test/views/BarChart/base.png new file mode 100644 index 000000000..e800b1c84 Binary files /dev/null and b/android/uhabits-core/assets/test/views/BarChart/base.png differ diff --git a/android/uhabits-core/assets/test/views/BarChart/offset.png b/android/uhabits-core/assets/test/views/BarChart/offset.png new file mode 100644 index 000000000..18c3286e9 Binary files /dev/null and b/android/uhabits-core/assets/test/views/BarChart/offset.png differ diff --git a/android/uhabits-core/assets/test/views/CanvasTest.png b/android/uhabits-core/assets/test/views/CanvasTest.png new file mode 100644 index 000000000..75b3d8f20 Binary files /dev/null and b/android/uhabits-core/assets/test/views/CanvasTest.png differ diff --git a/android/uhabits-core/assets/test/views/CheckmarkButton/explicit.png b/android/uhabits-core/assets/test/views/CheckmarkButton/explicit.png new file mode 100644 index 000000000..c2b66f74e Binary files /dev/null and b/android/uhabits-core/assets/test/views/CheckmarkButton/explicit.png differ diff --git a/android/uhabits-core/assets/test/views/CheckmarkButton/implicit.png b/android/uhabits-core/assets/test/views/CheckmarkButton/implicit.png new file mode 100644 index 000000000..acc067024 Binary files /dev/null and b/android/uhabits-core/assets/test/views/CheckmarkButton/implicit.png differ diff --git a/android/uhabits-core/assets/test/views/CheckmarkButton/unchecked.png b/android/uhabits-core/assets/test/views/CheckmarkButton/unchecked.png new file mode 100644 index 000000000..283f32d40 Binary files /dev/null and b/android/uhabits-core/assets/test/views/CheckmarkButton/unchecked.png differ diff --git a/android/uhabits-core/assets/test/views/HabitListHeader/light.png b/android/uhabits-core/assets/test/views/HabitListHeader/light.png new file mode 100644 index 000000000..c146b916d Binary files /dev/null and b/android/uhabits-core/assets/test/views/HabitListHeader/light.png differ diff --git a/android/uhabits-core/assets/test/views/HistoryChart/base.png b/android/uhabits-core/assets/test/views/HistoryChart/base.png new file mode 100644 index 000000000..949a40f35 Binary files /dev/null and b/android/uhabits-core/assets/test/views/HistoryChart/base.png differ diff --git a/android/uhabits-core/assets/test/views/HistoryChart/dark.png b/android/uhabits-core/assets/test/views/HistoryChart/dark.png new file mode 100644 index 000000000..89bedf070 Binary files /dev/null and b/android/uhabits-core/assets/test/views/HistoryChart/dark.png differ diff --git a/android/uhabits-core/assets/test/views/HistoryChart/scroll.png b/android/uhabits-core/assets/test/views/HistoryChart/scroll.png new file mode 100644 index 000000000..b55b147b7 Binary files /dev/null and b/android/uhabits-core/assets/test/views/HistoryChart/scroll.png differ diff --git a/android/uhabits-core/assets/test/views/HistoryChart/small.png b/android/uhabits-core/assets/test/views/HistoryChart/small.png new file mode 100644 index 000000000..07b419c5c Binary files /dev/null and b/android/uhabits-core/assets/test/views/HistoryChart/small.png differ diff --git a/android/uhabits-core/assets/test/views/NumberButton/render_above.png b/android/uhabits-core/assets/test/views/NumberButton/render_above.png new file mode 100644 index 000000000..4673ef0f3 Binary files /dev/null and b/android/uhabits-core/assets/test/views/NumberButton/render_above.png differ diff --git a/android/uhabits-core/assets/test/views/NumberButton/render_below.png b/android/uhabits-core/assets/test/views/NumberButton/render_below.png new file mode 100644 index 000000000..c3ffb68de Binary files /dev/null and b/android/uhabits-core/assets/test/views/NumberButton/render_below.png differ diff --git a/android/uhabits-core/assets/test/views/NumberButton/render_zero.png b/android/uhabits-core/assets/test/views/NumberButton/render_zero.png new file mode 100644 index 000000000..235de2f62 Binary files /dev/null and b/android/uhabits-core/assets/test/views/NumberButton/render_zero.png differ diff --git a/android/uhabits-core/assets/test/views/Ring/draw1.png b/android/uhabits-core/assets/test/views/Ring/draw1.png new file mode 100644 index 000000000..d5e80f455 Binary files /dev/null and b/android/uhabits-core/assets/test/views/Ring/draw1.png differ diff --git a/android/uhabits-core/src/main/java/org/isoron/platform/gui/Canvas.kt b/android/uhabits-core/src/main/java/org/isoron/platform/gui/Canvas.kt index 9a492c958..bd82edbe5 100644 --- a/android/uhabits-core/src/main/java/org/isoron/platform/gui/Canvas.kt +++ b/android/uhabits-core/src/main/java/org/isoron/platform/gui/Canvas.kt @@ -34,6 +34,7 @@ interface Canvas { fun drawLine(x1: Double, y1: Double, x2: Double, y2: Double) fun drawText(text: String, x: Double, y: 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 getHeight(): Double fun getWidth(): Double diff --git a/android/uhabits-core/src/main/java/org/isoron/platform/gui/Colors.kt b/android/uhabits-core/src/main/java/org/isoron/platform/gui/Color.kt similarity index 85% rename from android/uhabits-core/src/main/java/org/isoron/platform/gui/Colors.kt rename to android/uhabits-core/src/main/java/org/isoron/platform/gui/Color.kt index ee11e2c0b..040af1772 100644 --- a/android/uhabits-core/src/main/java/org/isoron/platform/gui/Colors.kt +++ b/android/uhabits-core/src/main/java/org/isoron/platform/gui/Color.kt @@ -19,8 +19,6 @@ package org.isoron.platform.gui -data class PaletteColor(val index: Int) - data class Color( val red: Double, val green: Double, @@ -48,4 +46,11 @@ data class Color( 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 + } } diff --git a/android/uhabits-core/src/main/java/org/isoron/platform/gui/JavaCanvas.kt b/android/uhabits-core/src/main/java/org/isoron/platform/gui/JavaCanvas.kt index 73034cad4..919c11dec 100644 --- a/android/uhabits-core/src/main/java/org/isoron/platform/gui/JavaCanvas.kt +++ b/android/uhabits-core/src/main/java/org/isoron/platform/gui/JavaCanvas.kt @@ -30,6 +30,7 @@ import java.awt.RenderingHints.VALUE_ANTIALIAS_ON import java.awt.RenderingHints.VALUE_FRACTIONALMETRICS_ON import java.awt.RenderingHints.VALUE_TEXT_ANTIALIAS_ON import java.awt.font.FontRenderContext +import java.awt.geom.RoundRectangle2D import java.awt.image.BufferedImage import kotlin.math.roundToInt @@ -115,6 +116,25 @@ class JavaCanvas( 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) { g2d.drawRect(toPixel(x), toPixel(y), toPixel(width), toPixel(height)) } diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/screens/habits/show/ShowHabit.kt b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/screens/habits/show/ShowHabit.kt index bbd8731be..2e18cc0d7 100644 --- a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/screens/habits/show/ShowHabit.kt +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/screens/habits/show/ShowHabit.kt @@ -96,6 +96,7 @@ class ShowHabitPresenter { habit = habit, firstWeekday = preferences.firstWeekday, isSkipEnabled = preferences.isSkipEnabled, + theme = theme, ), bar = BarCardPresenter().present( habit = habit, diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/screens/habits/show/views/HistoryCard.kt b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/screens/habits/show/views/HistoryCard.kt index 94b8040c9..0fbce37e1 100644 --- a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/screens/habits/show/views/HistoryCard.kt +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/screens/habits/show/views/HistoryCard.kt @@ -19,16 +19,24 @@ 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.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 kotlin.math.max data class HistoryCardViewModel( val color: PaletteColor, - val entries: IntArray, val firstWeekday: Int, - val isNumerical: Boolean, - val isSkipEnabled: Boolean, + val series: List, + val theme: Theme, + val today: LocalDate, ) class HistoryCardPresenter { @@ -36,18 +44,37 @@ class HistoryCardPresenter { habit: Habit, firstWeekday: Int, isSkipEnabled: Boolean, + theme: Theme, ): HistoryCardViewModel { val today = DateUtils.getTodayWithOffset() val oldest = habit.computedEntries.getKnown().lastOrNull()?.timestamp ?: today - val entries = - habit.computedEntries.getByInterval(oldest, today).map { it.value }.toIntArray() + val entries = habit.computedEntries.getByInterval(oldest, today) + 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( - entries = entries, color = habit.color, firstWeekday = firstWeekday, - isNumerical = habit.isNumerical, - isSkipEnabled = isSkipEnabled, + today = today.toLocalDate(), + theme = theme, + series = series, ) } } diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/views/CalendarChart.kt b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/views/HistoryChart.kt similarity index 54% rename from android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/views/CalendarChart.kt rename to android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/views/HistoryChart.kt index a5f7d6bd1..193b7011c 100644 --- a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/views/CalendarChart.kt +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/views/HistoryChart.kt @@ -21,53 +21,74 @@ package org.isoron.uhabits.core.ui.views import org.isoron.platform.gui.Canvas import org.isoron.platform.gui.Color +import org.isoron.platform.gui.DataView import org.isoron.platform.gui.TextAlign -import org.isoron.platform.gui.View import org.isoron.platform.time.LocalDate import org.isoron.platform.time.LocalDateFormatter +import org.isoron.uhabits.core.models.PaletteColor import kotlin.math.floor import kotlin.math.round -class CalendarChart( +class HistoryChart( var today: LocalDate, - var color: Color, + var paletteColor: PaletteColor, var theme: Theme, var dateFormatter: LocalDateFormatter -) : View { +) : DataView { - var padding = 5.0 - var backgroundColor = Color(0xFFFFFF) + enum class Square { + ON, + OFF, + DIMMED, + HATCHED, + } + + // Data + var series = listOf() + + // Style + var padding = 0.0 var squareSpacing = 1.0 - var series = listOf() - var scrollPosition = 0 + override var dataOffset = 0 private var squareSize = 0.0 + var lastPrintedMonth = "" + var lastPrintedYear = "" + + override val dataColumnWidth: Double + get() = squareSpacing + squareSize + override fun draw(canvas: Canvas) { val width = canvas.getWidth() val height = canvas.getHeight() - canvas.setColor(backgroundColor) + canvas.setColor(theme.cardBackgroundColor) canvas.fillRect(0.0, 0.0, width, height) squareSize = round((height - 2 * padding) / 8.0) canvas.setFontSize(height * 0.06) val nColumns = floor((width - 2 * padding) / squareSize).toInt() - 2 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) + lastPrintedYear = "" + lastPrintedMonth = "" + + // Draw main columns repeat(nColumns) { column -> val topOffset = topLeftOffset - 7 * column val topDate = topLeftDate.plus(7 * column) drawColumn(canvas, column, topDate, topOffset) } + // Draw week day names canvas.setColor(theme.mediumContrastTextColor) repeat(7) { row -> val date = topLeftDate.plus(row) canvas.setTextAlign(TextAlign.LEFT) canvas.drawText( dateFormatter.shortWeekdayName(date), - padding + nColumns * squareSize + padding, + padding + nColumns * squareSize + squareSpacing * 3, padding + squareSize * (row + 1) + squareSize / 2 ) } @@ -97,22 +118,29 @@ class CalendarChart( } private fun drawHeader(canvas: Canvas, column: Int, date: LocalDate) { - if (date.day >= 8) return - canvas.setColor(theme.mediumContrastTextColor) - if (date.month == 1) { - canvas.drawText( - date.year.toString(), - padding + column * squareSize + squareSize / 2, - padding + squareSize / 2 - ) - } else { - canvas.drawText( - dateFormatter.shortMonthName(date), - padding + column * squareSize + squareSize / 2, - padding + squareSize / 2 - ) + val monthText = dateFormatter.shortMonthName(date) + val yearText = date.year.toString() + val headerText: String + when { + monthText != lastPrintedMonth -> { + headerText = monthText + lastPrintedMonth = monthText + } + yearText != lastPrintedYear -> { + headerText = yearText + lastPrintedYear = headerText + } + else -> { + headerText = "" + } } + canvas.setTextAlign(TextAlign.LEFT) + canvas.drawText( + headerText, + padding + column * squareSize, + padding + squareSize / 2 + ) } private fun drawSquare( @@ -125,19 +153,46 @@ class CalendarChart( offset: Int ) { - var value = if (offset >= series.size) 0.0 else series[offset] - value = round(value * 5.0) / 5.0 + val value = if (offset >= series.size) Square.OFF else series[offset] + 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) - var textColor = backgroundColor + canvas.setColor(squareColor) + 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 - if (squareColor.luminosity > 0.8) - textColor = squareColor.blendWith(theme.highContrastTextColor, 0.5) + val c1 = squareColor.contrast(theme.cardBackgroundColor) + val c2 = squareColor.contrast(theme.mediumContrastTextColor) + val textColor = if (c1 > c2) theme.cardBackgroundColor else theme.mediumContrastTextColor - canvas.setColor(squareColor) - canvas.fillRect(x, y, width, height) canvas.setColor(textColor) + canvas.setTextAlign(TextAlign.CENTER) canvas.drawText(date.day.toString(), x + width / 2, y + width / 2) } } diff --git a/android/uhabits-core/src/test/java/org/isoron/uhabits/core/ui/views/HistoryChartTest.kt b/android/uhabits-core/src/test/java/org/isoron/uhabits/core/ui/views/HistoryChartTest.kt new file mode 100644 index 000000000..277659709 --- /dev/null +++ b/android/uhabits-core/src/test/java/org/isoron/uhabits/core/ui/views/HistoryChartTest.kt @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2016-2019 Álinson Santos Xavier + * + * 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 . + */ + +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) + } +}