From a6060f468df8ed3cb2fe7e64e4ca5153b7cd4fff Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Thu, 25 Jun 2020 06:12:26 -0500 Subject: [PATCH] Create new TargetCard and TargetChart --- .../activities/common/views/TargetChart.java | 217 ++++++++++++++++++ .../habits/show/ShowHabitRootView.java | 11 + .../habits/show/views/TargetCard.java | 159 +++++++++++++ .../main/res/layout/show_habit_history.xml | 2 +- .../src/main/res/layout/show_habit_inner.xml | 9 +- .../main/res/layout/show_habit_preview.xml | 7 +- .../src/main/res/layout/show_habit_target.xml | 31 +++ .../src/main/res/values/strings.xml | 1 + 8 files changed, 428 insertions(+), 9 deletions(-) create mode 100644 android/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/TargetChart.java create mode 100644 android/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/TargetCard.java create mode 100644 android/uhabits-android/src/main/res/layout/show_habit_target.xml diff --git a/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/TargetChart.java b/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/TargetChart.java new file mode 100644 index 000000000..cc1051f8d --- /dev/null +++ b/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/TargetChart.java @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.uhabits.activities.common.views; + +import android.content.*; +import android.graphics.*; +import android.util.*; +import android.view.*; + +import org.isoron.androidbase.utils.*; +import org.isoron.uhabits.*; +import org.isoron.uhabits.activities.habits.list.views.*; + +import java.util.*; + +import static android.view.View.MeasureSpec.*; +import static org.isoron.androidbase.utils.InterfaceUtils.*; + +public class TargetChart extends View +{ + private Paint paint; + private int baseSize; + private int primaryColor; + private int mediumContrastTextColor; + private int highContrastReverseTextColor; + private int lowContrastTextColor; + private RectF rect = new RectF(); + private RectF barRect = new RectF(); + private List values = Collections.emptyList(); + private List labels = Collections.emptyList(); + private List targets = Collections.emptyList(); + private float maxLabelSize; + private float tinyTextSize; + + public TargetChart(Context context) + { + super(context); + init(); + } + + public TargetChart(Context context, AttributeSet attrs) + { + super(context, attrs); + init(); + } + + public void populateWithRandomData() + { + labels = new ArrayList<>(); + values = new ArrayList<>(); + targets = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + double percentage = new Random().nextDouble(); + targets.add(new Random().nextDouble() * 1000.0); + values.add(targets.get(i) * percentage * 1.2); + labels.add(String.format(Locale.US, "Label %d", i + 1)); + } + } + + public void setColor(int color) + { + this.primaryColor = color; + postInvalidate(); + } + + @Override + protected void onDraw(Canvas canvas) + { + super.onDraw(canvas); + if (labels.size() == 0) return; + + maxLabelSize = 0; + for (String label : labels) { + paint.setTextSize(tinyTextSize); + float len = paint.measureText(label); + maxLabelSize = Math.max(maxLabelSize, len); + } + + rect.set(0, 0, getWidth(), 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) + { + int width = getSize(widthSpec); + int height = labels.size() * baseSize; + 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); + baseSize = getResources().getDimensionPixelSize(R.dimen.baseSize); + } + + public void setValues(List values) + { + this.values = values; + requestLayout(); + } + + public void setLabels(List labels) + { + this.labels = labels; + requestLayout(); + } + + public void setTargets(List targets) + { + this.targets = targets; + requestLayout(); + } +} diff --git a/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitRootView.java b/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitRootView.java index 9c2963867..e1a986294 100644 --- a/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitRootView.java +++ b/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitRootView.java @@ -21,6 +21,7 @@ package org.isoron.uhabits.activities.habits.show; import android.content.*; import android.os.*; +import android.view.*; import android.widget.TextView; import androidx.annotation.NonNull; @@ -74,6 +75,9 @@ public class ShowHabitRootView extends BaseRootView @BindView(R.id.toolbar) Toolbar toolbar; + @BindView(R.id.targetCard) + TargetCard targetCard; + @NonNull private Controller controller; @@ -150,6 +154,13 @@ public class ShowHabitRootView extends BaseRootView streakCard.setHabit(habit); frequencyCard.setHabit(habit); barCard.setHabit(habit); + targetCard.setHabit(habit); + + if(habit.isNumerical()) { + overviewCard.setVisibility(View.GONE); + } else { + targetCard.setVisibility(View.GONE); + } } public interface Controller extends HistoryCard.Controller diff --git a/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/TargetCard.java b/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/TargetCard.java new file mode 100644 index 000000000..aaa7a30d2 --- /dev/null +++ b/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/TargetCard.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.uhabits.activities.habits.show.views; + +import android.content.*; +import android.content.res.*; +import android.util.*; +import android.widget.*; + +import org.isoron.uhabits.R; +import org.isoron.uhabits.*; +import org.isoron.uhabits.activities.common.views.*; +import org.isoron.uhabits.core.models.*; +import org.isoron.uhabits.core.preferences.*; +import org.isoron.uhabits.core.tasks.*; +import org.isoron.uhabits.core.utils.*; +import org.isoron.uhabits.utils.*; + +import java.util.*; + +import butterknife.*; + +public class TargetCard extends HabitCard +{ + @BindView(R.id.title) + TextView title; + + @BindView(R.id.targetChart) + TargetChart targetChart; + + int firstWeekday = Calendar.SATURDAY; + + public TargetCard(Context context) + { + super(context); + init(); + } + + public TargetCard(Context context, AttributeSet attrs) + { + super(context, attrs); + init(); + } + + private void init() + { + inflate(getContext(), R.layout.show_habit_target, this); + ButterKnife.bind(this); + setOrientation(VERTICAL); + + Context app = getContext().getApplicationContext(); + if (app instanceof HabitsApplication) { + HabitsApplication habitsApp = (HabitsApplication) app; + Preferences prefs = habitsApp.getComponent().getPreferences(); + firstWeekday = prefs.getFirstWeekday(); + } + + if (isInEditMode()) initEditMode(); + } + + private void initEditMode() + { + int color = PaletteUtils.getAndroidTestColor(1); + title.setTextColor(color); + targetChart.setColor(color); + targetChart.populateWithRandomData(); + } + + @Override + protected Task createRefreshTask() + { + return new RefreshTask(); + } + + private class RefreshTask extends CancelableTask + { + double todayValue; + double thisWeekValue; + double thisMonthValue; + double thisQuarterValue; + double thisYearValue; + + @Override + public void doInBackground() + { + if (isCanceled()) return; + CheckmarkList checkmarks = getHabit().getCheckmarks(); + todayValue = checkmarks.getTodayValue() / 1e3; + thisWeekValue = checkmarks.getThisWeekValue(firstWeekday) / 1e3; + thisMonthValue = checkmarks.getThisMonthValue() / 1e3; + thisQuarterValue = checkmarks.getThisQuarterValue() / 1e3; + thisYearValue = checkmarks.getThisYearValue() / 1e3; + } + + @Override + public void onPostExecute() + { + if (isCanceled()) return; + + Calendar cal = DateUtils.getStartOfTodayCalendar(); + int daysInMonth = cal.getActualMaximum(Calendar.DAY_OF_MONTH); + int daysInQuarter = 91; + int daysInYear = cal.getActualMaximum(Calendar.DAY_OF_YEAR); + + int den = getHabit().getFrequency().getDenominator(); + double dailyTarget = getHabit().getTargetValue() / den; + Resources res = getResources(); + + ArrayList values = new ArrayList<>(); + if (den <= 1) values.add(todayValue); + if (den <= 7) values.add(thisWeekValue); + values.add(thisMonthValue); + values.add(thisQuarterValue); + values.add(thisYearValue); + targetChart.setValues(values); + + ArrayList targets = new ArrayList<>(); + if (den <= 1) targets.add(dailyTarget); + if (den <= 7) targets.add(dailyTarget * 7); + targets.add(dailyTarget * daysInMonth); + targets.add(dailyTarget * daysInQuarter); + targets.add(dailyTarget * daysInYear); + targetChart.setTargets(targets); + + ArrayList labels = new ArrayList<>(); + if (den <= 1) labels.add(res.getString(R.string.today)); + if (den <= 7) labels.add(res.getString(R.string.week)); + labels.add(res.getString(R.string.month)); + labels.add(res.getString(R.string.quarter)); + labels.add(res.getString(R.string.year)); + targetChart.setLabels(labels); + } + + @Override + public void onPreExecute() + { + int color = PaletteUtils.getColor(getContext(), getHabit().getColor()); + title.setTextColor(color); + targetChart.setColor(color); + } + } +} diff --git a/android/uhabits-android/src/main/res/layout/show_habit_history.xml b/android/uhabits-android/src/main/res/layout/show_habit_history.xml index 5763d2647..974ddb850 100644 --- a/android/uhabits-android/src/main/res/layout/show_habit_history.xml +++ b/android/uhabits-android/src/main/res/layout/show_habit_history.xml @@ -34,7 +34,7 @@ android:layout_width="match_parent" android:layout_height="160dp"/> -