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);
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));

@ -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<Streak> getAll()
public List<Streak> 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<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()

@ -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<Streak> 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<Streak> 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);
}
}

@ -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">
<org.isoron.uhabits.views.RingView
android:id="@+id/scoreRing"
style="@style/smallDataViewStyle"
app:label="@string/habit_strength"
app:maxDiameter="70"
android:layout_width="100dp"
app:label="@string/strength"
app:maxDiameter="80"
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>
@ -173,7 +193,7 @@
<TextView
android:id="@+id/tvStreaks"
style="@style/cardHeaderStyle"
android:text="@string/streaks"/>
android:text="@string/best_streaks"/>
<org.isoron.uhabits.views.HabitStreakView
android:id="@+id/streakView"

@ -18,7 +18,7 @@
-->
<resources>
<dimen name="small_square_size">20dp</dimen>
<dimen name="baseSize">20dp</dimen>
<dimen name="check_square_size">42dp</dimen>
<dimen name="history_editor_max_height">450dp</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="generate_bug_report">Generate bug report</string>
<string name="troubleshooting">Troubleshooting</string>
<string name="best_streaks">Best streaks</string>
<string name="strength">Strength</string>
</resources>

@ -96,11 +96,11 @@
</style>
<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_weight">1</item>
<item name="android:layout_marginLeft">8dp</item>
<item name="android:layout_marginRight">8dp</item>
<item name="android:textColor">@color/fadedTextColor</item>
</style>
</resources>

Loading…
Cancel
Save