Convert views

pull/716/head
Quentin Hibon 5 years ago
parent 228be95f9c
commit 98f9693cff

@ -1,71 +0,0 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.common.views;
import android.os.*;
import androidx.customview.view.*;
public class BundleSavedState extends AbsSavedState
{
public static final Parcelable.Creator<BundleSavedState> CREATOR =
new ClassLoaderCreator<BundleSavedState>()
{
@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);
}
}

@ -0,0 +1,62 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.common.views
import android.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<BundleSavedState> =
object : ClassLoaderCreator<BundleSavedState> {
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<BundleSavedState?> {
return arrayOfNulls(size)
}
}
}
}

@ -1,347 +0,0 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.common.views;
import android.content.*;
import android.graphics.*;
import android.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<Timestamp, Integer[]> 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<Timestamp, Integer[]> frequency)
{
this.frequency = frequency;
maxFreq = getMaxFreq(frequency);
postInvalidate();
}
public void setFirstWeekday(int firstWeekday)
{
this.firstWeekday = firstWeekday;
postInvalidate();
}
private int getMaxFreq(HashMap<Timestamp, Integer[]> 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);
}
}

@ -0,0 +1,291 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.common.views
import android.content.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<Timestamp, Array<Int>>
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<Timestamp, Array<Int>>) {
this.frequency = frequency
maxFreq = getMaxFreq(frequency)
postInvalidate()
}
fun setFirstWeekday(firstWeekday: Int) {
this.firstWeekday = firstWeekday
postInvalidate()
}
private fun getMaxFreq(frequency: HashMap<Timestamp, Array<Int>>): 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<Int> = 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)
}
}

@ -16,14 +16,11 @@
* You should have received a copy of the GNU General Public License along * You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>. * with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
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; interface HabitChart {
fun setHabit(habit: Habit?)
public interface HabitChart fun refreshData()
{
void setHabit(Habit habit);
void refreshData();
} }

@ -1,272 +0,0 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.common.views;
import android.content.*;
import android.graphics.*;
import android.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;
}
}

@ -0,0 +1,211 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.common.views
import android.content.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)
}
}

@ -1,452 +0,0 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.common.views;
import android.content.*;
import android.graphics.*;
import android.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<Score> 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<Score> 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);
}
}

@ -0,0 +1,375 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.common.views
import android.content.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<Score>? = 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<Score>()
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<Score>) {
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)
}
}

@ -1,245 +0,0 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.common.views;
import android.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) {}
}
}

@ -0,0 +1,196 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.common.views
import android.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) {}
}
}

@ -1,321 +0,0 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.common.views;
import android.content.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<Streak> 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<Streak> 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<Streak> 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;
}
}
}

@ -0,0 +1,246 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.common.views
import android.content.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<Streak>? = 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<Streak> = 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<Streak>?) {
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
}
}
}

@ -1,226 +0,0 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.common.views;
import android.content.*;
import android.graphics.*;
import android.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<Double> values = Collections.emptyList();
private List<String> labels = Collections.emptyList();
private List<Double> 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<Double> values)
{
this.values = values;
requestLayout();
}
public void setLabels(List<String> labels)
{
this.labels = labels;
requestLayout();
}
public void setTargets(List<Double> targets)
{
this.targets = targets;
requestLayout();
}
}

@ -0,0 +1,186 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.common.views
import android.content.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<Double>()
private var labels = emptyList<String>()
private var targets = emptyList<Double>()
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<Double>) {
this.values = values
requestLayout()
}
fun setLabels(labels: List<String>) {
this.labels = labels
requestLayout()
}
fun setTargets(targets: List<Double>) {
this.targets = targets
requestLayout()
}
}

@ -131,7 +131,7 @@ class HabitCardListView(
super.onRestoreInstanceState(state) super.onRestoreInstanceState(state)
return return
} }
dataOffset = state.bundle.getInt("dataOffset") dataOffset = state.bundle!!.getInt("dataOffset")
super.onRestoreInstanceState(state.superState) super.onRestoreInstanceState(state.superState)
} }

@ -90,10 +90,12 @@ class HabitCardView(
} }
var score var score
get() = scoreRing.percentage.toDouble() get() = scoreRing.getPercentage().toDouble()
set(value) { set(value) {
scoreRing.percentage = value.toFloat() // scoreRing.percentage = value.toFloat()
scoreRing.precision = 1.0f / 16 scoreRing.setPercentage(value.toFloat())
// scoreRing.precision = 1.0f / 16
scoreRing.setPrecision(1.0f / 16)
} }
var unit var unit
@ -225,7 +227,8 @@ class HabitCardView(
setTextColor(c) setTextColor(c)
} }
scoreRing.apply { scoreRing.apply {
color = c setColor(c)
// color = c
} }
checkmarkPanel.apply { checkmarkPanel.apply {
color = c color = c

@ -49,8 +49,11 @@ class OverviewCardView(context: Context, attrs: AttributeSet) : LinearLayout(con
binding.monthDiffLabel.text = formatPercentageDiff(state.scoreMonthDiff) binding.monthDiffLabel.text = formatPercentageDiff(state.scoreMonthDiff)
binding.scoreLabel.setTextColor(androidColor) binding.scoreLabel.setTextColor(androidColor)
binding.scoreLabel.text = String.format("%.0f%%", state.scoreToday * 100) binding.scoreLabel.text = String.format("%.0f%%", state.scoreToday * 100)
binding.scoreRing.color = androidColor // binding.scoreRing.color = androidColor
binding.scoreRing.percentage = state.scoreToday binding.scoreRing.setColor(androidColor)
// binding.scoreRing.percentage = state.scoreToday
binding.scoreRing.setPercentage(state.scoreToday)
binding.title.setTextColor(androidColor) binding.title.setTextColor(androidColor)
binding.totalCountLabel.setTextColor(androidColor) binding.totalCountLabel.setTextColor(androidColor)
binding.totalCountLabel.text = state.totalCount.toString() binding.totalCountLabel.text = state.totalCount.toString()

@ -36,7 +36,7 @@ class ScoreCardView(context: Context, attrs: AttributeSet) : LinearLayout(contex
val androidColor = state.color.toThemedAndroidColor(context) val androidColor = state.color.toThemedAndroidColor(context)
binding.title.setTextColor(androidColor) binding.title.setTextColor(androidColor)
binding.spinner.setSelection(state.spinnerPosition) binding.spinner.setSelection(state.spinnerPosition)
binding.scoreView.setScores(state.scores) binding.scoreView.setScores(state.scores.toMutableList())
binding.scoreView.reset() binding.scoreView.reset()
binding.scoreView.setBucketSize(state.bucketSize) binding.scoreView.setBucketSize(state.bucketSize)
binding.scoreView.setColor(androidColor) binding.scoreView.setColor(androidColor)

@ -52,7 +52,7 @@ class ScoreWidget(
setIsTransparencyEnabled(true) setIsTransparencyEnabled(true)
setBucketSize(viewModel.bucketSize) setBucketSize(viewModel.bucketSize)
setColor(habit.color.toThemedAndroidColor(context)) setColor(habit.color.toThemedAndroidColor(context))
setScores(viewModel.scores) setScores(viewModel.scores.toMutableList())
} }
} }

@ -83,8 +83,10 @@ class CheckmarkWidgetView : HabitWidgetView {
setShadowAlpha(0x00) setShadowAlpha(0x00)
} }
} }
ring.percentage = percentage // ring.percentage = percentage
ring.color = fgColor ring.setPercentage(percentage)
ring.setColor(fgColor)
// ring.color = fgColor
ring.setBackgroundColor(bgColor) ring.setBackgroundColor(bgColor)
ring.setText(text) ring.setText(text)
label.text = name label.text = name

Loading…
Cancel
Save