From 98f9693cffe0ce540490a9afcda8b319817fcbc5 Mon Sep 17 00:00:00 2001 From: Quentin Hibon Date: Wed, 20 Jan 2021 22:30:37 +0100 Subject: [PATCH] Convert views --- .../common/views/BundleSavedState.java | 71 --- .../common/views/BundleSavedState.kt | 62 +++ .../common/views/FrequencyChart.java | 347 -------------- .../activities/common/views/FrequencyChart.kt | 291 +++++++++++ .../views/{HabitChart.java => HabitChart.kt} | 13 +- .../activities/common/views/RingView.java | 272 ----------- .../activities/common/views/RingView.kt | 211 ++++++++ .../activities/common/views/ScoreChart.java | 452 ------------------ .../activities/common/views/ScoreChart.kt | 375 +++++++++++++++ .../common/views/ScrollableChart.java | 245 ---------- .../common/views/ScrollableChart.kt | 196 ++++++++ .../activities/common/views/StreakChart.java | 321 ------------- .../activities/common/views/StreakChart.kt | 246 ++++++++++ .../activities/common/views/TargetChart.java | 226 --------- .../activities/common/views/TargetChart.kt | 186 +++++++ .../habits/list/views/HabitCardListView.kt | 2 +- .../habits/list/views/HabitCardView.kt | 11 +- .../habits/show/views/OverviewCardView.kt | 7 +- .../habits/show/views/ScoreCardView.kt | 2 +- .../org/isoron/uhabits/widgets/ScoreWidget.kt | 2 +- .../widgets/views/CheckmarkWidgetView.kt | 6 +- 21 files changed, 1591 insertions(+), 1953 deletions(-) delete mode 100644 uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/BundleSavedState.java create mode 100644 uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/BundleSavedState.kt delete mode 100644 uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/FrequencyChart.java create mode 100644 uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/FrequencyChart.kt rename uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/{HabitChart.java => HabitChart.kt} (80%) delete mode 100644 uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/RingView.java create mode 100644 uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/RingView.kt delete mode 100644 uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScoreChart.java create mode 100644 uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScoreChart.kt delete mode 100644 uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScrollableChart.java create mode 100644 uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScrollableChart.kt delete mode 100644 uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/StreakChart.java create mode 100644 uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/StreakChart.kt delete mode 100644 uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/TargetChart.java create mode 100644 uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/TargetChart.kt diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/BundleSavedState.java b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/BundleSavedState.java deleted file mode 100644 index 516a0a80b..000000000 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/BundleSavedState.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.activities.common.views; - -import android.os.*; - -import androidx.customview.view.*; - -public class BundleSavedState extends AbsSavedState -{ - public static final Parcelable.Creator CREATOR = - new ClassLoaderCreator() - { - @Override - public BundleSavedState createFromParcel(Parcel source, - ClassLoader loader) - { - return new BundleSavedState(source, loader); - } - - @Override - public BundleSavedState createFromParcel(Parcel source) - { - return null; - } - - @Override - public BundleSavedState[] newArray(int size) - { - return new BundleSavedState[size]; - } - }; - - public final Bundle bundle; - - public BundleSavedState(Parcelable superState, Bundle bundle) - { - super(superState); - this.bundle = bundle; - } - - public BundleSavedState(Parcel source, ClassLoader loader) - { - super(source, loader); - this.bundle = source.readBundle(loader); - } - - @Override - public void writeToParcel(Parcel out, int flags) - { - super.writeToParcel(out, flags); - out.writeBundle(bundle); - } -} \ No newline at end of file diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/BundleSavedState.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/BundleSavedState.kt new file mode 100644 index 000000000..3537259dd --- /dev/null +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/BundleSavedState.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits.activities.common.views + +import android.os.Bundle +import android.os.Parcel +import android.os.Parcelable +import android.os.Parcelable.ClassLoaderCreator +import androidx.customview.view.AbsSavedState + +class BundleSavedState : AbsSavedState { + val bundle: Bundle? + + constructor(superState: Parcelable?, bundle: Bundle?) : super(superState!!) { + this.bundle = bundle + } + + constructor(source: Parcel, loader: ClassLoader?) : super(source, loader) { + bundle = source.readBundle(loader) + } + + override fun writeToParcel(out: Parcel, flags: Int) { + super.writeToParcel(out, flags) + out.writeBundle(bundle) + } + + companion object { + val CREATOR: Parcelable.Creator = + object : ClassLoaderCreator { + override fun createFromParcel( + source: Parcel, + loader: ClassLoader + ): BundleSavedState { + return BundleSavedState(source, loader) + } + + override fun createFromParcel(source: Parcel): BundleSavedState? { + return null + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } + } +} diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/FrequencyChart.java b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/FrequencyChart.java deleted file mode 100644 index f9e095b9c..000000000 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/FrequencyChart.java +++ /dev/null @@ -1,347 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.activities.common.views; - -import android.content.*; -import android.graphics.*; -import android.util.*; - -import androidx.annotation.NonNull; - -import org.isoron.uhabits.*; -import org.isoron.uhabits.core.models.*; -import org.isoron.uhabits.core.utils.*; -import org.isoron.uhabits.utils.*; - -import java.text.*; -import java.util.*; - -public class FrequencyChart extends ScrollableChart -{ - private Paint pGrid; - - private float em; - - private SimpleDateFormat dfMonth; - - private SimpleDateFormat dfYear; - - private Paint pText, pGraph; - - private RectF rect, prevRect; - - private int baseSize; - - private int paddingTop; - - private float columnWidth; - - private int columnHeight; - - private int nColumns; - - private int textColor; - - private int gridColor; - - private int[] colors; - - private int primaryColor; - - private boolean isBackgroundTransparent; - - @NonNull - private HashMap frequency; - - private int maxFreq; - - private int firstWeekday = Calendar.SUNDAY; - - public FrequencyChart(Context context) - { - super(context); - init(); - } - - public FrequencyChart(Context context, AttributeSet attrs) - { - super(context, attrs); - this.frequency = new HashMap<>(); - init(); - } - - public void setColor(int color) - { - this.primaryColor = color; - initColors(); - postInvalidate(); - } - - public void setFrequency(HashMap frequency) - { - this.frequency = frequency; - maxFreq = getMaxFreq(frequency); - postInvalidate(); - } - - public void setFirstWeekday(int firstWeekday) - { - this.firstWeekday = firstWeekday; - postInvalidate(); - } - - private int getMaxFreq(HashMap frequency) - { - int maxValue = 1; - - for (Integer[] values : frequency.values()) - for (Integer value : values) - maxValue = Math.max(value, maxValue); - - return maxValue; - } - - public void setIsBackgroundTransparent(boolean isBackgroundTransparent) - { - this.isBackgroundTransparent = isBackgroundTransparent; - initColors(); - } - - protected void initPaints() - { - pText = new Paint(); - pText.setAntiAlias(true); - - pGraph = new Paint(); - pGraph.setTextAlign(Paint.Align.CENTER); - pGraph.setAntiAlias(true); - - pGrid = new Paint(); - pGrid.setAntiAlias(true); - } - - @Override - protected void onDraw(Canvas canvas) - { - super.onDraw(canvas); - - rect.set(0, 0, nColumns * columnWidth, columnHeight); - rect.offset(0, paddingTop); - - drawGrid(canvas, rect); - - pText.setTextAlign(Paint.Align.CENTER); - pText.setColor(textColor); - pGraph.setColor(primaryColor); - prevRect.setEmpty(); - - GregorianCalendar currentDate = DateUtils.getStartOfTodayCalendarWithOffset(); - currentDate.set(Calendar.DAY_OF_MONTH, 1); - currentDate.add(Calendar.MONTH, -nColumns + 2 - getDataOffset()); - - for (int i = 0; i < nColumns - 1; i++) - { - rect.set(0, 0, columnWidth, columnHeight); - rect.offset(i * columnWidth, 0); - - drawColumn(canvas, rect, currentDate); - currentDate.add(Calendar.MONTH, 1); - } - } - - @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 < 9) height = 200; - - baseSize = height / 8; - setScrollerBucketSize(baseSize); - - pText.setTextSize(baseSize * 0.4f); - pGraph.setTextSize(baseSize * 0.4f); - pGraph.setStrokeWidth(baseSize * 0.1f); - pGrid.setStrokeWidth(baseSize * 0.05f); - em = pText.getFontSpacing(); - - columnWidth = baseSize; - columnWidth = Math.max(columnWidth, getMaxMonthWidth() * 1.2f); - - columnHeight = 8 * baseSize; - nColumns = (int) (width / columnWidth); - paddingTop = 0; - } - - private void drawColumn(Canvas canvas, RectF rect, GregorianCalendar date) - { - Integer[] values = frequency.get(new Timestamp(date)); - float rowHeight = rect.height() / 8.0f; - prevRect.set(rect); - - Integer[] localeWeekdayList = DateUtils.getWeekdaySequence(firstWeekday); - for (int j = 0; j < localeWeekdayList.length; j++) - { - rect.set(0, 0, baseSize, baseSize); - rect.offset(prevRect.left, prevRect.top + baseSize * j); - - int i = localeWeekdayList[j] % 7; - if (values != null) drawMarker(canvas, rect, values[i]); - - rect.offset(0, rowHeight); - } - - drawFooter(canvas, rect, date); - } - - private void drawFooter(Canvas canvas, RectF rect, GregorianCalendar date) - { - Date time = date.getTime(); - - canvas.drawText(dfMonth.format(time), rect.centerX(), - rect.centerY() - 0.1f * em, pText); - - if (date.get(Calendar.MONTH) == 1) - canvas.drawText(dfYear.format(time), rect.centerX(), - rect.centerY() + 0.9f * em, pText); - } - - private void drawGrid(Canvas canvas, RectF rGrid) - { - int nRows = 7; - float rowHeight = rGrid.height() / (nRows + 1); - - pText.setTextAlign(Paint.Align.LEFT); - pText.setColor(textColor); - pGrid.setColor(gridColor); - - for (String day : DateUtils.getShortWeekdayNames(firstWeekday)) - { - canvas.drawText(day, rGrid.right - columnWidth, - rGrid.top + rowHeight / 2 + 0.25f * em, pText); - - pGrid.setStrokeWidth(1f); - canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top, - pGrid); - - rGrid.offset(0, rowHeight); - } - - canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top, pGrid); - } - - private void drawMarker(Canvas canvas, RectF rect, Integer value) - { - float padding = rect.height() * 0.2f; - // maximal allowed mark radius - float maxRadius = (rect.height() - 2 * padding) / 2.0f; - // the real mark radius is scaled down by a factor depending on the maximal frequency - float scale = 1.0f/maxFreq * value; - float radius = maxRadius * scale; - - int colorIndex = Math.min(colors.length - 1, Math.round((colors.length - 1) * scale)); - pGraph.setColor(colors[colorIndex]); - canvas.drawCircle(rect.centerX(), rect.centerY(), radius, pGraph); - } - - private float getMaxMonthWidth() - { - float maxMonthWidth = 0; - GregorianCalendar day = DateUtils.getStartOfTodayCalendarWithOffset(); - - for (int i = 0; i < 12; i++) - { - day.set(Calendar.MONTH, i); - float monthWidth = pText.measureText(dfMonth.format(day.getTime())); - maxMonthWidth = Math.max(maxMonthWidth, monthWidth); - } - - return maxMonthWidth; - } - - private void init() - { - initPaints(); - initColors(); - initDateFormats(); - initRects(); - } - - private void initColors() - { - StyledResources res = new StyledResources(getContext()); - textColor = res.getColor(R.attr.mediumContrastTextColor); - gridColor = res.getColor(R.attr.lowContrastTextColor); - - colors = new int[4]; - colors[0] = gridColor; - colors[3] = primaryColor; - colors[1] = ColorUtils.mixColors(colors[0], colors[3], 0.66f); - colors[2] = ColorUtils.mixColors(colors[0], colors[3], 0.33f); - } - - 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() - { - rect = new RectF(); - prevRect = new RectF(); - } - - public void populateWithRandomData() - { - GregorianCalendar date = DateUtils.getStartOfTodayCalendar(); - date.set(Calendar.DAY_OF_MONTH, 1); - Random rand = new Random(); - frequency.clear(); - - for (int i = 0; i < 40; i++) - { - Integer values[] = new Integer[7]; - for (int j = 0; j < 7; j++) - values[j] = rand.nextInt(5); - - frequency.put(new Timestamp(date), values); - date.add(Calendar.MONTH, -1); - } - maxFreq = getMaxFreq(frequency); - } -} diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/FrequencyChart.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/FrequencyChart.kt new file mode 100644 index 000000000..f0c85d036 --- /dev/null +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/FrequencyChart.kt @@ -0,0 +1,291 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits.activities.common.views + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.RectF +import android.util.AttributeSet +import org.isoron.uhabits.R +import org.isoron.uhabits.core.models.Timestamp +import org.isoron.uhabits.core.utils.DateUtils.Companion.getShortWeekdayNames +import org.isoron.uhabits.core.utils.DateUtils.Companion.getStartOfTodayCalendar +import org.isoron.uhabits.core.utils.DateUtils.Companion.getStartOfTodayCalendarWithOffset +import org.isoron.uhabits.core.utils.DateUtils.Companion.getWeekdaySequence +import org.isoron.uhabits.utils.ColorUtils.mixColors +import org.isoron.uhabits.utils.StyledResources +import org.isoron.uhabits.utils.toSimpleDataFormat +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.GregorianCalendar +import java.util.Locale +import java.util.Random +import kotlin.collections.HashMap + +class FrequencyChart : ScrollableChart { + private var pGrid: Paint? = null + private var em = 0f + private var dfMonth: SimpleDateFormat? = null + private var dfYear: SimpleDateFormat? = null + private var pText: Paint? = null + private var pGraph: Paint? = null + private var rect: RectF? = null + private var prevRect: RectF? = null + private var baseSize = 0 + private var internalPaddingTop = 0 + private var columnWidth = 0f + private var columnHeight = 0 + private var nColumns = 0 + private var textColor = 0 + private var gridColor = 0 + private lateinit var colors: IntArray + private var primaryColor = 0 + private var isBackgroundTransparent = false + private lateinit var frequency: HashMap> + private var maxFreq = 0 + private var firstWeekday = Calendar.SUNDAY + + constructor(context: Context?) : super(context) { + init() + } + + constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) { + frequency = HashMap() + init() + } + + fun setColor(color: Int) { + primaryColor = color + initColors() + postInvalidate() + } + + fun setFrequency(frequency: java.util.HashMap>) { + this.frequency = frequency + maxFreq = getMaxFreq(frequency) + postInvalidate() + } + + fun setFirstWeekday(firstWeekday: Int) { + this.firstWeekday = firstWeekday + postInvalidate() + } + + private fun getMaxFreq(frequency: HashMap>): Int { + var maxValue = 1 + for (values in frequency.values) for (value in values) maxValue = Math.max( + value!!, + maxValue + ) + return maxValue + } + + fun setIsBackgroundTransparent(isBackgroundTransparent: Boolean) { + this.isBackgroundTransparent = isBackgroundTransparent + initColors() + } + + protected fun initPaints() { + pText = Paint() + pText!!.isAntiAlias = true + pGraph = Paint() + pGraph!!.textAlign = Paint.Align.CENTER + pGraph!!.isAntiAlias = true + pGrid = Paint() + pGrid!!.isAntiAlias = true + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + rect!![0f, 0f, nColumns * columnWidth] = columnHeight.toFloat() + rect!!.offset(0f, internalPaddingTop.toFloat()) + drawGrid(canvas, rect) + pText!!.textAlign = Paint.Align.CENTER + pText!!.color = textColor + pGraph!!.color = primaryColor + prevRect!!.setEmpty() + val currentDate: GregorianCalendar = + getStartOfTodayCalendarWithOffset() + currentDate[Calendar.DAY_OF_MONTH] = 1 + currentDate.add(Calendar.MONTH, -nColumns + 2 - dataOffset) + for (i in 0 until nColumns - 1) { + rect!![0f, 0f, columnWidth] = columnHeight.toFloat() + rect!!.offset(i * columnWidth, 0f) + drawColumn(canvas, rect, currentDate) + currentDate.add(Calendar.MONTH, 1) + } + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val width = MeasureSpec.getSize(widthMeasureSpec) + val height = MeasureSpec.getSize(heightMeasureSpec) + setMeasuredDimension(width, height) + } + + override fun onSizeChanged( + width: Int, + height: Int, + oldWidth: Int, + oldHeight: Int + ) { + var height = height + if (height < 9) height = 200 + baseSize = height / 8 + setScrollerBucketSize(baseSize) + pText!!.textSize = baseSize * 0.4f + pGraph!!.textSize = baseSize * 0.4f + pGraph!!.strokeWidth = baseSize * 0.1f + pGrid!!.strokeWidth = baseSize * 0.05f + em = pText!!.fontSpacing + columnWidth = baseSize.toFloat() + columnWidth = Math.max(columnWidth, maxMonthWidth * 1.2f) + columnHeight = 8 * baseSize + nColumns = (width / columnWidth).toInt() + internalPaddingTop = 0 + } + + private fun drawColumn(canvas: Canvas, rect: RectF?, date: GregorianCalendar) { + val values = frequency[Timestamp(date)] + val rowHeight = rect!!.height() / 8.0f + prevRect!!.set(rect) + val localeWeekdayList: Array = getWeekdaySequence(firstWeekday) + for (j in localeWeekdayList.indices) { + rect[0f, 0f, baseSize.toFloat()] = baseSize.toFloat() + rect.offset(prevRect!!.left, prevRect!!.top + baseSize * j) + val i = localeWeekdayList[j] % 7 + if (values != null) drawMarker(canvas, rect, values[i]) + rect.offset(0f, rowHeight) + } + drawFooter(canvas, rect, date) + } + + private fun drawFooter(canvas: Canvas, rect: RectF?, date: GregorianCalendar) { + val time = date.time + canvas.drawText( + dfMonth!!.format(time), + rect!!.centerX(), + rect.centerY() - 0.1f * em, + pText!! + ) + if (date[Calendar.MONTH] == 1) canvas.drawText( + dfYear!!.format(time), + rect.centerX(), + rect.centerY() + 0.9f * em, + pText!! + ) + } + + private fun drawGrid(canvas: Canvas, rGrid: RectF?) { + val nRows = 7 + val rowHeight = rGrid!!.height() / (nRows + 1) + pText!!.textAlign = Paint.Align.LEFT + pText!!.color = textColor + pGrid!!.color = gridColor + for (day in getShortWeekdayNames(firstWeekday)) { + canvas.drawText( + day, + rGrid.right - columnWidth, + rGrid.top + rowHeight / 2 + 0.25f * em, + pText!! + ) + pGrid!!.strokeWidth = 1f + canvas.drawLine( + rGrid.left, + rGrid.top, + rGrid.right, + rGrid.top, + pGrid!! + ) + rGrid.offset(0f, rowHeight) + } + canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top, pGrid!!) + } + + private fun drawMarker(canvas: Canvas, rect: RectF?, value: Int?) { + val padding = rect!!.height() * 0.2f + // maximal allowed mark radius + val maxRadius = (rect.height() - 2 * padding) / 2.0f + // the real mark radius is scaled down by a factor depending on the maximal frequency + val scale = 1.0f / maxFreq * value!! + val radius = maxRadius * scale + val colorIndex = Math.min(colors.size - 1, Math.round((colors.size - 1) * scale)) + pGraph!!.color = colors[colorIndex] + canvas.drawCircle(rect.centerX(), rect.centerY(), radius, pGraph!!) + } + + private val maxMonthWidth: Float + private get() { + var maxMonthWidth = 0f + val day: GregorianCalendar = + getStartOfTodayCalendarWithOffset() + for (i in 0..11) { + day[Calendar.MONTH] = i + val monthWidth = pText!!.measureText(dfMonth!!.format(day.time)) + maxMonthWidth = Math.max(maxMonthWidth, monthWidth) + } + return maxMonthWidth + } + + private fun init() { + initPaints() + initColors() + initDateFormats() + initRects() + } + + private fun initColors() { + val res = StyledResources(context) + textColor = res.getColor(R.attr.mediumContrastTextColor) + gridColor = res.getColor(R.attr.lowContrastTextColor) + colors = IntArray(4) + colors[0] = gridColor + colors[3] = primaryColor + colors[1] = mixColors(colors[0], colors[3], 0.66f) + colors[2] = mixColors(colors[0], colors[3], 0.33f) + } + + private fun initDateFormats() { + if (isInEditMode) { + dfMonth = SimpleDateFormat("MMM", Locale.getDefault()) + dfYear = SimpleDateFormat("yyyy", Locale.getDefault()) + } else { + dfMonth = "MMM".toSimpleDataFormat() + dfYear = "yyyy".toSimpleDataFormat() + } + } + + private fun initRects() { + rect = RectF() + prevRect = RectF() + } + + fun populateWithRandomData() { + val date: GregorianCalendar = getStartOfTodayCalendar() + date[Calendar.DAY_OF_MONTH] = 1 + val rand = Random() + frequency.clear() + for (i in 0..39) { + val values = IntArray(7) { rand.nextInt(5) }.toTypedArray() + frequency[Timestamp(date)] = values + date.add(Calendar.MONTH, -1) + } + maxFreq = getMaxFreq(frequency) + } +} diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/HabitChart.java b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/HabitChart.kt similarity index 80% rename from uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/HabitChart.java rename to uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/HabitChart.kt index a973da9f0..a7882c80f 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/HabitChart.java +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/HabitChart.kt @@ -16,14 +16,11 @@ * You should have received a copy of the GNU General Public License along * with this program. If not, see . */ +package org.isoron.uhabits.activities.common.views -package org.isoron.uhabits.activities.common.views; +import org.isoron.uhabits.core.models.Habit -import org.isoron.uhabits.core.models.Habit; - -public interface HabitChart -{ - void setHabit(Habit habit); - - void refreshData(); +interface HabitChart { + fun setHabit(habit: Habit?) + fun refreshData() } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/RingView.java b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/RingView.java deleted file mode 100644 index df3b6ff80..000000000 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/RingView.java +++ /dev/null @@ -1,272 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.activities.common.views; - -import android.content.*; -import android.graphics.*; -import android.text.*; -import android.util.*; -import android.view.*; - -import androidx.annotation.Nullable; - -import org.isoron.uhabits.*; -import org.isoron.uhabits.utils.*; - -import static org.isoron.uhabits.utils.AttributeSetUtils.*; -import static org.isoron.uhabits.utils.InterfaceUtils.*; - -public class RingView extends View -{ - public static final PorterDuffXfermode XFERMODE_CLEAR = - new PorterDuffXfermode(PorterDuff.Mode.CLEAR); - - private int color; - - private float precision; - - private float percentage; - - private int diameter; - - private float thickness; - - private RectF rect; - - private TextPaint pRing; - - private Integer backgroundColor; - - private Integer inactiveColor; - - private float em; - - private String text; - - private float textSize; - - private boolean enableFontAwesome; - - @Nullable - private Bitmap drawingCache; - - private Canvas cacheCanvas; - - private boolean isTransparencyEnabled; - - public RingView(Context context) - { - super(context); - - percentage = 0.0f; - precision = 0.01f; - color = PaletteUtils.getAndroidTestColor(0); - thickness = dpToPixels(getContext(), 2); - text = ""; - textSize = getDimension(context, R.dimen.smallTextSize); - - init(); - } - - public RingView(Context ctx, AttributeSet attrs) - { - super(ctx, attrs); - - percentage = getFloatAttribute(ctx, attrs, "percentage", 0); - precision = getFloatAttribute(ctx, attrs, "precision", 0.01f); - - color = getColorAttribute(ctx, attrs, "color", 0); - backgroundColor = getColorAttribute(ctx, attrs, "backgroundColor", null); - inactiveColor = getColorAttribute(ctx, attrs, "inactiveColor", null); - - thickness = getFloatAttribute(ctx, attrs, "thickness", 0); - thickness = dpToPixels(ctx, thickness); - - float defaultTextSize = getDimension(ctx, R.dimen.smallTextSize); - textSize = getFloatAttribute(ctx, attrs, "textSize", defaultTextSize); - textSize = spToPixels(ctx, textSize); - text = getAttribute(ctx, attrs, "text", ""); - - enableFontAwesome = - getBooleanAttribute(ctx, attrs, "enableFontAwesome", false); - - init(); - } - - @Override - public void setBackgroundColor(int backgroundColor) - { - this.backgroundColor = backgroundColor; - invalidate(); - } - - public void setColor(int color) - { - this.color = color; - invalidate(); - } - - public int getColor() - { - return color; - } - - public void setIsTransparencyEnabled(boolean isTransparencyEnabled) - { - this.isTransparencyEnabled = isTransparencyEnabled; - } - - public void setPercentage(float percentage) - { - this.percentage = percentage; - invalidate(); - } - - public void setPrecision(float precision) - { - this.precision = precision; - invalidate(); - } - - public void setText(String text) - { - this.text = text; - invalidate(); - } - - public void setTextSize(float textSize) - { - this.textSize = textSize; - } - - public void setThickness(float thickness) - { - this.thickness = thickness; - invalidate(); - } - - @Override - protected void onDraw(Canvas canvas) - { - super.onDraw(canvas); - Canvas activeCanvas; - - if (isTransparencyEnabled) - { - if (drawingCache == null) reallocateCache(); - activeCanvas = cacheCanvas; - drawingCache.eraseColor(Color.TRANSPARENT); - } - else - { - activeCanvas = canvas; - } - - pRing.setColor(color); - rect.set(0, 0, diameter, diameter); - - float angle = 360 * Math.round(percentage / precision) * precision; - - activeCanvas.drawArc(rect, -90, angle, true, pRing); - - pRing.setColor(inactiveColor); - activeCanvas.drawArc(rect, angle - 90, 360 - angle, true, pRing); - - if (thickness > 0) - { - if (isTransparencyEnabled) pRing.setXfermode(XFERMODE_CLEAR); - else pRing.setColor(backgroundColor); - - rect.inset(thickness, thickness); - activeCanvas.drawArc(rect, 0, 360, true, pRing); - pRing.setXfermode(null); - - pRing.setColor(color); - pRing.setTextSize(textSize); - if (enableFontAwesome) - pRing.setTypeface(getFontAwesome(getContext())); - activeCanvas.drawText(text, rect.centerX(), - rect.centerY() + 0.4f * em, pRing); - } - - if (activeCanvas != canvas) canvas.drawBitmap(drawingCache, 0, 0, null); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) - { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - - int width = MeasureSpec.getSize(widthMeasureSpec); - int height = MeasureSpec.getSize(heightMeasureSpec); - diameter = Math.min(height, width); - - pRing.setTextSize(textSize); - em = pRing.measureText("M"); - - setMeasuredDimension(diameter, diameter); - } - - @Override - protected void onSizeChanged(int w, int h, int oldw, int oldh) - { - super.onSizeChanged(w, h, oldw, oldh); - - if (isTransparencyEnabled) reallocateCache(); - } - - private void init() - { - pRing = new TextPaint(); - pRing.setAntiAlias(true); - pRing.setColor(color); - pRing.setTextAlign(Paint.Align.CENTER); - - StyledResources res = new StyledResources(getContext()); - - if (backgroundColor == null) - backgroundColor = res.getColor(R.attr.cardBgColor); - - if (inactiveColor == null) - inactiveColor = res.getColor(R.attr.highContrastTextColor); - - inactiveColor = ColorUtils.setAlpha(inactiveColor, 0.1f); - - rect = new RectF(); - } - - private void reallocateCache() - { - if (drawingCache != null) drawingCache.recycle(); - drawingCache = - Bitmap.createBitmap(diameter, diameter, Bitmap.Config.ARGB_8888); - cacheCanvas = new Canvas(drawingCache); - } - - public float getPercentage() - { - return percentage; - } - - public float getPrecision() - { - return precision; - } -} diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/RingView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/RingView.kt new file mode 100644 index 000000000..67635bfaf --- /dev/null +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/RingView.kt @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits.activities.common.views + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.PorterDuff +import android.graphics.PorterDuffXfermode +import android.graphics.RectF +import android.text.TextPaint +import android.util.AttributeSet +import android.view.View +import org.isoron.uhabits.R +import org.isoron.uhabits.utils.AttributeSetUtils.getAttribute +import org.isoron.uhabits.utils.AttributeSetUtils.getBooleanAttribute +import org.isoron.uhabits.utils.AttributeSetUtils.getColorAttribute +import org.isoron.uhabits.utils.AttributeSetUtils.getFloatAttribute +import org.isoron.uhabits.utils.ColorUtils.setAlpha +import org.isoron.uhabits.utils.InterfaceUtils.dpToPixels +import org.isoron.uhabits.utils.InterfaceUtils.getDimension +import org.isoron.uhabits.utils.InterfaceUtils.getFontAwesome +import org.isoron.uhabits.utils.InterfaceUtils.spToPixels +import org.isoron.uhabits.utils.PaletteUtils.getAndroidTestColor +import org.isoron.uhabits.utils.StyledResources + +class RingView : View { + private var color: Int + private var precision: Float + private var percentage: Float + private var diameter = 0 + private var thickness: Float + private var rect: RectF? = null + private var pRing: TextPaint? = null + private var backgroundColor: Int? = null + private var inactiveColor: Int? = null + private var em = 0f + private var text: String? + private var textSize: Float + private var enableFontAwesome = false + private var internalDrawingCache: Bitmap? = null + private var cacheCanvas: Canvas? = null + private var isTransparencyEnabled = false + + constructor(context: Context?) : super(context) { + percentage = 0.0f + precision = 0.01f + color = getAndroidTestColor(0) + thickness = dpToPixels(getContext(), 2f) + text = "" + textSize = getDimension(context!!, R.dimen.smallTextSize) + init() + } + + constructor(ctx: Context?, attrs: AttributeSet?) : super(ctx, attrs) { + percentage = getFloatAttribute(ctx!!, attrs!!, "percentage", 0f) + precision = getFloatAttribute(ctx, attrs, "precision", 0.01f) + color = getColorAttribute(ctx, attrs, "color", 0)!! + backgroundColor = getColorAttribute(ctx, attrs, "backgroundColor", null) + inactiveColor = getColorAttribute(ctx, attrs, "inactiveColor", null) + thickness = getFloatAttribute(ctx, attrs, "thickness", 0f) + thickness = dpToPixels(ctx, thickness) + val defaultTextSize = getDimension(ctx, R.dimen.smallTextSize) + textSize = getFloatAttribute(ctx, attrs, "textSize", defaultTextSize) + textSize = spToPixels(ctx, textSize) + text = getAttribute(ctx, attrs, "text", "") + enableFontAwesome = getBooleanAttribute(ctx, attrs, "enableFontAwesome", false) + init() + } + + override fun setBackgroundColor(backgroundColor: Int) { + this.backgroundColor = backgroundColor + invalidate() + } + + fun setColor(color: Int) { + this.color = color + invalidate() + } + + fun getColor(): Int { + return color + } + + fun setIsTransparencyEnabled(isTransparencyEnabled: Boolean) { + this.isTransparencyEnabled = isTransparencyEnabled + } + + fun setPercentage(percentage: Float) { + this.percentage = percentage + invalidate() + } + + fun setPrecision(precision: Float) { + this.precision = precision + invalidate() + } + + fun setText(text: String?) { + this.text = text + invalidate() + } + + fun setTextSize(textSize: Float) { + this.textSize = textSize + } + + fun setThickness(thickness: Float) { + this.thickness = thickness + invalidate() + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + val activeCanvas: Canvas? + if (isTransparencyEnabled) { + if (internalDrawingCache == null) reallocateCache() + activeCanvas = cacheCanvas + internalDrawingCache!!.eraseColor(Color.TRANSPARENT) + } else { + activeCanvas = canvas + } + pRing!!.color = color + rect!![0f, 0f, diameter.toFloat()] = diameter.toFloat() + val angle = 360 * Math.round(percentage / precision) * precision + activeCanvas!!.drawArc(rect!!, -90f, angle, true, pRing!!) + pRing!!.color = inactiveColor!! + activeCanvas.drawArc(rect!!, angle - 90, 360 - angle, true, pRing!!) + if (thickness > 0) { + if (isTransparencyEnabled) pRing!!.xfermode = XFERMODE_CLEAR else pRing!!.color = + backgroundColor!! + rect!!.inset(thickness, thickness) + activeCanvas.drawArc(rect!!, 0f, 360f, true, pRing!!) + pRing!!.xfermode = null + pRing!!.color = color + pRing!!.textSize = textSize + if (enableFontAwesome) pRing!!.typeface = getFontAwesome(context) + activeCanvas.drawText( + text!!, + rect!!.centerX(), + rect!!.centerY() + 0.4f * em, + pRing!! + ) + } + if (activeCanvas !== canvas) canvas.drawBitmap(internalDrawingCache!!, 0f, 0f, null) + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + val width = MeasureSpec.getSize(widthMeasureSpec) + val height = MeasureSpec.getSize(heightMeasureSpec) + diameter = Math.min(height, width) + pRing!!.textSize = textSize + em = pRing!!.measureText("M") + setMeasuredDimension(diameter, diameter) + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + if (isTransparencyEnabled) reallocateCache() + } + + private fun init() { + pRing = TextPaint() + pRing!!.isAntiAlias = true + pRing!!.color = color + pRing!!.textAlign = Paint.Align.CENTER + val res = StyledResources(context) + if (backgroundColor == null) backgroundColor = res.getColor(R.attr.cardBgColor) + if (inactiveColor == null) inactiveColor = res.getColor(R.attr.highContrastTextColor) + inactiveColor = setAlpha(inactiveColor!!, 0.1f) + rect = RectF() + } + + private fun reallocateCache() { + if (internalDrawingCache != null) internalDrawingCache!!.recycle() + val newDrawingCache = Bitmap.createBitmap(diameter, diameter, Bitmap.Config.ARGB_8888) + internalDrawingCache = newDrawingCache + cacheCanvas = Canvas(newDrawingCache) + } + + fun getPercentage(): Float { + return percentage + } + + fun getPrecision(): Float { + return precision + } + + companion object { + val XFERMODE_CLEAR = PorterDuffXfermode(PorterDuff.Mode.CLEAR) + } +} diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScoreChart.java b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScoreChart.java deleted file mode 100644 index 60b47c7a3..000000000 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScoreChart.java +++ /dev/null @@ -1,452 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.activities.common.views; - -import android.content.*; -import android.graphics.*; -import android.util.*; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.isoron.uhabits.*; -import org.isoron.uhabits.core.models.*; -import org.isoron.uhabits.core.utils.*; -import org.isoron.uhabits.utils.*; - -import java.text.*; -import java.util.*; - -import static org.isoron.uhabits.utils.InterfaceUtils.*; - -public class ScoreChart extends ScrollableChart -{ - private static final PorterDuffXfermode XFERMODE_CLEAR = - new PorterDuffXfermode(PorterDuff.Mode.CLEAR); - - private static final PorterDuffXfermode XFERMODE_SRC = - new PorterDuffXfermode(PorterDuff.Mode.SRC); - - private Paint pGrid; - - private float em; - - private SimpleDateFormat dfMonth; - - private SimpleDateFormat dfDay; - - private SimpleDateFormat dfYear; - - private Paint pText, pGraph; - - private RectF rect, prevRect; - - private int baseSize; - - private int paddingTop; - - private float columnWidth; - - private int columnHeight; - - private int nColumns; - - private int textColor; - - private int gridColor; - - @Nullable - private List scores; - - private int primaryColor; - - @Deprecated - private int bucketSize = 7; - - private int backgroundColor; - - private Bitmap drawingCache; - - private Canvas cacheCanvas; - - private boolean isTransparencyEnabled; - - private int skipYear = 0; - - private String previousYearText; - - private String previousMonthText; - - public ScoreChart(Context context) - { - super(context); - init(); - } - - public ScoreChart(Context context, AttributeSet attrs) - { - super(context, attrs); - init(); - } - - public void populateWithRandomData() - { - Random random = new Random(); - scores = new LinkedList<>(); - - double previous = 0.5f; - Timestamp timestamp = DateUtils.getToday(); - - for (int i = 1; i < 100; i++) - { - double step = 0.1f; - double current = previous + random.nextDouble() * step * 2 - step; - current = Math.max(0, Math.min(1.0f, current)); - scores.add(new Score(timestamp.minus(i), current)); - previous = current; - } - } - - public void setBucketSize(int bucketSize) - { - this.bucketSize = bucketSize; - postInvalidate(); - } - - public void setIsTransparencyEnabled(boolean enabled) - { - this.isTransparencyEnabled = enabled; - postInvalidate(); - } - - public void setColor(int primaryColor) - { - this.primaryColor = primaryColor; - postInvalidate(); - } - - public void setScores(@NonNull List scores) - { - this.scores = scores; - postInvalidate(); - } - - @Override - protected void onDraw(Canvas canvas) - { - super.onDraw(canvas); - Canvas activeCanvas; - - if (isTransparencyEnabled) - { - if (drawingCache == null) initCache(getWidth(), getHeight()); - - activeCanvas = cacheCanvas; - drawingCache.eraseColor(Color.TRANSPARENT); - } - else - { - activeCanvas = canvas; - } - - if (scores == null) return; - - rect.set(0, 0, nColumns * columnWidth, columnHeight); - rect.offset(0, paddingTop); - - drawGrid(activeCanvas, rect); - - pText.setColor(textColor); - pGraph.setColor(primaryColor); - prevRect.setEmpty(); - - previousMonthText = ""; - previousYearText = ""; - skipYear = 0; - - for (int k = 0; k < nColumns; k++) - { - int offset = nColumns - k - 1 + getDataOffset(); - if (offset >= scores.size()) continue; - - double score = scores.get(offset).getValue(); - Timestamp timestamp = scores.get(offset).getTimestamp(); - - int height = (int) (columnHeight * score); - - rect.set(0, 0, baseSize, baseSize); - rect.offset(k * columnWidth + (columnWidth - baseSize) / 2, - paddingTop + columnHeight - height - baseSize / 2); - - if (!prevRect.isEmpty()) - { - drawLine(activeCanvas, prevRect, rect); - drawMarker(activeCanvas, prevRect); - } - - if (k == nColumns - 1) drawMarker(activeCanvas, rect); - - prevRect.set(rect); - rect.set(0, 0, columnWidth, columnHeight); - rect.offset(k * columnWidth, paddingTop); - - drawFooter(activeCanvas, rect, timestamp); - } - - if (activeCanvas != canvas) canvas.drawBitmap(drawingCache, 0, 0, null); - } - - @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 < 9) height = 200; - - float maxTextSize = getDimension(getContext(), R.dimen.tinyTextSize); - float textSize = height * 0.06f; - pText.setTextSize(Math.min(textSize, maxTextSize)); - em = pText.getFontSpacing(); - - int footerHeight = (int) (3 * em); - paddingTop = (int) (em); - - baseSize = (height - footerHeight - paddingTop) / 8; - columnWidth = baseSize; - columnWidth = Math.max(columnWidth, getMaxDayWidth() * 1.5f); - columnWidth = Math.max(columnWidth, getMaxMonthWidth() * 1.2f); - - nColumns = (int) (width / columnWidth); - columnWidth = (float) width / nColumns; - setScrollerBucketSize((int) columnWidth); - - columnHeight = 8 * baseSize; - - float minStrokeWidth = dpToPixels(getContext(), 1); - pGraph.setTextSize(baseSize * 0.5f); - pGraph.setStrokeWidth(baseSize * 0.1f); - pGrid.setStrokeWidth(Math.min(minStrokeWidth, baseSize * 0.05f)); - - if (isTransparencyEnabled) initCache(width, height); - } - - private void drawFooter(Canvas canvas, RectF rect, Timestamp currentDate) - { - String yearText = dfYear.format(currentDate.toJavaDate()); - String monthText = dfMonth.format(currentDate.toJavaDate()); - String dayText = dfDay.format(currentDate.toJavaDate()); - - GregorianCalendar calendar = currentDate.toCalendar(); - - String text; - int year = calendar.get(Calendar.YEAR); - - boolean shouldPrintYear = true; - if (yearText.equals(previousYearText)) shouldPrintYear = false; - if (bucketSize >= 365 && (year % 2) != 0) shouldPrintYear = false; - - if (skipYear > 0) - { - skipYear--; - shouldPrintYear = false; - } - - if (shouldPrintYear) - { - previousYearText = yearText; - previousMonthText = ""; - - pText.setTextAlign(Paint.Align.CENTER); - canvas.drawText(yearText, rect.centerX(), rect.bottom + em * 2.2f, - pText); - - skipYear = 1; - } - - if (bucketSize < 365) - { - if (!monthText.equals(previousMonthText)) - { - previousMonthText = monthText; - text = monthText; - } - else - { - text = dayText; - } - - pText.setTextAlign(Paint.Align.CENTER); - canvas.drawText(text, rect.centerX(), rect.bottom + em * 1.2f, - pText); - } - } - - private void drawGrid(Canvas canvas, RectF rGrid) - { - int nRows = 5; - float rowHeight = rGrid.height() / nRows; - - pText.setTextAlign(Paint.Align.LEFT); - pText.setColor(textColor); - pGrid.setColor(gridColor); - - for (int i = 0; i < nRows; i++) - { - canvas.drawText(String.format("%d%%", (100 - i * 100 / nRows)), - rGrid.left + 0.5f * em, rGrid.top + 1f * em, pText); - canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top, - pGrid); - rGrid.offset(0, rowHeight); - } - - canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top, pGrid); - } - - private void drawLine(Canvas canvas, RectF rectFrom, RectF rectTo) - { - pGraph.setColor(primaryColor); - canvas.drawLine(rectFrom.centerX(), rectFrom.centerY(), - rectTo.centerX(), rectTo.centerY(), pGraph); - } - - private void drawMarker(Canvas canvas, RectF rect) - { - rect.inset(baseSize * 0.225f, baseSize * 0.225f); - setModeOrColor(pGraph, XFERMODE_CLEAR, backgroundColor); - canvas.drawOval(rect, pGraph); - - rect.inset(baseSize * 0.1f, baseSize * 0.1f); - setModeOrColor(pGraph, XFERMODE_SRC, primaryColor); - canvas.drawOval(rect, pGraph); - -// rect.inset(baseSize * 0.1f, baseSize * 0.1f); -// setModeOrColor(pGraph, XFERMODE_CLEAR, backgroundColor); -// canvas.drawOval(rect, pGraph); - - if (isTransparencyEnabled) pGraph.setXfermode(XFERMODE_SRC); - } - - private float getMaxDayWidth() - { - float maxDayWidth = 0; - GregorianCalendar day = DateUtils.getStartOfTodayCalendarWithOffset(); - - for (int i = 0; i < 28; i++) - { - day.set(Calendar.DAY_OF_MONTH, i); - float monthWidth = pText.measureText(dfMonth.format(day.getTime())); - maxDayWidth = Math.max(maxDayWidth, monthWidth); - } - - return maxDayWidth; - } - - private float getMaxMonthWidth() - { - float maxMonthWidth = 0; - GregorianCalendar day = DateUtils.getStartOfTodayCalendarWithOffset(); - - for (int i = 0; i < 12; i++) - { - day.set(Calendar.MONTH, i); - float monthWidth = pText.measureText(dfMonth.format(day.getTime())); - maxMonthWidth = Math.max(maxMonthWidth, monthWidth); - } - - return maxMonthWidth; - } - - private void init() - { - initPaints(); - initColors(); - initDateFormats(); - initRects(); - } - - private void initCache(int width, int height) - { - if (drawingCache != null) drawingCache.recycle(); - drawingCache = - Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); - cacheCanvas = new Canvas(drawingCache); - } - - private void initColors() - { - StyledResources res = new StyledResources(getContext()); - - primaryColor = Color.BLACK; - textColor = res.getColor(R.attr.mediumContrastTextColor); - gridColor = res.getColor(R.attr.lowContrastTextColor); - backgroundColor = res.getColor(R.attr.cardBgColor); - } - - private void initDateFormats() - { - if (isInEditMode()) - { - dfMonth = new SimpleDateFormat("MMM", Locale.getDefault()); - dfYear = new SimpleDateFormat("yyyy", Locale.getDefault()); - dfDay = new SimpleDateFormat("d", Locale.getDefault()); - - } - else - { - dfMonth = DateExtensionsKt.toSimpleDataFormat("MMM"); - dfYear = DateExtensionsKt.toSimpleDataFormat("yyyy"); - dfDay = DateExtensionsKt.toSimpleDataFormat("d"); - } - } - - private void initPaints() - { - pText = new Paint(); - pText.setAntiAlias(true); - - pGraph = new Paint(); - pGraph.setTextAlign(Paint.Align.CENTER); - pGraph.setAntiAlias(true); - - pGrid = new Paint(); - pGrid.setAntiAlias(true); - } - - private void initRects() - { - rect = new RectF(); - prevRect = new RectF(); - } - - private void setModeOrColor(Paint p, PorterDuffXfermode mode, int color) - { - if (isTransparencyEnabled) p.setXfermode(mode); - else p.setColor(color); - } -} diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScoreChart.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScoreChart.kt new file mode 100644 index 000000000..9a0a5bb76 --- /dev/null +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScoreChart.kt @@ -0,0 +1,375 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits.activities.common.views + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.PorterDuff +import android.graphics.PorterDuffXfermode +import android.graphics.RectF +import android.util.AttributeSet +import org.isoron.uhabits.R +import org.isoron.uhabits.core.models.Score +import org.isoron.uhabits.core.models.Timestamp +import org.isoron.uhabits.core.utils.DateUtils.Companion.getStartOfTodayCalendarWithOffset +import org.isoron.uhabits.core.utils.DateUtils.Companion.getToday +import org.isoron.uhabits.utils.InterfaceUtils.dpToPixels +import org.isoron.uhabits.utils.InterfaceUtils.getDimension +import org.isoron.uhabits.utils.StyledResources +import org.isoron.uhabits.utils.toSimpleDataFormat +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.GregorianCalendar +import java.util.LinkedList +import java.util.Locale +import java.util.Random + +class ScoreChart : ScrollableChart { + private var pGrid: Paint? = null + private var em = 0f + private var dfMonth: SimpleDateFormat? = null + private var dfDay: SimpleDateFormat? = null + private var dfYear: SimpleDateFormat? = null + private var pText: Paint? = null + private var pGraph: Paint? = null + private var rect: RectF? = null + private var prevRect: RectF? = null + private var baseSize = 0 + private var internalPaddingTop = 0 + private var columnWidth = 0f + private var columnHeight = 0 + private var nColumns = 0 + private var textColor = 0 + private var gridColor = 0 + private var scores: MutableList? = null + private var primaryColor = 0 + + @Deprecated("") + private var bucketSize = 7 + private var internalBackgroundColor = 0 + private var internalDrawingCache: Bitmap? = null + private var cacheCanvas: Canvas? = null + private var isTransparencyEnabled = false + private var skipYear = 0 + private var previousYearText: String? = null + private var previousMonthText: String? = null + + constructor(context: Context?) : super(context) { + init() + } + + constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) { + init() + } + + fun populateWithRandomData() { + val random = Random() + val newScores = LinkedList() + var previous = 0.5 + val timestamp: Timestamp = getToday() + for (i in 1..99) { + val step = 0.1 + var current = previous + random.nextDouble() * step * 2 - step + current = Math.max(0.0, Math.min(1.0, current)) + newScores.add(Score(timestamp.minus(i), current)) + previous = current + } + scores = newScores + } + + fun setBucketSize(bucketSize: Int) { + this.bucketSize = bucketSize + postInvalidate() + } + + fun setIsTransparencyEnabled(enabled: Boolean) { + isTransparencyEnabled = enabled + postInvalidate() + } + + fun setColor(primaryColor: Int) { + this.primaryColor = primaryColor + postInvalidate() + } + + fun setScores(scores: MutableList) { + this.scores = scores + postInvalidate() + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + val activeCanvas: Canvas? + if (isTransparencyEnabled) { + if (internalDrawingCache == null) initCache(width, height) + activeCanvas = cacheCanvas + internalDrawingCache!!.eraseColor(Color.TRANSPARENT) + } else { + activeCanvas = canvas + } + if (scores == null) return + rect!![0f, 0f, nColumns * columnWidth] = columnHeight.toFloat() + rect!!.offset(0f, internalPaddingTop.toFloat()) + drawGrid(activeCanvas, rect) + pText!!.color = textColor + pGraph!!.color = primaryColor + prevRect!!.setEmpty() + previousMonthText = "" + previousYearText = "" + skipYear = 0 + for (k in 0 until nColumns) { + val offset = nColumns - k - 1 + dataOffset + if (offset >= scores!!.size) continue + val score = scores!![offset].value + val timestamp = scores!![offset].timestamp + val height = (columnHeight * score).toInt() + rect!![0f, 0f, baseSize.toFloat()] = baseSize.toFloat() + rect!!.offset( + k * columnWidth + (columnWidth - baseSize) / 2, + ( + internalPaddingTop + columnHeight - height - baseSize / 2 + ).toFloat() + ) + if (!prevRect!!.isEmpty) { + drawLine(activeCanvas, prevRect, rect) + drawMarker(activeCanvas, prevRect) + } + if (k == nColumns - 1) drawMarker(activeCanvas, rect) + prevRect!!.set(rect) + rect!![0f, 0f, columnWidth] = columnHeight.toFloat() + rect!!.offset(k * columnWidth, internalPaddingTop.toFloat()) + drawFooter(activeCanvas, rect, timestamp) + } + if (activeCanvas !== canvas) canvas.drawBitmap(internalDrawingCache!!, 0f, 0f, null) + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val width = MeasureSpec.getSize(widthMeasureSpec) + val height = MeasureSpec.getSize(heightMeasureSpec) + setMeasuredDimension(width, height) + } + + override fun onSizeChanged( + width: Int, + height: Int, + oldWidth: Int, + oldHeight: Int + ) { + var height = height + if (height < 9) height = 200 + val maxTextSize = getDimension(context, R.dimen.tinyTextSize) + val textSize = height * 0.06f + pText!!.textSize = Math.min(textSize, maxTextSize) + em = pText!!.fontSpacing + val footerHeight = (3 * em).toInt() + internalPaddingTop = em.toInt() + baseSize = (height - footerHeight - internalPaddingTop) / 8 + columnWidth = baseSize.toFloat() + columnWidth = Math.max(columnWidth, maxDayWidth * 1.5f) + columnWidth = Math.max(columnWidth, maxMonthWidth * 1.2f) + nColumns = (width / columnWidth).toInt() + columnWidth = width.toFloat() / nColumns + setScrollerBucketSize(columnWidth.toInt()) + columnHeight = 8 * baseSize + val minStrokeWidth = dpToPixels(context, 1f) + pGraph!!.textSize = baseSize * 0.5f + pGraph!!.strokeWidth = baseSize * 0.1f + pGrid!!.strokeWidth = Math.min(minStrokeWidth, baseSize * 0.05f) + if (isTransparencyEnabled) initCache(width, height) + } + + private fun drawFooter(canvas: Canvas?, rect: RectF?, currentDate: Timestamp) { + val yearText = dfYear!!.format(currentDate.toJavaDate()) + val monthText = dfMonth!!.format(currentDate.toJavaDate()) + val dayText = dfDay!!.format(currentDate.toJavaDate()) + val calendar = currentDate.toCalendar() + val text: String + val year = calendar[Calendar.YEAR] + var shouldPrintYear = true + if (yearText == previousYearText) shouldPrintYear = false + if (bucketSize >= 365 && year % 2 != 0) shouldPrintYear = false + if (skipYear > 0) { + skipYear-- + shouldPrintYear = false + } + if (shouldPrintYear) { + previousYearText = yearText + previousMonthText = "" + pText!!.textAlign = Paint.Align.CENTER + canvas!!.drawText( + yearText, + rect!!.centerX(), + rect.bottom + em * 2.2f, + pText!! + ) + skipYear = 1 + } + if (bucketSize < 365) { + if (monthText != previousMonthText) { + previousMonthText = monthText + text = monthText + } else { + text = dayText + } + pText!!.textAlign = Paint.Align.CENTER + canvas!!.drawText( + text, + rect!!.centerX(), + rect.bottom + em * 1.2f, + pText!! + ) + } + } + + private fun drawGrid(canvas: Canvas?, rGrid: RectF?) { + val nRows = 5 + val rowHeight = rGrid!!.height() / nRows + pText!!.textAlign = Paint.Align.LEFT + pText!!.color = textColor + pGrid!!.color = gridColor + for (i in 0 until nRows) { + canvas!!.drawText( + String.format("%d%%", 100 - i * 100 / nRows), + rGrid.left + 0.5f * em, + rGrid.top + 1f * em, + pText!! + ) + canvas.drawLine( + rGrid.left, + rGrid.top, + rGrid.right, + rGrid.top, + pGrid!! + ) + rGrid.offset(0f, rowHeight) + } + canvas!!.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top, pGrid!!) + } + + private fun drawLine(canvas: Canvas?, rectFrom: RectF?, rectTo: RectF?) { + pGraph!!.color = primaryColor + canvas!!.drawLine( + rectFrom!!.centerX(), + rectFrom.centerY(), + rectTo!!.centerX(), + rectTo.centerY(), + pGraph!! + ) + } + + private fun drawMarker(canvas: Canvas?, rect: RectF?) { + rect!!.inset(baseSize * 0.225f, baseSize * 0.225f) + setModeOrColor(pGraph, XFERMODE_CLEAR, internalBackgroundColor) + canvas!!.drawOval(rect, pGraph!!) + rect.inset(baseSize * 0.1f, baseSize * 0.1f) + setModeOrColor(pGraph, XFERMODE_SRC, primaryColor) + canvas.drawOval(rect, pGraph!!) + +// rect.inset(baseSize * 0.1f, baseSize * 0.1f); +// setModeOrColor(pGraph, XFERMODE_CLEAR, backgroundColor); +// canvas.drawOval(rect, pGraph); + if (isTransparencyEnabled) pGraph!!.xfermode = XFERMODE_SRC + } + + private val maxDayWidth: Float + private get() { + var maxDayWidth = 0f + val day: GregorianCalendar = + getStartOfTodayCalendarWithOffset() + for (i in 0..27) { + day[Calendar.DAY_OF_MONTH] = i + val monthWidth = pText!!.measureText(dfMonth!!.format(day.time)) + maxDayWidth = Math.max(maxDayWidth, monthWidth) + } + return maxDayWidth + } + private val maxMonthWidth: Float + private get() { + var maxMonthWidth = 0f + val day: GregorianCalendar = + getStartOfTodayCalendarWithOffset() + for (i in 0..11) { + day[Calendar.MONTH] = i + val monthWidth = pText!!.measureText(dfMonth!!.format(day.time)) + maxMonthWidth = Math.max(maxMonthWidth, monthWidth) + } + return maxMonthWidth + } + + private fun init() { + initPaints() + initColors() + initDateFormats() + initRects() + } + + private fun initCache(width: Int, height: Int) { + if (internalDrawingCache != null) internalDrawingCache!!.recycle() + val newDrawingCache = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + internalDrawingCache = newDrawingCache + cacheCanvas = Canvas(newDrawingCache) + } + + private fun initColors() { + val res = StyledResources(context) + primaryColor = Color.BLACK + textColor = res.getColor(R.attr.mediumContrastTextColor) + gridColor = res.getColor(R.attr.lowContrastTextColor) + internalBackgroundColor = res.getColor(R.attr.cardBgColor) + } + + private fun initDateFormats() { + if (isInEditMode) { + dfMonth = SimpleDateFormat("MMM", Locale.getDefault()) + dfYear = SimpleDateFormat("yyyy", Locale.getDefault()) + dfDay = SimpleDateFormat("d", Locale.getDefault()) + } else { + dfMonth = "MMM".toSimpleDataFormat() + dfYear = "yyyy".toSimpleDataFormat() + dfDay = "d".toSimpleDataFormat() + } + } + + private fun initPaints() { + pText = Paint() + pText!!.isAntiAlias = true + pGraph = Paint() + pGraph!!.textAlign = Paint.Align.CENTER + pGraph!!.isAntiAlias = true + pGrid = Paint() + pGrid!!.isAntiAlias = true + } + + private fun initRects() { + rect = RectF() + prevRect = RectF() + } + + private fun setModeOrColor(p: Paint?, mode: PorterDuffXfermode, color: Int) { + if (isTransparencyEnabled) p!!.xfermode = mode else p!!.color = color + } + + companion object { + private val XFERMODE_CLEAR = PorterDuffXfermode(PorterDuff.Mode.CLEAR) + private val XFERMODE_SRC = PorterDuffXfermode(PorterDuff.Mode.SRC) + } +} diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScrollableChart.java b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScrollableChart.java deleted file mode 100644 index 5de149283..000000000 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScrollableChart.java +++ /dev/null @@ -1,245 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.activities.common.views; - -import android.animation.*; -import android.content.*; -import android.os.*; -import android.util.*; -import android.view.*; -import android.widget.*; - -public abstract class ScrollableChart extends View - implements GestureDetector.OnGestureListener, - ValueAnimator.AnimatorUpdateListener -{ - - private int dataOffset; - - private int scrollerBucketSize = 1; - - private int direction = 1; - - private GestureDetector detector; - - private Scroller scroller; - - private ValueAnimator scrollAnimator; - - private ScrollController scrollController; - - private int maxDataOffset = 10000; - - public ScrollableChart(Context context) - { - super(context); - init(context); - } - - public ScrollableChart(Context context, AttributeSet attrs) - { - super(context, attrs); - init(context); - } - - public int getDataOffset() - { - return dataOffset; - } - - @Override - public void onAnimationUpdate(ValueAnimator animation) - { - if (!scroller.isFinished()) - { - scroller.computeScrollOffset(); - updateDataOffset(); - } - else - { - scrollAnimator.cancel(); - } - } - - @Override - public boolean onDown(MotionEvent e) - { - return true; - } - - @Override - public boolean onFling(MotionEvent e1, - MotionEvent e2, - float velocityX, - float velocityY) - { - scroller.fling(scroller.getCurrX(), scroller.getCurrY(), - direction * ((int) velocityX) / 2, 0, 0, getMaxX(), 0, 0); - invalidate(); - - scrollAnimator.setDuration(scroller.getDuration()); - scrollAnimator.start(); - return false; - } - - private int getMaxX() - { - return maxDataOffset * scrollerBucketSize; - } - - @Override - public void onRestoreInstanceState(Parcelable state) - { - if(!(state instanceof BundleSavedState)) - { - super.onRestoreInstanceState(state); - return; - } - - BundleSavedState bss = (BundleSavedState) state; - int x = bss.bundle.getInt("x"); - int y = bss.bundle.getInt("y"); - direction = bss.bundle.getInt("direction"); - dataOffset = bss.bundle.getInt("dataOffset"); - maxDataOffset = bss.bundle.getInt("maxDataOffset"); - scroller.startScroll(0, 0, x, y, 0); - scroller.computeScrollOffset(); - super.onRestoreInstanceState(bss.getSuperState()); - } - - @Override - public Parcelable onSaveInstanceState() - { - Parcelable superState = super.onSaveInstanceState(); - Bundle bundle = new Bundle(); - bundle.putInt("x", scroller.getCurrX()); - bundle.putInt("y", scroller.getCurrY()); - bundle.putInt("dataOffset", dataOffset); - bundle.putInt("direction", direction); - bundle.putInt("maxDataOffset", maxDataOffset); - return new BundleSavedState(superState, bundle); - } - - @Override - public boolean onScroll(MotionEvent e1, MotionEvent e2, float dx, float dy) - { - if (scrollerBucketSize == 0) return false; - - if (Math.abs(dx) > Math.abs(dy)) - { - ViewParent parent = getParent(); - if (parent != null) parent.requestDisallowInterceptTouchEvent(true); - } - - - dx = - direction * dx; - dx = Math.min(dx, getMaxX() - scroller.getCurrX()); - scroller.startScroll(scroller.getCurrX(), scroller.getCurrY(), (int) dx, - (int) dy, 0); - - scroller.computeScrollOffset(); - updateDataOffset(); - return true; - } - - @Override - public void onShowPress(MotionEvent e) - { - - } - - @Override - public boolean onSingleTapUp(MotionEvent e) - { - return false; - } - - @Override - public boolean onTouchEvent(MotionEvent event) - { - return detector.onTouchEvent(event); - } - - public void setScrollDirection(int direction) - { - if (direction != 1 && direction != -1) - throw new IllegalArgumentException(); - this.direction = direction; - } - - @Override - public void onLongPress(MotionEvent e) - { - - } - - public void setMaxDataOffset(int maxDataOffset) - { - this.maxDataOffset = maxDataOffset; - this.dataOffset = Math.min(dataOffset, maxDataOffset); - scrollController.onDataOffsetChanged(this.dataOffset); - postInvalidate(); - } - - public void setScrollController(ScrollController scrollController) - { - this.scrollController = scrollController; - } - - public void setScrollerBucketSize(int scrollerBucketSize) - { - this.scrollerBucketSize = scrollerBucketSize; - } - - private void init(Context context) - { - detector = new GestureDetector(context, this); - scroller = new Scroller(context, null, true); - scrollAnimator = ValueAnimator.ofFloat(0, 1); - scrollAnimator.addUpdateListener(this); - scrollController = new ScrollController() {}; - } - - public void reset() - { - scroller.setFinalX(0); - scroller.computeScrollOffset(); - updateDataOffset(); - } - - private void updateDataOffset() - { - int newDataOffset = scroller.getCurrX() / scrollerBucketSize; - newDataOffset = Math.max(0, newDataOffset); - newDataOffset = Math.min(maxDataOffset, newDataOffset); - - if (newDataOffset != dataOffset) - { - dataOffset = newDataOffset; - scrollController.onDataOffsetChanged(dataOffset); - postInvalidate(); - } - } - - public interface ScrollController - { - default void onDataOffsetChanged(int newDataOffset) {} - } -} diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScrollableChart.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScrollableChart.kt new file mode 100644 index 000000000..a1c86449a --- /dev/null +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScrollableChart.kt @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits.activities.common.views + +import android.animation.ValueAnimator +import android.animation.ValueAnimator.AnimatorUpdateListener +import android.content.Context +import android.os.Bundle +import android.os.Parcelable +import android.util.AttributeSet +import android.view.GestureDetector +import android.view.MotionEvent +import android.view.View +import android.widget.Scroller + +abstract class ScrollableChart : View, GestureDetector.OnGestureListener, AnimatorUpdateListener { + var dataOffset = 0 + private set + private var scrollerBucketSize = 1 + private var direction = 1 + private var detector: GestureDetector? = null + private var scroller: Scroller? = null + private var scrollAnimator: ValueAnimator? = null + private var scrollController: ScrollController? = null + private var maxDataOffset = 10000 + + constructor(context: Context?) : super(context) { + init(context) + } + + constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) { + init(context) + } + + override fun onAnimationUpdate(animation: ValueAnimator) { + if (!scroller!!.isFinished) { + scroller!!.computeScrollOffset() + updateDataOffset() + } else { + scrollAnimator!!.cancel() + } + } + + override fun onDown(e: MotionEvent): Boolean { + return true + } + + override fun onFling( + e1: MotionEvent, + e2: MotionEvent, + velocityX: Float, + velocityY: Float + ): Boolean { + scroller!!.fling( + scroller!!.currX, + scroller!!.currY, + direction * velocityX.toInt() / 2, + 0, + 0, + maxX, + 0, + 0 + ) + invalidate() + scrollAnimator!!.duration = scroller!!.duration.toLong() + scrollAnimator!!.start() + return false + } + + private val maxX: Int + private get() = maxDataOffset * scrollerBucketSize + + public override fun onRestoreInstanceState(state: Parcelable) { + if (state !is BundleSavedState) { + super.onRestoreInstanceState(state) + return + } + val bss = state + val x = bss.bundle!!.getInt("x") + val y = bss.bundle.getInt("y") + direction = bss.bundle.getInt("direction") + dataOffset = bss.bundle.getInt("dataOffset") + maxDataOffset = bss.bundle.getInt("maxDataOffset") + scroller!!.startScroll(0, 0, x, y, 0) + scroller!!.computeScrollOffset() + super.onRestoreInstanceState(bss.superState) + } + + public override fun onSaveInstanceState(): Parcelable? { + val superState = super.onSaveInstanceState() + val bundle = Bundle() + bundle.putInt("x", scroller!!.currX) + bundle.putInt("y", scroller!!.currY) + bundle.putInt("dataOffset", dataOffset) + bundle.putInt("direction", direction) + bundle.putInt("maxDataOffset", maxDataOffset) + return BundleSavedState(superState, bundle) + } + + override fun onScroll(e1: MotionEvent?, e2: MotionEvent?, dx: Float, dy: Float): Boolean { + var dx = dx + if (scrollerBucketSize == 0) return false + if (Math.abs(dx) > Math.abs(dy)) { + val parent = parent + parent?.requestDisallowInterceptTouchEvent(true) + } + dx = -direction * dx + dx = Math.min(dx, (maxX - scroller!!.currX).toFloat()) + scroller!!.startScroll( + scroller!!.currX, + scroller!!.currY, + dx.toInt(), + dy.toInt(), + 0 + ) + scroller!!.computeScrollOffset() + updateDataOffset() + return true + } + + override fun onShowPress(e: MotionEvent) {} + override fun onSingleTapUp(e: MotionEvent): Boolean { + return false + } + + override fun onTouchEvent(event: MotionEvent): Boolean { + return detector!!.onTouchEvent(event) + } + + fun setScrollDirection(direction: Int) { + require(!(direction != 1 && direction != -1)) + this.direction = direction + } + + override fun onLongPress(e: MotionEvent) {} + fun setMaxDataOffset(maxDataOffset: Int) { + this.maxDataOffset = maxDataOffset + dataOffset = Math.min(dataOffset, maxDataOffset) + scrollController!!.onDataOffsetChanged(dataOffset) + postInvalidate() + } + + fun setScrollController(scrollController: ScrollController?) { + this.scrollController = scrollController + } + + fun setScrollerBucketSize(scrollerBucketSize: Int) { + this.scrollerBucketSize = scrollerBucketSize + } + + private fun init(context: Context?) { + detector = GestureDetector(context, this) + scroller = Scroller(context, null, true) + val newScrollAnimator = ValueAnimator.ofFloat(0f, 1f) + newScrollAnimator.addUpdateListener(this) + scrollAnimator = newScrollAnimator + scrollController = object : ScrollController {} + } + + fun reset() { + scroller!!.finalX = 0 + scroller!!.computeScrollOffset() + updateDataOffset() + } + + private fun updateDataOffset() { + var newDataOffset = scroller!!.currX / scrollerBucketSize + newDataOffset = Math.max(0, newDataOffset) + newDataOffset = Math.min(maxDataOffset, newDataOffset) + if (newDataOffset != dataOffset) { + dataOffset = newDataOffset + scrollController!!.onDataOffsetChanged(dataOffset) + postInvalidate() + } + } + + interface ScrollController { + fun onDataOffsetChanged(newDataOffset: Int) {} + } +} diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/StreakChart.java b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/StreakChart.java deleted file mode 100644 index 1ebf43202..000000000 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/StreakChart.java +++ /dev/null @@ -1,321 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.activities.common.views; - -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.RectF; -import android.util.AttributeSet; -import android.view.View; -import android.view.ViewGroup.LayoutParams; - -import org.isoron.uhabits.R; -import org.isoron.uhabits.core.models.Streak; -import org.isoron.uhabits.core.models.Timestamp; -import org.isoron.uhabits.core.utils.DateUtils; -import org.isoron.uhabits.utils.StyledResources; - -import java.text.DateFormat; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; -import java.util.Random; -import java.util.TimeZone; - -import static android.view.View.MeasureSpec.*; -import static org.isoron.uhabits.utils.InterfaceUtils.*; - -public class StreakChart extends View -{ - private Paint paint; - - private long minLength; - - private long maxLength; - - private int[] colors; - - private int[] textColors; - - private RectF rect; - - private int baseSize; - - private int primaryColor; - - private List streaks; - - private boolean isBackgroundTransparent; - - private DateFormat dateFormat; - - private int width; - - private float em; - - private float maxLabelWidth; - - private float textMargin; - - private boolean shouldShowLabels; - - private int textColor; - - public StreakChart(Context context) - { - super(context); - init(); - } - - public StreakChart(Context context, AttributeSet attrs) - { - super(context, attrs); - init(); - } - - /** - * Returns the maximum number of streaks this view is able to show, given - * its current size. - * - * @return max number of visible streaks - */ - public int getMaxStreakCount() - { - return (int) Math.floor(getMeasuredHeight() / baseSize); - } - - public void populateWithRandomData() - { - Timestamp start = DateUtils.getToday(); - List streaks = new LinkedList<>(); - - for (int i = 0; i < 10; i++) - { - int length = new Random().nextInt(100); - Timestamp end = start.plus(length); - streaks.add(new Streak(start, end)); - start = end.plus(1); - } - - setStreaks(streaks); - } - - public void setColor(int color) - { - this.primaryColor = color; - postInvalidate(); - } - - public void setIsBackgroundTransparent(boolean isBackgroundTransparent) - { - this.isBackgroundTransparent = isBackgroundTransparent; - initColors(); - } - - public void setStreaks(List streaks) - { - this.streaks = streaks; - initColors(); - updateMaxMinLengths(); - requestLayout(); - } - - @Override - protected void onDraw(Canvas canvas) - { - super.onDraw(canvas); - if (streaks.size() == 0) return; - - rect.set(0, 0, width, baseSize); - - for (Streak s : streaks) - { - drawRow(canvas, s, rect); - rect.offset(0, baseSize); - } - } - - @Override - protected void onMeasure(int widthSpec, int heightSpec) - { - LayoutParams params = getLayoutParams(); - - if (params != null && params.height == LayoutParams.WRAP_CONTENT) - { - int width = getSize(widthSpec); - int height = streaks.size() * baseSize; - - heightSpec = makeMeasureSpec(height, EXACTLY); - widthSpec = makeMeasureSpec(width, EXACTLY); - } - - setMeasuredDimension(widthSpec, heightSpec); - } - - @Override - protected void onSizeChanged(int width, - int height, - int oldWidth, - int oldHeight) - { - this.width = width; - - Context context = getContext(); - float minTextSize = getDimension(context, R.dimen.tinyTextSize); - float maxTextSize = getDimension(context, R.dimen.regularTextSize); - float textSize = baseSize * 0.5f; - - paint.setTextSize( - Math.max(Math.min(textSize, maxTextSize), minTextSize)); - em = paint.getFontSpacing(); - textMargin = 0.5f * em; - - updateMaxMinLengths(); - } - - private void drawRow(Canvas canvas, Streak streak, RectF rect) - { - if (maxLength == 0) return; - - float percentage = (float) streak.getLength() / maxLength; - float availableWidth = width - 2 * maxLabelWidth; - if (shouldShowLabels) availableWidth -= 2 * textMargin; - - float barWidth = percentage * availableWidth; - float minBarWidth = - paint.measureText(Long.toString(streak.getLength())) + em; - barWidth = Math.max(barWidth, minBarWidth); - - float gap = (width - barWidth) / 2; - float paddingTopBottom = baseSize * 0.05f; - - paint.setColor(percentageToColor(percentage)); - - float round = dpToPixels(getContext(), 2); - canvas.drawRoundRect(rect.left + gap, - rect.top + paddingTopBottom, - rect.right - gap, - rect.bottom - paddingTopBottom, - round, - round, - paint); - - float yOffset = rect.centerY() + 0.3f * em; - - paint.setColor(percentageToTextColor(percentage)); - paint.setTextAlign(Paint.Align.CENTER); - canvas.drawText(Long.toString(streak.getLength()), rect.centerX(), - yOffset, paint); - - if (shouldShowLabels) - { - String startLabel = dateFormat.format(streak.getStart().toJavaDate()); - String endLabel = dateFormat.format(streak.getEnd().toJavaDate()); - - paint.setColor(textColors[1]); - paint.setTextAlign(Paint.Align.RIGHT); - canvas.drawText(startLabel, gap - textMargin, yOffset, paint); - - paint.setTextAlign(Paint.Align.LEFT); - canvas.drawText(endLabel, width - gap + textMargin, yOffset, paint); - } - } - - private void init() - { - initPaints(); - initColors(); - - streaks = Collections.emptyList(); - - dateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM); - if (!isInEditMode()) dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); - rect = new RectF(); - baseSize = getResources().getDimensionPixelSize(R.dimen.baseSize); - } - - private void initColors() - { - int red = Color.red(primaryColor); - int green = Color.green(primaryColor); - int blue = Color.blue(primaryColor); - - StyledResources res = new StyledResources(getContext()); - - colors = new int[4]; - colors[3] = primaryColor; - colors[2] = Color.argb(192, red, green, blue); - colors[1] = Color.argb(96, red, green, blue); - colors[0] = res.getColor(R.attr.lowContrastTextColor); - - textColors = new int[3]; - textColors[2] = res.getColor(R.attr.highContrastReverseTextColor); - textColors[1] = res.getColor(R.attr.mediumContrastTextColor); - textColors[0] = res.getColor(R.attr.lowContrastReverseTextColor); - } - - private void initPaints() - { - paint = new Paint(); - paint.setTextAlign(Paint.Align.CENTER); - paint.setAntiAlias(true); - } - - private int percentageToColor(float percentage) - { - if (percentage >= 1.0f) return colors[3]; - if (percentage >= 0.8f) return colors[2]; - if (percentage >= 0.5f) return colors[1]; - return colors[0]; - } - - private int percentageToTextColor(float percentage) - { - if (percentage >= 0.5f) return textColors[2]; - return textColors[1]; - } - - private void updateMaxMinLengths() - { - maxLength = 0; - minLength = Long.MAX_VALUE; - shouldShowLabels = true; - - for (Streak s : streaks) - { - maxLength = Math.max(maxLength, s.getLength()); - minLength = Math.min(minLength, s.getLength()); - - float lw1 = - paint.measureText(dateFormat.format(s.getStart().toJavaDate())); - float lw2 = - paint.measureText(dateFormat.format(s.getEnd().toJavaDate())); - maxLabelWidth = Math.max(maxLabelWidth, Math.max(lw1, lw2)); - } - - if (width - 2 * maxLabelWidth < width * 0.25f) - { - maxLabelWidth = 0; - shouldShowLabels = false; - } - } -} diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/StreakChart.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/StreakChart.kt new file mode 100644 index 000000000..098813921 --- /dev/null +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/StreakChart.kt @@ -0,0 +1,246 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits.activities.common.views + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.RectF +import android.util.AttributeSet +import android.view.View +import android.view.ViewGroup +import org.isoron.uhabits.R +import org.isoron.uhabits.core.models.Streak +import org.isoron.uhabits.core.models.Timestamp +import org.isoron.uhabits.core.utils.DateUtils.Companion.getToday +import org.isoron.uhabits.utils.InterfaceUtils.dpToPixels +import org.isoron.uhabits.utils.InterfaceUtils.getDimension +import org.isoron.uhabits.utils.StyledResources +import java.text.DateFormat +import java.util.LinkedList +import java.util.Random +import java.util.TimeZone + +class StreakChart : View { + private var paint: Paint? = null + private var minLength: Long = 0 + private var maxLength: Long = 0 + private lateinit var colors: IntArray + private lateinit var textColors: IntArray + private var rect: RectF? = null + private var baseSize = 0 + private var primaryColor = 0 + private var streaks: List? = null + private var isBackgroundTransparent = false + private var dateFormat: DateFormat? = null + private var internalWidth = 0 + private var em = 0f + private var maxLabelWidth = 0f + private var textMargin = 0f + private var shouldShowLabels = false + private val textColor = 0 + + constructor(context: Context?) : super(context) { + init() + } + + constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) { + init() + } + + /** + * Returns the maximum number of streaks this view is able to show, given + * its current size. + * + * @return max number of visible streaks + */ + val maxStreakCount: Int + get() = Math.floor((measuredHeight / baseSize).toDouble()).toInt() + + fun populateWithRandomData() { + var start: Timestamp = getToday() + val streaks: MutableList = LinkedList() + for (i in 0..9) { + val length = Random().nextInt(100) + val end = start.plus(length) + streaks.add(Streak(start, end)) + start = end.plus(1) + } + setStreaks(streaks) + } + + fun setColor(color: Int) { + primaryColor = color + postInvalidate() + } + + fun setIsBackgroundTransparent(isBackgroundTransparent: Boolean) { + this.isBackgroundTransparent = isBackgroundTransparent + initColors() + } + + fun setStreaks(streaks: List?) { + this.streaks = streaks + initColors() + updateMaxMinLengths() + requestLayout() + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + if (streaks!!.size == 0) return + rect!![0f, 0f, internalWidth.toFloat()] = baseSize.toFloat() + for (s in streaks!!) { + drawRow(canvas, s, rect) + rect!!.offset(0f, baseSize.toFloat()) + } + } + + override fun onMeasure(widthSpec: Int, heightSpec: Int) { + var widthSpec = widthSpec + var heightSpec = heightSpec + val params = layoutParams + if (params != null && params.height == ViewGroup.LayoutParams.WRAP_CONTENT) { + val width = MeasureSpec.getSize(widthSpec) + val height = streaks!!.size * baseSize + heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY) + widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY) + } + setMeasuredDimension(widthSpec, heightSpec) + } + + override fun onSizeChanged( + width: Int, + height: Int, + oldWidth: Int, + oldHeight: Int + ) { + this.internalWidth = width + val context = context + val minTextSize = getDimension(context, R.dimen.tinyTextSize) + val maxTextSize = getDimension(context, R.dimen.regularTextSize) + val textSize = baseSize * 0.5f + paint!!.textSize = Math.max(Math.min(textSize, maxTextSize), minTextSize) + em = paint!!.fontSpacing + textMargin = 0.5f * em + updateMaxMinLengths() + } + + private fun drawRow(canvas: Canvas, streak: Streak, rect: RectF?) { + if (maxLength == 0L) return + val percentage = streak.length.toFloat() / maxLength + var availableWidth = internalWidth - 2 * maxLabelWidth + if (shouldShowLabels) availableWidth -= 2 * textMargin + var barWidth = percentage * availableWidth + val minBarWidth = paint!!.measureText(java.lang.Long.toString(streak.length.toLong())) + em + barWidth = Math.max(barWidth, minBarWidth) + val gap = (internalWidth - barWidth) / 2 + val paddingTopBottom = baseSize * 0.05f + paint!!.color = percentageToColor(percentage) + val round = dpToPixels(context, 2f) + canvas.drawRoundRect( + rect!!.left + gap, + rect.top + paddingTopBottom, + rect.right - gap, + rect.bottom - paddingTopBottom, + round, + round, + paint!! + ) + val yOffset = rect.centerY() + 0.3f * em + paint!!.color = percentageToTextColor(percentage) + paint!!.textAlign = Paint.Align.CENTER + canvas.drawText( + java.lang.Long.toString(streak.length.toLong()), + rect.centerX(), + yOffset, + paint!! + ) + if (shouldShowLabels) { + val startLabel = dateFormat!!.format(streak.start.toJavaDate()) + val endLabel = dateFormat!!.format(streak.end.toJavaDate()) + paint!!.color = textColors[1] + paint!!.textAlign = Paint.Align.RIGHT + canvas.drawText(startLabel, gap - textMargin, yOffset, paint!!) + paint!!.textAlign = Paint.Align.LEFT + canvas.drawText(endLabel, internalWidth - gap + textMargin, yOffset, paint!!) + } + } + + private fun init() { + initPaints() + initColors() + streaks = emptyList() + val newDateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM) + if (!isInEditMode) newDateFormat.setTimeZone(TimeZone.getTimeZone("GMT")) + dateFormat = newDateFormat + rect = RectF() + baseSize = resources.getDimensionPixelSize(R.dimen.baseSize) + } + + private fun initColors() { + val red = Color.red(primaryColor) + val green = Color.green(primaryColor) + val blue = Color.blue(primaryColor) + val res = StyledResources(context) + colors = IntArray(4) + colors[3] = primaryColor + colors[2] = Color.argb(192, red, green, blue) + colors[1] = Color.argb(96, red, green, blue) + colors[0] = res.getColor(R.attr.lowContrastTextColor) + textColors = IntArray(3) + textColors[2] = res.getColor(R.attr.highContrastReverseTextColor) + textColors[1] = res.getColor(R.attr.mediumContrastTextColor) + textColors[0] = res.getColor(R.attr.lowContrastReverseTextColor) + } + + private fun initPaints() { + paint = Paint() + paint!!.textAlign = Paint.Align.CENTER + paint!!.isAntiAlias = true + } + + private fun percentageToColor(percentage: Float): Int { + if (percentage >= 1.0f) return colors[3] + if (percentage >= 0.8f) return colors[2] + return if (percentage >= 0.5f) colors[1] else colors[0] + } + + private fun percentageToTextColor(percentage: Float): Int { + return if (percentage >= 0.5f) textColors[2] else textColors[1] + } + + private fun updateMaxMinLengths() { + maxLength = 0 + minLength = Long.MAX_VALUE + shouldShowLabels = true + for (s in streaks!!) { + maxLength = Math.max(maxLength, s.length.toLong()) + minLength = Math.min(minLength, s.length.toLong()) + val lw1 = paint!!.measureText(dateFormat!!.format(s.start.toJavaDate())) + val lw2 = paint!!.measureText(dateFormat!!.format(s.end.toJavaDate())) + maxLabelWidth = Math.max(maxLabelWidth, Math.max(lw1, lw2)) + } + if (internalWidth - 2 * maxLabelWidth < internalWidth * 0.25f) { + maxLabelWidth = 0f + shouldShowLabels = false + } + } +} diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/TargetChart.java b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/TargetChart.java deleted file mode 100644 index b9c73bacd..000000000 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/TargetChart.java +++ /dev/null @@ -1,226 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.activities.common.views; - -import android.content.*; -import android.graphics.*; -import android.util.*; -import android.view.*; - -import org.isoron.uhabits.*; -import org.isoron.uhabits.activities.habits.list.views.*; -import org.isoron.uhabits.utils.*; - -import java.util.*; - -import static android.view.View.MeasureSpec.*; -import static org.isoron.uhabits.utils.InterfaceUtils.*; - -public class TargetChart extends View -{ - private Paint paint; - private int baseSize; - private int primaryColor; - private int mediumContrastTextColor; - private int highContrastReverseTextColor; - private int lowContrastTextColor; - private RectF rect = new RectF(); - private RectF barRect = new RectF(); - private List values = Collections.emptyList(); - private List labels = Collections.emptyList(); - private List targets = Collections.emptyList(); - private float maxLabelSize; - private float tinyTextSize; - - public TargetChart(Context context) - { - super(context); - init(); - } - - public TargetChart(Context context, AttributeSet attrs) - { - super(context, attrs); - init(); - } - - public void populateWithRandomData() - { - labels = new ArrayList<>(); - values = new ArrayList<>(); - targets = new ArrayList<>(); - for (int i = 0; i < 5; i++) { - double percentage = new Random().nextDouble(); - targets.add(new Random().nextDouble() * 1000.0); - values.add(targets.get(i) * percentage * 1.2); - labels.add(String.format(Locale.US, "Label %d", i + 1)); - } - } - - public void setColor(int color) - { - this.primaryColor = color; - postInvalidate(); - } - - @Override - protected void onDraw(Canvas canvas) - { - super.onDraw(canvas); - if (labels.size() == 0) return; - - maxLabelSize = 0; - for (String label : labels) { - paint.setTextSize(tinyTextSize); - float len = paint.measureText(label); - maxLabelSize = Math.max(maxLabelSize, len); - } - - float marginTop = (getHeight() - baseSize * labels.size()) / 2.0f; - rect.set(0, marginTop, getWidth(), marginTop + baseSize); - for (int i = 0; i < labels.size(); i++) { - drawRow(canvas, i, rect); - rect.offset(0, baseSize); - } - } - - @Override - protected void onMeasure(int widthSpec, int heightSpec) - { - baseSize = getResources().getDimensionPixelSize(R.dimen.baseSize); - - int width = getSize(widthSpec); - int height = labels.size() * baseSize; - - ViewGroup.LayoutParams params = getLayoutParams(); - if (params != null && params.height == ViewGroup.LayoutParams.MATCH_PARENT) { - height = getSize(heightSpec); - if (labels.size() > 0) baseSize = height / labels.size(); - } - - heightSpec = makeMeasureSpec(height, EXACTLY); - widthSpec = makeMeasureSpec(width, EXACTLY); - setMeasuredDimension(widthSpec, heightSpec); - } - - private void drawRow(Canvas canvas, int row, RectF rect) - { - float padding = dpToPixels(getContext(), 4); - float round = dpToPixels(getContext(), 2); - float stop = maxLabelSize + padding * 2; - - paint.setColor(mediumContrastTextColor); - - // Draw label - paint.setTextSize(tinyTextSize); - paint.setTextAlign(Paint.Align.RIGHT); - float yTextAdjust = (paint.descent() + paint.ascent()) / 2.0f; - canvas.drawText(labels.get(row), - rect.left + stop - padding, - rect.centerY() - yTextAdjust, - paint); - - // Draw background box - paint.setColor(lowContrastTextColor); - barRect.set(rect.left + stop + padding, - rect.top + baseSize * 0.05f, - rect.right - padding, - rect.bottom - baseSize * 0.05f); - canvas.drawRoundRect(barRect, round, round, paint); - - float percentage = (float) (values.get(row) / targets.get(row)); - percentage = Math.min(1.0f, percentage); - - // Draw completed box - float completedWidth = percentage * barRect.width(); - if (completedWidth > 0 && completedWidth < 2 * round) { - completedWidth = 2 * round; - } - float remainingWidth = barRect.width() - completedWidth; - - paint.setColor(primaryColor); - barRect.set(barRect.left, - barRect.top, - barRect.left + completedWidth, - barRect.bottom); - canvas.drawRoundRect(barRect, round, round, paint); - - // Draw values - paint.setColor(Color.WHITE); - paint.setTextSize(tinyTextSize); - paint.setTextAlign(Paint.Align.CENTER); - yTextAdjust = (paint.descent() + paint.ascent()) / 2.0f; - - double remaining = targets.get(row) - values.get(row); - String completedText = NumberButtonViewKt.toShortString(values.get(row)); - String remainingText = NumberButtonViewKt.toShortString(remaining); - - if (completedWidth > paint.measureText(completedText) + 2 * padding) { - paint.setColor(highContrastReverseTextColor); - canvas.drawText(completedText, - barRect.centerX(), - barRect.centerY() - yTextAdjust, - paint); - } - - if (remainingWidth > paint.measureText(remainingText) + 2 * padding) { - paint.setColor(mediumContrastTextColor); - barRect.set(rect.left + stop + padding + completedWidth, - barRect.top, - rect.right - padding, - barRect.bottom); - canvas.drawText(remainingText, - barRect.centerX(), - barRect.centerY() - yTextAdjust, - paint); - } - } - - private void init() - { - paint = new Paint(); - paint.setTextAlign(Paint.Align.CENTER); - paint.setAntiAlias(true); - - StyledResources res = new StyledResources(getContext()); - lowContrastTextColor = res.getColor(R.attr.lowContrastTextColor); - mediumContrastTextColor = res.getColor(R.attr.mediumContrastTextColor); - highContrastReverseTextColor = res.getColor(R.attr.highContrastReverseTextColor); - tinyTextSize = getDimension(getContext(), R.dimen.tinyTextSize); - } - - public void setValues(List values) - { - this.values = values; - requestLayout(); - } - - public void setLabels(List labels) - { - this.labels = labels; - requestLayout(); - } - - public void setTargets(List targets) - { - this.targets = targets; - requestLayout(); - } -} diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/TargetChart.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/TargetChart.kt new file mode 100644 index 000000000..296263f9e --- /dev/null +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/TargetChart.kt @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits.activities.common.views + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.RectF +import android.util.AttributeSet +import android.view.View +import android.view.ViewGroup +import org.isoron.uhabits.R +import org.isoron.uhabits.activities.habits.list.views.toShortString +import org.isoron.uhabits.utils.InterfaceUtils.dpToPixels +import org.isoron.uhabits.utils.InterfaceUtils.getDimension +import org.isoron.uhabits.utils.StyledResources + +class TargetChart : View { + private var paint: Paint? = null + private var baseSize = 0 + private var primaryColor = 0 + private var mediumContrastTextColor = 0 + private var highContrastReverseTextColor = 0 + private var lowContrastTextColor = 0 + private val rect = RectF() + private val barRect = RectF() + private var values = emptyList() + private var labels = emptyList() + private var targets = emptyList() + private var maxLabelSize = 0f + private var tinyTextSize = 0f + + constructor(context: Context?) : super(context) { + init() + } + + constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) { + init() + } + + fun setColor(color: Int) { + primaryColor = color + postInvalidate() + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + if (labels.size == 0) return + maxLabelSize = 0f + for (label in labels) { + paint!!.textSize = tinyTextSize + val len = paint!!.measureText(label) + maxLabelSize = Math.max(maxLabelSize, len) + } + val marginTop = (height - baseSize * labels.size) / 2.0f + rect[0f, marginTop, width.toFloat()] = marginTop + baseSize + for (i in labels.indices) { + drawRow(canvas, i, rect) + rect.offset(0f, baseSize.toFloat()) + } + } + + override fun onMeasure(widthSpec: Int, heightSpec: Int) { + var widthSpec = widthSpec + var heightSpec = heightSpec + baseSize = resources.getDimensionPixelSize(R.dimen.baseSize) + val width = MeasureSpec.getSize(widthSpec) + var height = labels.size * baseSize + val params = layoutParams + if (params != null && params.height == ViewGroup.LayoutParams.MATCH_PARENT) { + height = MeasureSpec.getSize(heightSpec) + if (labels.size > 0) baseSize = height / labels.size + } + heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY) + widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY) + setMeasuredDimension(widthSpec, heightSpec) + } + + private fun drawRow(canvas: Canvas, row: Int, rect: RectF) { + val padding = dpToPixels(context, 4f) + val round = dpToPixels(context, 2f) + val stop = maxLabelSize + padding * 2 + paint!!.color = mediumContrastTextColor + + // Draw label + paint!!.textSize = tinyTextSize + paint!!.textAlign = Paint.Align.RIGHT + var yTextAdjust = (paint!!.descent() + paint!!.ascent()) / 2.0f + canvas.drawText( + labels[row], + rect.left + stop - padding, + rect.centerY() - yTextAdjust, + paint!! + ) + + // Draw background box + paint!!.color = lowContrastTextColor + barRect[rect.left + stop + padding, rect.top + baseSize * 0.05f, rect.right - padding] = + rect.bottom - baseSize * 0.05f + canvas.drawRoundRect(barRect, round, round, paint!!) + var percentage = (values[row] / targets[row]).toFloat() + percentage = Math.min(1.0f, percentage) + + // Draw completed box + var completedWidth = percentage * barRect.width() + if (completedWidth > 0 && completedWidth < 2 * round) { + completedWidth = 2 * round + } + val remainingWidth = barRect.width() - completedWidth + paint!!.color = primaryColor + barRect[barRect.left, barRect.top, barRect.left + completedWidth] = barRect.bottom + canvas.drawRoundRect(barRect, round, round, paint!!) + + // Draw values + paint!!.color = Color.WHITE + paint!!.textSize = tinyTextSize + paint!!.textAlign = Paint.Align.CENTER + yTextAdjust = (paint!!.descent() + paint!!.ascent()) / 2.0f + val remaining = targets[row] - values[row] + val completedText = values[row].toShortString() + val remainingText = remaining.toShortString() + if (completedWidth > paint!!.measureText(completedText) + 2 * padding) { + paint!!.color = highContrastReverseTextColor + canvas.drawText( + completedText, + barRect.centerX(), + barRect.centerY() - yTextAdjust, + paint!! + ) + } + if (remainingWidth > paint!!.measureText(remainingText) + 2 * padding) { + paint!!.color = mediumContrastTextColor + barRect[rect.left + stop + padding + completedWidth, barRect.top, rect.right - padding] = + barRect.bottom + canvas.drawText( + remainingText, + barRect.centerX(), + barRect.centerY() - yTextAdjust, + paint!! + ) + } + } + + private fun init() { + paint = Paint() + paint!!.textAlign = Paint.Align.CENTER + paint!!.isAntiAlias = true + val res = StyledResources(context) + lowContrastTextColor = res.getColor(R.attr.lowContrastTextColor) + mediumContrastTextColor = res.getColor(R.attr.mediumContrastTextColor) + highContrastReverseTextColor = res.getColor(R.attr.highContrastReverseTextColor) + tinyTextSize = getDimension(context, R.dimen.tinyTextSize) + } + + fun setValues(values: List) { + this.values = values + requestLayout() + } + + fun setLabels(labels: List) { + this.labels = labels + requestLayout() + } + + fun setTargets(targets: List) { + this.targets = targets + requestLayout() + } +} diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListView.kt index 3d8ad81c5..56bc7bc9c 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListView.kt @@ -131,7 +131,7 @@ class HabitCardListView( super.onRestoreInstanceState(state) return } - dataOffset = state.bundle.getInt("dataOffset") + dataOffset = state.bundle!!.getInt("dataOffset") super.onRestoreInstanceState(state.superState) } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt index cc2f1c254..f54d15e04 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt @@ -90,10 +90,12 @@ class HabitCardView( } var score - get() = scoreRing.percentage.toDouble() + get() = scoreRing.getPercentage().toDouble() set(value) { - scoreRing.percentage = value.toFloat() - scoreRing.precision = 1.0f / 16 + // scoreRing.percentage = value.toFloat() + scoreRing.setPercentage(value.toFloat()) + // scoreRing.precision = 1.0f / 16 + scoreRing.setPrecision(1.0f / 16) } var unit @@ -225,7 +227,8 @@ class HabitCardView( setTextColor(c) } scoreRing.apply { - color = c + setColor(c) + // color = c } checkmarkPanel.apply { color = c diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/OverviewCardView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/OverviewCardView.kt index ddf657e27..1de75f34a 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/OverviewCardView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/OverviewCardView.kt @@ -49,8 +49,11 @@ class OverviewCardView(context: Context, attrs: AttributeSet) : LinearLayout(con binding.monthDiffLabel.text = formatPercentageDiff(state.scoreMonthDiff) binding.scoreLabel.setTextColor(androidColor) binding.scoreLabel.text = String.format("%.0f%%", state.scoreToday * 100) - binding.scoreRing.color = androidColor - binding.scoreRing.percentage = state.scoreToday + // binding.scoreRing.color = androidColor + binding.scoreRing.setColor(androidColor) + // binding.scoreRing.percentage = state.scoreToday + binding.scoreRing.setPercentage(state.scoreToday) + binding.title.setTextColor(androidColor) binding.totalCountLabel.setTextColor(androidColor) binding.totalCountLabel.text = state.totalCount.toString() diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/ScoreCardView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/ScoreCardView.kt index e5c740574..fb28bc055 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/ScoreCardView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/ScoreCardView.kt @@ -36,7 +36,7 @@ class ScoreCardView(context: Context, attrs: AttributeSet) : LinearLayout(contex val androidColor = state.color.toThemedAndroidColor(context) binding.title.setTextColor(androidColor) binding.spinner.setSelection(state.spinnerPosition) - binding.scoreView.setScores(state.scores) + binding.scoreView.setScores(state.scores.toMutableList()) binding.scoreView.reset() binding.scoreView.setBucketSize(state.bucketSize) binding.scoreView.setColor(androidColor) diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/ScoreWidget.kt b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/ScoreWidget.kt index 4beeed281..45d78e9f2 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/ScoreWidget.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/ScoreWidget.kt @@ -52,7 +52,7 @@ class ScoreWidget( setIsTransparencyEnabled(true) setBucketSize(viewModel.bucketSize) setColor(habit.color.toThemedAndroidColor(context)) - setScores(viewModel.scores) + setScores(viewModel.scores.toMutableList()) } } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetView.kt index e30dcc22a..effe7f398 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetView.kt @@ -83,8 +83,10 @@ class CheckmarkWidgetView : HabitWidgetView { setShadowAlpha(0x00) } } - ring.percentage = percentage - ring.color = fgColor + // ring.percentage = percentage + ring.setPercentage(percentage) + ring.setColor(fgColor) + // ring.color = fgColor ring.setBackgroundColor(bgColor) ring.setText(text) label.text = name