Improve visualization of streaks

Closes #20
pull/77/merge
Alinson S. Xavier 10 years ago
parent dfe41176bc
commit e2c99d745e

@ -106,6 +106,7 @@ public class ShowHabitFragment extends Fragment
sStrengthInterval.setOnItemSelectedListener(this); sStrengthInterval.setOnItemSelectedListener(this);
dataViews.add((HabitStreakView) view.findViewById(R.id.streakView)); 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((HabitScoreView) view.findViewById(R.id.scoreView));
dataViews.add((HabitHistoryView) view.findViewById(R.id.historyView)); dataViews.add((HabitHistoryView) view.findViewById(R.id.historyView));
dataViews.add((HabitFrequencyView) view.findViewById(R.id.punchcardView)); dataViews.add((HabitFrequencyView) view.findViewById(R.id.punchcardView));

@ -19,13 +19,18 @@
package org.isoron.uhabits.models; package org.isoron.uhabits.models;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import com.activeandroid.ActiveAndroid; import com.activeandroid.ActiveAndroid;
import com.activeandroid.Cache;
import com.activeandroid.query.Delete; import com.activeandroid.query.Delete;
import com.activeandroid.query.Select; import com.activeandroid.query.Select;
import org.isoron.uhabits.helpers.DateHelper; import org.isoron.uhabits.helpers.DateHelper;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List; import java.util.List;
public class StreakList public class StreakList
@ -37,14 +42,37 @@ public class StreakList
this.habit = habit; this.habit = habit;
} }
public List<Streak> getAll() public List<Streak> getAll(int limit)
{ {
rebuild(); rebuild();
return new Select().from(Streak.class) String query = "select * from (select * from streak where habit=? " +
.where("habit = ?", habit.getId()) "order by end <> ?, length desc, end desc limit ?) order by end desc";
.orderBy("end asc")
.execute(); 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<Streak> 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() public Streak getNewest()

@ -23,49 +23,50 @@ import android.content.Context;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.Paint; import android.graphics.Paint;
import android.graphics.Rect; import android.graphics.RectF;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.View;
import org.isoron.uhabits.R;
import org.isoron.uhabits.helpers.ColorHelper; import org.isoron.uhabits.helpers.ColorHelper;
import org.isoron.uhabits.helpers.DateHelper;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.Streak; 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.List;
import java.util.Locale; import java.util.TimeZone;
import java.util.Random;
public class HabitStreakView extends ScrollableDataView implements HabitDataView public class HabitStreakView extends View implements HabitDataView
{ {
private Habit habit; private Habit habit;
private Paint pText, pBar; private Paint paint;
private long[] startTimes; private long minLength;
private long[] endTimes; private long maxLength;
private long[] lengths;
private int columnWidth;
private int columnHeight;
private int headerHeight;
private int nColumns;
private long maxStreakLength;
private int[] colors; private int[] colors;
private SimpleDateFormat dfMonth; private RectF rect;
private Rect rect;
private int baseSize; private int baseSize;
private int primaryColor; private int primaryColor;
private List<Streak> streaks;
private boolean isBackgroundTransparent; private boolean isBackgroundTransparent;
private int textColor; 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) public HabitStreakView(Context context, AttributeSet attrs)
{ {
super(context, attrs); super(context, attrs);
this.primaryColor = ColorHelper.palette[7]; this.primaryColor = ColorHelper.palette[7];
startTimes = endTimes = lengths = new long[0]; streaks = Collections.emptyList();
init(); init();
} }
@ -74,18 +75,19 @@ public class HabitStreakView extends ScrollableDataView implements HabitDataView
this.habit = habit; this.habit = habit;
createColors(); createColors();
refreshData();
postInvalidate(); postInvalidate();
} }
private void init() private void init()
{ {
refreshData();
createPaints(); createPaints();
createColors(); createColors();
dfMonth = new SimpleDateFormat("MMM", Locale.getDefault()); dateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM);
rect = new Rect(); dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
rect = new RectF();
baseSize = getResources().getDimensionPixelSize(R.dimen.baseSize);
} }
@Override @Override
@ -99,16 +101,18 @@ public class HabitStreakView extends ScrollableDataView implements HabitDataView
@Override @Override
protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight)
{ {
baseSize = height / 10; maxStreakCount = height / baseSize;
setScrollerBucketSize(baseSize); this.width = width;
columnWidth = baseSize; int maxTextSize = getResources().getDimensionPixelSize(R.dimen.regularTextSize);
columnHeight = 8 * baseSize; float regularTextSize = Math.min(baseSize * 0.56f, maxTextSize);
headerHeight = baseSize;
nColumns = width / baseSize - 1;
pText.setTextSize(baseSize * 0.5f); paint.setTextSize(regularTextSize);
pBar.setTextSize(baseSize * 0.5f); em = paint.getFontSpacing();
textMargin = 0.5f * em;
refreshData();
updateMaxMin();
} }
private void createColors() private void createColors()
@ -134,7 +138,6 @@ public class HabitStreakView extends ScrollableDataView implements HabitDataView
colors[1] = Color.argb(170, red, green, blue); colors[1] = Color.argb(170, red, green, blue);
colors[0] = Color.argb(128, red, green, blue); colors[0] = Color.argb(128, red, green, blue);
textColor = Color.rgb(255, 255, 255); textColor = Color.rgb(255, 255, 255);
pBarText = pText;
} }
else else
{ {
@ -144,111 +147,106 @@ public class HabitStreakView extends ScrollableDataView implements HabitDataView
colors[1] = Color.argb(96, red, green, blue); colors[1] = Color.argb(96, red, green, blue);
colors[0] = Color.argb(32, 0, 0, 0); colors[0] = Color.argb(32, 0, 0, 0);
textColor = Color.argb(64, 0, 0, 0); textColor = Color.argb(64, 0, 0, 0);
pBarText = pBar;
} }
} }
protected void createPaints() protected void createPaints()
{ {
pText = new Paint(); paint = new Paint();
pText.setTextAlign(Paint.Align.CENTER); paint.setTextAlign(Paint.Align.CENTER);
pText.setAntiAlias(true); paint.setAntiAlias(true);
pBar = new Paint();
pBar.setTextAlign(Paint.Align.CENTER);
pBar.setAntiAlias(true);
} }
public void refreshData() public void refreshData()
{
if(isInEditMode())
generateRandomData();
else
{ {
if(habit == null) return; if(habit == null) return;
streaks = habit.streaks.getAll(maxStreakCount);
updateMaxMin();
postInvalidate();
}
List<Streak> streaks = habit.streaks.getAll(); @Override
int size = streaks.size(); protected void onDraw(Canvas canvas)
{
super.onDraw(canvas);
if(streaks.size() == 0) return;
startTimes = new long[size]; rect.set(0, 0, width, baseSize);
endTimes = new long[size];
lengths = new long[size];
int k = 0;
for(Streak s : streaks) for(Streak s : streaks)
{ {
startTimes[k] = s.start; drawRow(canvas, s, rect);
endTimes[k] = s.end; rect.offset(0, baseSize);
lengths[k] = s.length;
k++;
maxStreakLength = Math.max(maxStreakLength, s.length);
}
} }
invalidate();
} }
private void generateRandomData() private void updateMaxMin()
{ {
int size = 30; maxLength = 0;
minLength = Long.MAX_VALUE;
startTimes = new long[size]; shouldShowLabels = true;
endTimes = new long[size];
lengths = new long[size];
Random random = new Random(); for (Streak s : streaks)
Long date = DateHelper.getStartOfToday();
for(int i = 0; i < size; i++)
{ {
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; float lw1 = paint.measureText(dateFormat.format(new Date(s.start)));
date -= l * DateHelper.millisecondsInOneDay; float lw2 = paint.measureText(dateFormat.format(new Date(s.end)));
lengths[i] = (long) l; maxLabelWidth = Math.max(maxLabelWidth, Math.max(lw1, lw2));
startTimes[i] = date; }
maxStreakLength = Math.max(maxStreakLength, l); if(width - 2 * maxLabelWidth < width * 0.25f)
{
maxLabelWidth = 0;
shouldShowLabels = false;
} }
} }
@Override private void drawRow(Canvas canvas, Streak streak, RectF rect)
protected void onDraw(Canvas canvas)
{ {
super.onDraw(canvas); if(maxLength == 0) return;
float lineHeight = pText.getFontSpacing(); float percentage = (float) streak.length / maxLength;
float barHeaderOffset = lineHeight * 0.4f; float availableWidth = width - 2 * maxLabelWidth;
if(shouldShowLabels) availableWidth -= 2 * textMargin;
int nStreaks = startTimes.length; float barWidth = percentage * availableWidth;
int start = nStreaks - nColumns - getDataOffset(); 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++) int c = (int) (croppedPercentage * 3);
{ paint.setColor(colors[(c)]);
if(start + offset < 0) continue;
String month = dfMonth.format(startTimes[start + offset]);
long l = lengths[offset + start]; canvas.drawRect(rect.left + gap, rect.top + paddingTopBottom, rect.right - gap,
double lRelative = ((double) l) / maxStreakLength; rect.bottom - paddingTopBottom, paint);
pBar.setColor(colors[(int) Math.floor(lRelative * 3)]); float yOffset = rect.centerY() + 0.3f * em;
int height = (int) (columnHeight * lRelative); paint.setColor(Color.WHITE);
rect.set(0, 0, columnWidth - 2, height); paint.setTextAlign(Paint.Align.CENTER);
rect.offset(offset * columnWidth, headerHeight + columnHeight - height); canvas.drawText(streak.length.toString(), rect.centerX(), yOffset, paint);
canvas.drawRect(rect, pBar); if(shouldShowLabels)
canvas.drawText(Long.toString(l), rect.centerX(), rect.top - barHeaderOffset, pBarText); {
String startLabel = dateFormat.format(new Date(streak.start));
String endLabel = dateFormat.format(new Date(streak.end));
if (!month.equals(previousMonth)) paint.setColor(textColor);
canvas.drawText(month, rect.centerX(), rect.bottom + lineHeight * 1.2f, pText); 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);
} }
} }

@ -42,16 +42,36 @@
android:id="@+id/llOverview" android:id="@+id/llOverview"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:gravity="center"
android:orientation="horizontal"> android:orientation="horizontal">
<org.isoron.uhabits.views.RingView <org.isoron.uhabits.views.RingView
android:id="@+id/scoreRing" android:id="@+id/scoreRing"
style="@style/smallDataViewStyle" style="@style/smallDataViewStyle"
app:label="@string/habit_strength" android:layout_width="100dp"
app:maxDiameter="70" app:label="@string/strength"
app:maxDiameter="80"
app:textSize="@dimen/smallTextSize"/> app:textSize="@dimen/smallTextSize"/>
<LinearLayout
style="@style/smallDataViewStyle"
android:orientation="vertical">
<org.isoron.uhabits.views.HabitStreakView
android:id="@+id/smallStreakView"
android:layout_width="match_parent"
android:layout_height="80dp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/best_streaks"
android:layout_marginTop="9dp"
android:textColor="@color/fadedTextColor"
android:gravity="center"/>
</LinearLayout>
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
@ -173,7 +193,7 @@
<TextView <TextView
android:id="@+id/tvStreaks" android:id="@+id/tvStreaks"
style="@style/cardHeaderStyle" style="@style/cardHeaderStyle"
android:text="@string/streaks"/> android:text="@string/best_streaks"/>
<org.isoron.uhabits.views.HabitStreakView <org.isoron.uhabits.views.HabitStreakView
android:id="@+id/streakView" android:id="@+id/streakView"

@ -18,7 +18,7 @@
--> -->
<resources> <resources>
<dimen name="small_square_size">20dp</dimen> <dimen name="baseSize">20dp</dimen>
<dimen name="check_square_size">42dp</dimen> <dimen name="check_square_size">42dp</dimen>
<dimen name="history_editor_max_height">450dp</dimen> <dimen name="history_editor_max_height">450dp</dimen>
<dimen name="history_editor_padding">8dp</dimen> <dimen name="history_editor_padding">8dp</dimen>

@ -151,4 +151,6 @@
<string name="bug_report_failed">Failed to generate bug report.</string> <string name="bug_report_failed">Failed to generate bug report.</string>
<string name="generate_bug_report">Generate bug report</string> <string name="generate_bug_report">Generate bug report</string>
<string name="troubleshooting">Troubleshooting</string> <string name="troubleshooting">Troubleshooting</string>
<string name="best_streaks">Best streaks</string>
<string name="strength">Strength</string>
</resources> </resources>

@ -96,11 +96,11 @@
</style> </style>
<style name="smallDataViewStyle"> <style name="smallDataViewStyle">
<item name="android:layout_width">0dp</item> <item name="android:layout_width">100dp</item>
<item name="android:layout_height">wrap_content</item> <item name="android:layout_height">wrap_content</item>
<item name="android:layout_weight">1</item>
<item name="android:layout_marginLeft">8dp</item> <item name="android:layout_marginLeft">8dp</item>
<item name="android:layout_marginRight">8dp</item> <item name="android:layout_marginRight">8dp</item>
<item name="android:textColor">@color/fadedTextColor</item>
</style> </style>
</resources> </resources>

Loading…
Cancel
Save