From e2c99d745e8ce6311d757b6e36346333637c82a9 Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Sat, 26 Mar 2016 08:27:23 -0400 Subject: [PATCH] Improve visualization of streaks Closes #20 --- .../uhabits/fragments/ShowHabitFragment.java | 1 + .../org/isoron/uhabits/models/StreakList.java | 38 +++- .../isoron/uhabits/views/HabitStreakView.java | 204 +++++++++--------- app/src/main/res/layout/show_habit.xml | 28 ++- app/src/main/res/values/dimens.xml | 2 +- app/src/main/res/values/strings.xml | 2 + app/src/main/res/values/styles.xml | 4 +- 7 files changed, 164 insertions(+), 115 deletions(-) diff --git a/app/src/main/java/org/isoron/uhabits/fragments/ShowHabitFragment.java b/app/src/main/java/org/isoron/uhabits/fragments/ShowHabitFragment.java index 61b99cca1..fa3afb932 100644 --- a/app/src/main/java/org/isoron/uhabits/fragments/ShowHabitFragment.java +++ b/app/src/main/java/org/isoron/uhabits/fragments/ShowHabitFragment.java @@ -106,6 +106,7 @@ public class ShowHabitFragment extends Fragment sStrengthInterval.setOnItemSelectedListener(this); dataViews.add((HabitStreakView) view.findViewById(R.id.streakView)); + dataViews.add((HabitStreakView) view.findViewById(R.id.smallStreakView)); dataViews.add((HabitScoreView) view.findViewById(R.id.scoreView)); dataViews.add((HabitHistoryView) view.findViewById(R.id.historyView)); dataViews.add((HabitFrequencyView) view.findViewById(R.id.punchcardView)); diff --git a/app/src/main/java/org/isoron/uhabits/models/StreakList.java b/app/src/main/java/org/isoron/uhabits/models/StreakList.java index 81c3a57ea..f8a296ae2 100644 --- a/app/src/main/java/org/isoron/uhabits/models/StreakList.java +++ b/app/src/main/java/org/isoron/uhabits/models/StreakList.java @@ -19,13 +19,18 @@ package org.isoron.uhabits.models; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; + import com.activeandroid.ActiveAndroid; +import com.activeandroid.Cache; import com.activeandroid.query.Delete; import com.activeandroid.query.Select; import org.isoron.uhabits.helpers.DateHelper; import java.util.ArrayList; +import java.util.LinkedList; import java.util.List; public class StreakList @@ -37,14 +42,37 @@ public class StreakList this.habit = habit; } - public List getAll() + public List getAll(int limit) { rebuild(); - return new Select().from(Streak.class) - .where("habit = ?", habit.getId()) - .orderBy("end asc") - .execute(); + String query = "select * from (select * from streak where habit=? " + + "order by end <> ?, length desc, end desc limit ?) order by end desc"; + + String params[] = {habit.getId().toString(), Long.toString(DateHelper.getStartOfToday()), + Integer.toString(limit)}; + + SQLiteDatabase db = Cache.openDatabase(); + Cursor cursor = db.rawQuery(query, params); + + if(!cursor.moveToFirst()) + { + cursor.close(); + return new LinkedList<>(); + } + + List streaks = new LinkedList<>(); + + do + { + Streak s = Streak.load(Streak.class, cursor.getInt(0)); + streaks.add(s); + } + while (cursor.moveToNext()); + + cursor.close(); + return streaks; + } public Streak getNewest() diff --git a/app/src/main/java/org/isoron/uhabits/views/HabitStreakView.java b/app/src/main/java/org/isoron/uhabits/views/HabitStreakView.java index 07a606017..36045454b 100644 --- a/app/src/main/java/org/isoron/uhabits/views/HabitStreakView.java +++ b/app/src/main/java/org/isoron/uhabits/views/HabitStreakView.java @@ -23,49 +23,50 @@ import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; -import android.graphics.Rect; +import android.graphics.RectF; import android.util.AttributeSet; +import android.view.View; +import org.isoron.uhabits.R; import org.isoron.uhabits.helpers.ColorHelper; -import org.isoron.uhabits.helpers.DateHelper; import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Streak; -import java.text.SimpleDateFormat; +import java.text.DateFormat; +import java.util.Collections; +import java.util.Date; import java.util.List; -import java.util.Locale; -import java.util.Random; +import java.util.TimeZone; -public class HabitStreakView extends ScrollableDataView implements HabitDataView +public class HabitStreakView extends View implements HabitDataView { private Habit habit; - private Paint pText, pBar; + private Paint paint; - private long[] startTimes; - private long[] endTimes; - private long[] lengths; + private long minLength; + private long maxLength; - private int columnWidth; - private int columnHeight; - private int headerHeight; - private int nColumns; - - private long maxStreakLength; private int[] colors; - private SimpleDateFormat dfMonth; - private Rect rect; + private RectF rect; private int baseSize; private int primaryColor; + private List streaks; private boolean isBackgroundTransparent; private int textColor; - private Paint pBarText; + private DateFormat dateFormat; + private int width; + private float em; + private float maxLabelWidth; + private float textMargin; + private boolean shouldShowLabels; + private int maxStreakCount; public HabitStreakView(Context context, AttributeSet attrs) { super(context, attrs); this.primaryColor = ColorHelper.palette[7]; - startTimes = endTimes = lengths = new long[0]; + streaks = Collections.emptyList(); init(); } @@ -74,18 +75,19 @@ public class HabitStreakView extends ScrollableDataView implements HabitDataView this.habit = habit; createColors(); - refreshData(); postInvalidate(); } private void init() { - refreshData(); createPaints(); createColors(); - dfMonth = new SimpleDateFormat("MMM", Locale.getDefault()); - rect = new Rect(); + dateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM); + dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); + rect = new RectF(); + + baseSize = getResources().getDimensionPixelSize(R.dimen.baseSize); } @Override @@ -99,16 +101,18 @@ public class HabitStreakView extends ScrollableDataView implements HabitDataView @Override protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { - baseSize = height / 10; - setScrollerBucketSize(baseSize); + maxStreakCount = height / baseSize; + this.width = width; - columnWidth = baseSize; - columnHeight = 8 * baseSize; - headerHeight = baseSize; - nColumns = width / baseSize - 1; + int maxTextSize = getResources().getDimensionPixelSize(R.dimen.regularTextSize); + float regularTextSize = Math.min(baseSize * 0.56f, maxTextSize); - pText.setTextSize(baseSize * 0.5f); - pBar.setTextSize(baseSize * 0.5f); + paint.setTextSize(regularTextSize); + em = paint.getFontSpacing(); + textMargin = 0.5f * em; + + refreshData(); + updateMaxMin(); } private void createColors() @@ -134,7 +138,6 @@ public class HabitStreakView extends ScrollableDataView implements HabitDataView colors[1] = Color.argb(170, red, green, blue); colors[0] = Color.argb(128, red, green, blue); textColor = Color.rgb(255, 255, 255); - pBarText = pText; } else { @@ -144,111 +147,106 @@ public class HabitStreakView extends ScrollableDataView implements HabitDataView colors[1] = Color.argb(96, red, green, blue); colors[0] = Color.argb(32, 0, 0, 0); textColor = Color.argb(64, 0, 0, 0); - pBarText = pBar; } } protected void createPaints() { - pText = new Paint(); - pText.setTextAlign(Paint.Align.CENTER); - pText.setAntiAlias(true); - - pBar = new Paint(); - pBar.setTextAlign(Paint.Align.CENTER); - pBar.setAntiAlias(true); + paint = new Paint(); + paint.setTextAlign(Paint.Align.CENTER); + paint.setAntiAlias(true); } public void refreshData() { - if(isInEditMode()) - generateRandomData(); - else - { - if(habit == null) return; - - List streaks = habit.streaks.getAll(); - int size = streaks.size(); + if(habit == null) return; + streaks = habit.streaks.getAll(maxStreakCount); + updateMaxMin(); + postInvalidate(); + } - startTimes = new long[size]; - endTimes = new long[size]; - lengths = new long[size]; + @Override + protected void onDraw(Canvas canvas) + { + super.onDraw(canvas); + if(streaks.size() == 0) return; - int k = 0; - for (Streak s : streaks) - { - startTimes[k] = s.start; - endTimes[k] = s.end; - lengths[k] = s.length; - k++; + rect.set(0, 0, width, baseSize); - maxStreakLength = Math.max(maxStreakLength, s.length); - } + for(Streak s : streaks) + { + drawRow(canvas, s, rect); + rect.offset(0, baseSize); } - - invalidate(); } - private void generateRandomData() + private void updateMaxMin() { - int size = 30; + maxLength = 0; + minLength = Long.MAX_VALUE; + shouldShowLabels = true; - startTimes = new long[size]; - endTimes = new long[size]; - lengths = new long[size]; - - Random random = new Random(); - Long date = DateHelper.getStartOfToday(); - - for(int i = 0; i < size; i++) + for (Streak s : streaks) { - int l = (int) Math.pow(2, random.nextFloat() * 5 + 1); + maxLength = Math.max(maxLength, s.length); + minLength = Math.min(minLength, s.length); - endTimes[i] = date; - date -= l * DateHelper.millisecondsInOneDay; - lengths[i] = (long) l; - startTimes[i] = date; + float lw1 = paint.measureText(dateFormat.format(new Date(s.start))); + float lw2 = paint.measureText(dateFormat.format(new Date(s.end))); + maxLabelWidth = Math.max(maxLabelWidth, Math.max(lw1, lw2)); + } - maxStreakLength = Math.max(maxStreakLength, l); + if(width - 2 * maxLabelWidth < width * 0.25f) + { + maxLabelWidth = 0; + shouldShowLabels = false; } } - @Override - protected void onDraw(Canvas canvas) + private void drawRow(Canvas canvas, Streak streak, RectF rect) { - super.onDraw(canvas); + if(maxLength == 0) return; - float lineHeight = pText.getFontSpacing(); - float barHeaderOffset = lineHeight * 0.4f; + float percentage = (float) streak.length / maxLength; + float availableWidth = width - 2 * maxLabelWidth; + if(shouldShowLabels) availableWidth -= 2 * textMargin; - int nStreaks = startTimes.length; - int start = nStreaks - nColumns - getDataOffset(); + float barWidth = percentage * availableWidth; + float minBarWidth = paint.measureText(streak.length.toString()); + barWidth = Math.max(barWidth, minBarWidth); - pText.setColor(textColor); + float gap = (width - barWidth) / 2; + float paddingTopBottom = baseSize * 0.05f; - String previousMonth = ""; + float croppedPercentage; + if (maxLength == minLength) + croppedPercentage = 1.0f; + else + croppedPercentage = (float) (streak.length - minLength) / (maxLength - minLength); - for (int offset = 0; offset < nColumns && start + offset < nStreaks; offset++) - { - if(start + offset < 0) continue; - String month = dfMonth.format(startTimes[start + offset]); + int c = (int) (croppedPercentage * 3); + paint.setColor(colors[(c)]); - long l = lengths[offset + start]; - double lRelative = ((double) l) / maxStreakLength; + canvas.drawRect(rect.left + gap, rect.top + paddingTopBottom, rect.right - gap, + rect.bottom - paddingTopBottom, paint); - pBar.setColor(colors[(int) Math.floor(lRelative * 3)]); + float yOffset = rect.centerY() + 0.3f * em; - int height = (int) (columnHeight * lRelative); - rect.set(0, 0, columnWidth - 2, height); - rect.offset(offset * columnWidth, headerHeight + columnHeight - height); + paint.setColor(Color.WHITE); + paint.setTextAlign(Paint.Align.CENTER); + canvas.drawText(streak.length.toString(), rect.centerX(), yOffset, paint); - canvas.drawRect(rect, pBar); - canvas.drawText(Long.toString(l), rect.centerX(), rect.top - barHeaderOffset, pBarText); + if(shouldShowLabels) + { + String startLabel = dateFormat.format(new Date(streak.start)); + String endLabel = dateFormat.format(new Date(streak.end)); - if (!month.equals(previousMonth)) - canvas.drawText(month, rect.centerX(), rect.bottom + lineHeight * 1.2f, pText); + paint.setColor(textColor); + paint.setTextAlign(Paint.Align.RIGHT); + canvas.drawText(startLabel, gap - textMargin, yOffset, paint); - previousMonth = month; + paint.setTextAlign(Paint.Align.LEFT); + canvas.drawText(endLabel, width - gap + textMargin, yOffset, paint); } } diff --git a/app/src/main/res/layout/show_habit.xml b/app/src/main/res/layout/show_habit.xml index 4833fa5fc..6505299c6 100644 --- a/app/src/main/res/layout/show_habit.xml +++ b/app/src/main/res/layout/show_habit.xml @@ -42,16 +42,36 @@ android:id="@+id/llOverview" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_gravity="center" + android:gravity="center" android:orientation="horizontal"> + + + + + + + + @@ -173,7 +193,7 @@ + android:text="@string/best_streaks"/> - 20dp + 20dp 42dp 450dp 8dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c6c66f843..6c2ffae2d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -151,4 +151,6 @@ Failed to generate bug report. Generate bug report Troubleshooting + Best streaks + Strength \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 61b0e8f2e..dcf0588eb 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -96,11 +96,11 @@