diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 8d7e8eff2..75e3ed840 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -18,15 +18,18 @@
~ with this program. If not, see .
-->
-
+
+
+
@@ -38,6 +41,7 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/main_activity_title"
android:theme="@style/AppBaseTheme">
+
@@ -58,8 +62,6 @@
-
-
+
+
+
+
+
+
+
+
+
+
+ android:label="@string/checkmark">
@@ -93,9 +110,10 @@
android:name="android.appwidget.provider"
android:resource="@xml/widget_checkmark_info"/>
+
+ android:label="@string/history">
@@ -104,9 +122,10 @@
android:name="android.appwidget.provider"
android:resource="@xml/widget_history_info"/>
+
+ android:label="@string/habit_strength">
@@ -115,9 +134,10 @@
android:name="android.appwidget.provider"
android:resource="@xml/widget_score_info"/>
+
+ android:label="@string/streaks">
@@ -127,18 +147,20 @@
android:resource="@xml/widget_streak_info"/>
-
+
-
+
-
-
-
+
+
+
+
+
+
diff --git a/app/src/main/java/org/isoron/uhabits/MainActivity.java b/app/src/main/java/org/isoron/uhabits/MainActivity.java
index 769e58a8f..697b18d18 100644
--- a/app/src/main/java/org/isoron/uhabits/MainActivity.java
+++ b/app/src/main/java/org/isoron/uhabits/MainActivity.java
@@ -41,6 +41,7 @@ import org.isoron.uhabits.fragments.ListHabitsFragment;
import org.isoron.uhabits.helpers.ReminderHelper;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.widgets.CheckmarkWidgetProvider;
+import org.isoron.uhabits.widgets.FrequencyWidgetProvider;
import org.isoron.uhabits.widgets.HistoryWidgetProvider;
import org.isoron.uhabits.widgets.ScoreWidgetProvider;
import org.isoron.uhabits.widgets.StreakWidgetProvider;
@@ -159,6 +160,7 @@ public class MainActivity extends ReplayableActivity
updateWidgets(context, HistoryWidgetProvider.class);
updateWidgets(context, ScoreWidgetProvider.class);
updateWidgets(context, StreakWidgetProvider.class);
+ updateWidgets(context, FrequencyWidgetProvider.class);
}
private static void updateWidgets(Context context, Class providerClass)
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 aa5c917d7..e1dde9c66 100644
--- a/app/src/main/java/org/isoron/uhabits/fragments/ShowHabitFragment.java
+++ b/app/src/main/java/org/isoron/uhabits/fragments/ShowHabitFragment.java
@@ -42,6 +42,7 @@ import org.isoron.uhabits.helpers.ReminderHelper;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.Score;
import org.isoron.uhabits.views.HabitHistoryView;
+import org.isoron.uhabits.views.HabitFrequencyView;
import org.isoron.uhabits.views.HabitScoreView;
import org.isoron.uhabits.views.HabitStreakView;
import org.isoron.uhabits.views.RingView;
@@ -54,6 +55,7 @@ public class ShowHabitFragment extends Fragment
private HabitStreakView streakView;
private HabitScoreView scoreView;
private HabitHistoryView historyView;
+ private HabitFrequencyView punchcardView;
@Override
public void onStart()
@@ -75,6 +77,7 @@ public class ShowHabitFragment extends Fragment
streakView = (HabitStreakView) view.findViewById(R.id.streakView);
scoreView = (HabitScoreView) view.findViewById(R.id.scoreView);
historyView = (HabitHistoryView) view.findViewById(R.id.historyView);
+ punchcardView = (HabitFrequencyView) view.findViewById(R.id.punchcardView);
updateHeaders(view);
updateScoreRing(view);
@@ -82,6 +85,7 @@ public class ShowHabitFragment extends Fragment
streakView.setHabit(habit);
scoreView.setHabit(habit);
historyView.setHabit(habit);
+ punchcardView.setHabit(habit);
btEditHistory.setOnClickListener(new View.OnClickListener()
{
@@ -125,14 +129,17 @@ public class ShowHabitFragment extends Fragment
activity.getWindow().setStatusBarColor(darkerHabitColor);
}
- TextView tvHistory = (TextView) view.findViewById(R.id.tvHistory);
- TextView tvOverview = (TextView) view.findViewById(R.id.tvOverview);
- TextView tvStrength = (TextView) view.findViewById(R.id.tvStrength);
- TextView tvStreaks = (TextView) view.findViewById(R.id.tvStreaks);
- tvHistory.setTextColor(habit.color);
- tvOverview.setTextColor(habit.color);
- tvStrength.setTextColor(habit.color);
- tvStreaks.setTextColor(habit.color);
+ updateColor(view, R.id.tvHistory);
+ updateColor(view, R.id.tvOverview);
+ updateColor(view, R.id.tvStrength);
+ updateColor(view, R.id.tvStreaks);
+ updateColor(view, R.id.tvWeekdayFreq);
+ }
+
+ private void updateColor(View view, int viewId)
+ {
+ TextView textView = (TextView) view.findViewById(viewId);
+ textView.setTextColor(habit.color);
}
@Override
@@ -182,6 +189,7 @@ public class ShowHabitFragment extends Fragment
streakView.refreshData();
historyView.refreshData();
scoreView.refreshData();
+ punchcardView.refreshData();
updateScoreRing(getView());
}
}
diff --git a/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java b/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java
index 3d9986dec..f21038748 100644
--- a/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java
+++ b/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java
@@ -19,12 +19,21 @@
package org.isoron.uhabits.models;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+
+import com.activeandroid.Cache;
import com.activeandroid.query.Delete;
import com.activeandroid.query.From;
import com.activeandroid.query.Select;
import org.isoron.helpers.DateHelper;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.HashMap;
+
public class RepetitionList
{
@@ -98,4 +107,53 @@ public class RepetitionList
int reps[] = habit.checkmarks.getValues(today - DateHelper.millisecondsInOneDay, today);
return (reps[0] > 0);
}
+
+ public HashMap getWeekdayFrequency()
+ {
+ Repetition oldestRep = getOldest();
+ if(oldestRep == null) return new HashMap<>();
+
+ String query = "select strftime('%Y', timestamp / 1000, 'unixepoch') as year," +
+ "strftime('%m', timestamp / 1000, 'unixepoch') as month," +
+ "strftime('%w', timestamp / 1000, 'unixepoch') as weekday, " +
+ "count(*) from repetitions " +
+ "where habit = ? " +
+ "group by year, month, weekday";
+
+ String[] params = { habit.getId().toString() };
+
+ SQLiteDatabase db = Cache.openDatabase();
+ Cursor cursor = db.rawQuery(query, params);
+
+ if(!cursor.moveToFirst()) return new HashMap<>();
+
+ HashMap map = new HashMap<>();
+ GregorianCalendar date = DateHelper.getStartOfTodayCalendar();
+
+ do
+ {
+ int year = Integer.parseInt(cursor.getString(0));
+ int month = Integer.parseInt(cursor.getString(1));
+ int weekday = (Integer.parseInt(cursor.getString(2)) + 1) % 7;
+ int count = cursor.getInt(3);
+
+ date.set(year, month - 1, 1);
+ long timestamp = date.getTimeInMillis();
+
+ Integer[] list = map.get(timestamp);
+
+ if(list == null)
+ {
+ list = new Integer[7];
+ Arrays.fill(list, 0);
+ map.put(timestamp, list);
+ }
+
+ list[weekday] = count;
+ }
+ while (cursor.moveToNext());
+ cursor.close();
+
+ return map;
+ }
}
diff --git a/app/src/main/java/org/isoron/uhabits/views/HabitFrequencyView.java b/app/src/main/java/org/isoron/uhabits/views/HabitFrequencyView.java
new file mode 100644
index 000000000..42c41b64b
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/views/HabitFrequencyView.java
@@ -0,0 +1,295 @@
+/*
+ * 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.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 org.isoron.helpers.ColorHelper;
+import org.isoron.helpers.DateHelper;
+import org.isoron.uhabits.models.Habit;
+
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Random;
+import java.util.TimeZone;
+
+public class HabitFrequencyView extends ScrollableDataView
+{
+
+ private Paint pGrid;
+ private float em;
+ private Habit habit;
+ private SimpleDateFormat dfMonth;
+ private SimpleDateFormat dfYear;
+
+ private Paint pText, pGraph;
+ private RectF rect, prevRect;
+ private int baseSize;
+ private int paddingTop;
+
+ private int columnWidth;
+ private int columnHeight;
+ private int nColumns;
+
+ private int textColor;
+ private int dimmedTextColor;
+ private int[] colors;
+ private int primaryColor;
+ private boolean isBackgroundTransparent;
+
+ private HashMap frequency;
+ private String wdays[];
+
+ public HabitFrequencyView(Context context, AttributeSet attrs)
+ {
+ super(context, attrs);
+ this.primaryColor = ColorHelper.palette[7];
+ this.frequency = new HashMap<>();
+ wdays = DateHelper.getShortDayNames();
+ init();
+ }
+
+ public void setHabit(Habit habit)
+ {
+ this.habit = habit;
+ createColors();
+ refreshData();
+ postInvalidate();
+ }
+
+ private void init()
+ {
+ refreshData();
+ createPaints();
+ createColors();
+
+ dfMonth = new SimpleDateFormat("MMM", Locale.getDefault());
+ dfYear = new SimpleDateFormat("yyyy", Locale.getDefault());
+
+ dfMonth.setTimeZone(TimeZone.getTimeZone("GMT"));
+ dfYear.setTimeZone(TimeZone.getTimeZone("GMT"));
+
+ rect = new RectF();
+ prevRect = new RectF();
+ }
+
+ private void createColors()
+ {
+ if(habit != null)
+ this.primaryColor = habit.color;
+
+ if (isBackgroundTransparent)
+ {
+ primaryColor = ColorHelper.setSaturation(primaryColor, 0.75f);
+ primaryColor = ColorHelper.setValue(primaryColor, 1.0f);
+
+ textColor = Color.argb(192, 255, 255, 255);
+ dimmedTextColor = Color.argb(128, 255, 255, 255);
+ }
+ else
+ {
+ textColor = Color.argb(64, 0, 0, 0);
+ dimmedTextColor = Color.argb(16, 0, 0, 0);
+ }
+
+ colors = new int[4];
+
+ colors[0] = Color.rgb(230, 230, 230);
+ colors[3] = primaryColor;
+ colors[1] = ColorHelper.mixColors(colors[0], colors[3], 0.66f);
+ colors[2] = ColorHelper.mixColors(colors[0], colors[3], 0.33f);
+ }
+
+ protected void createPaints()
+ {
+ 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 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);
+
+ columnWidth = baseSize;
+ columnHeight = 8 * baseSize;
+ nColumns = width / baseSize;
+ paddingTop = 0;
+
+ pText.setTextSize(baseSize * 0.4f);
+ pGraph.setTextSize(baseSize * 0.4f);
+ pGraph.setStrokeWidth(baseSize * 0.1f);
+ pGrid.setStrokeWidth(baseSize * 0.05f);
+ em = pText.getFontSpacing();
+ }
+
+ public void refreshData()
+ {
+ if(isInEditMode())
+ generateRandomData();
+ else if(habit != null)
+ frequency = habit.repetitions.getWeekdayFrequency();
+
+ invalidate();
+ }
+
+ private void generateRandomData()
+ {
+ GregorianCalendar date = DateHelper.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(date.getTimeInMillis(), values);
+ date.add(Calendar.MONTH, -1);
+ }
+ }
+
+ @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 = DateHelper.getStartOfTodayCalendar();
+
+ 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);
+ }
+ }
+
+ private void drawColumn(Canvas canvas, RectF rect, GregorianCalendar date)
+ {
+ Integer values[] = frequency.get(date.getTimeInMillis());
+ float rowHeight = rect.height() / 8.0f;
+ prevRect.set(rect);
+
+ for (int i = 0; i < 7; i++)
+ {
+ rect.set(0, 0, baseSize, baseSize);
+ rect.offset(prevRect.left, prevRect.top + columnWidth * i);
+
+ 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 drawMarker(Canvas canvas, RectF rect, Integer value)
+ {
+ float padding = rect.height() * 0.2f;
+ float radius = (rect.height() - 2 * padding) / 2.0f / 4.0f * Math.min(value, 4);
+
+ pGraph.setColor(colors[Math.min(3, Math.max(0, value - 1))]);
+ canvas.drawCircle(rect.centerX(), rect.centerY(), radius, pGraph);
+ }
+
+ 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(dimmedTextColor);
+
+ for (int i = 0; i < nRows; i++)
+ {
+ canvas.drawText(wdays[i], 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);
+ }
+
+ public void setIsBackgroundTransparent(boolean isBackgroundTransparent)
+ {
+ this.isBackgroundTransparent = isBackgroundTransparent;
+ createColors();
+ }
+}
diff --git a/app/src/main/java/org/isoron/uhabits/widgets/FrequencyWidgetProvider.java b/app/src/main/java/org/isoron/uhabits/widgets/FrequencyWidgetProvider.java
new file mode 100644
index 000000000..8ea2248df
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/widgets/FrequencyWidgetProvider.java
@@ -0,0 +1,64 @@
+/*
+ * 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.widgets;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.view.View;
+
+import org.isoron.uhabits.R;
+import org.isoron.uhabits.models.Habit;
+import org.isoron.uhabits.views.HabitFrequencyView;
+
+public class FrequencyWidgetProvider extends BaseWidgetProvider
+{
+ @Override
+ protected View buildCustomView(Context context, Habit habit)
+ {
+ HabitFrequencyView view = new HabitFrequencyView(context, null);
+ view.setIsBackgroundTransparent(true);
+ view.setHabit(habit);
+ return view;
+ }
+
+ @Override
+ protected PendingIntent getOnClickPendingIntent(Context context, Habit habit)
+ {
+ return null;
+ }
+
+ @Override
+ protected int getDefaultHeight()
+ {
+ return 200;
+ }
+
+ @Override
+ protected int getDefaultWidth()
+ {
+ return 200;
+ }
+
+ @Override
+ protected int getLayoutId()
+ {
+ return R.layout.widget_graph;
+ }
+}
diff --git a/app/src/main/res/drawable/widget_preview_frequency.png b/app/src/main/res/drawable/widget_preview_frequency.png
new file mode 100644
index 000000000..a3fc86505
Binary files /dev/null and b/app/src/main/res/drawable/widget_preview_frequency.png differ
diff --git a/app/src/main/res/layout/show_habit.xml b/app/src/main/res/layout/show_habit.xml
index 8db43453e..d603b891a 100644
--- a/app/src/main/res/layout/show_habit.xml
+++ b/app/src/main/res/layout/show_habit.xml
@@ -62,9 +62,9 @@
+ android:orientation="vertical"
+ android:paddingBottom="0dp">
+ android:layout_height="160dp"/>
+ android:text="@string/edit"
+ android:textColor="@color/grey_400"/>
-
+
-
-
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index 206f8c71c..96c48a8b0 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -95,4 +95,5 @@
"Version %s"
+ Fréquence
\ No newline at end of file
diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml
index 1a0b06e22..6ef9f817d 100644
--- a/app/src/main/res/values-pt/strings.xml
+++ b/app/src/main/res/values-pt/strings.xml
@@ -106,4 +106,5 @@
Desenvolvedores
Tradutores
Versão %s
+ Frequência
\ No newline at end of file
diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml
index 88631ae3c..48a057667 100644
--- a/app/src/main/res/values-zh/strings.xml
+++ b/app/src/main/res/values-zh/strings.xml
@@ -116,4 +116,5 @@
翻译者
导出数据
%s版
+ 频率
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 5ca20cd66..793dec774 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -122,5 +122,7 @@
Translators
Developers
Version %s
+ Frequency
+ Checkmark
\ No newline at end of file
diff --git a/app/src/main/res/xml/widget_frequency_info.xml b/app/src/main/res/xml/widget_frequency_info.xml
new file mode 100644
index 000000000..32a25906b
--- /dev/null
+++ b/app/src/main/res/xml/widget_frequency_info.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
\ No newline at end of file