Replace HistoryChart by new Kotlin implementation
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 30 KiB |
@@ -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)
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
|
|
||||||
|
// 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);
|
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"/>
|
||||||
|
|
||||||
|
|||||||
BIN
android/uhabits-core/assets/test/views/BarChart/base.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
android/uhabits-core/assets/test/views/BarChart/offset.png
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
BIN
android/uhabits-core/assets/test/views/CanvasTest.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 350 B |
|
After Width: | Height: | Size: 345 B |
|
After Width: | Height: | Size: 424 B |
BIN
android/uhabits-core/assets/test/views/HabitListHeader/light.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
android/uhabits-core/assets/test/views/HistoryChart/base.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
android/uhabits-core/assets/test/views/HistoryChart/dark.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
android/uhabits-core/assets/test/views/HistoryChart/scroll.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
android/uhabits-core/assets/test/views/HistoryChart/small.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
BIN
android/uhabits-core/assets/test/views/Ring/draw1.png
Normal file
|
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,22 +118,29 @@ 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
|
||||||
canvas.drawText(
|
|
||||||
dateFormatter.shortMonthName(date),
|
|
||||||
padding + column * squareSize + squareSize / 2,
|
|
||||||
padding + squareSize / 2
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
yearText != lastPrintedYear -> {
|
||||||
|
headerText = yearText
|
||||||
|
lastPrintedYear = headerText
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
headerText = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
canvas.setTextAlign(TextAlign.LEFT)
|
||||||
|
canvas.drawText(
|
||||||
|
headerText,
|
||||||
|
padding + column * squareSize,
|
||||||
|
padding + squareSize / 2
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun drawSquare(
|
private fun drawSquare(
|
||||||
@@ -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)
|
||||||
var squareColor = color.blendWith(backgroundColor, 1 - value)
|
when (value) {
|
||||||
var textColor = backgroundColor
|
Square.ON -> {
|
||||||
|
squareColor = color
|
||||||
if (value == 0.0) squareColor = theme.lowContrastTextColor
|
}
|
||||||
if (squareColor.luminosity > 0.8)
|
Square.OFF -> {
|
||||||
textColor = squareColor.blendWith(theme.highContrastTextColor, 0.5)
|
squareColor = theme.lowContrastTextColor
|
||||||
|
}
|
||||||
|
Square.DIMMED, Square.HATCHED -> {
|
||||||
|
squareColor = color.blendWith(theme.cardBackgroundColor, 0.5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
canvas.setColor(squareColor)
|
canvas.setColor(squareColor)
|
||||||
canvas.fillRect(x, y, width, height)
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val c1 = squareColor.contrast(theme.cardBackgroundColor)
|
||||||
|
val c2 = squareColor.contrast(theme.mediumContrastTextColor)
|
||||||
|
val textColor = if (c1 > c2) theme.cardBackgroundColor else theme.mediumContrastTextColor
|
||||||
|
|
||||||
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)
|
||||||
|
}
|
||||||
|
}
|
||||||