Replace HistoryChart by new Kotlin implementation

This commit is contained in:
2021-01-01 09:36:17 -06:00
parent 354c930d85
commit 93a2ec3186
44 changed files with 366 additions and 799 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -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()

View File

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

View File

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

View File

@@ -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) {

View File

@@ -31,7 +31,7 @@ import android.widget.Scroller
*/
class AndroidDataView(
context: Context,
attrs: AttributeSet,
attrs: AttributeSet? = null,
) : AndroidView<DataView>(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

View File

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

View File

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

View File

@@ -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<HistoryChart.Square> 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());
}
}
}

View File

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

View File

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

View File

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

View File

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