diff --git a/app/src/main/java/org/isoron/uhabits/dialogs/HistoryEditorDialog.java b/app/src/main/java/org/isoron/uhabits/dialogs/HistoryEditorDialog.java new file mode 100644 index 000000000..9979ef8ab --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/dialogs/HistoryEditorDialog.java @@ -0,0 +1,115 @@ +/* + * 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.dialogs; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.util.DisplayMetrics; +import android.util.Log; + +import org.isoron.uhabits.R; +import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.views.HabitHistoryView; + +public class HistoryEditorDialog extends DialogFragment + implements DialogInterface.OnClickListener +{ + private Habit habit; + private Listener listener; + HabitHistoryView historyView; + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) + { + Context context = getActivity(); + historyView = new HabitHistoryView(context, null); + int p = (int) getResources().getDimension(R.dimen.history_editor_padding); + + if(savedInstanceState != null) + { + long id = savedInstanceState.getLong("habit", -1); + if(id > 0) this.habit = Habit.get(id); + } + + historyView.setPadding(p, 0, p, 0); + historyView.setHabit(habit); + historyView.setIsEditable(true); + + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle("History Editor") + .setView(historyView) + .setPositiveButton(android.R.string.ok, this); + + return builder.create(); + } + + @Override + public void onResume() + { + super.onResume(); + + DisplayMetrics metrics = getResources().getDisplayMetrics(); + int maxHeight = getResources().getDimensionPixelSize(R.dimen.history_editor_max_height); + int width = metrics.widthPixels; + int height = Math.min(metrics.heightPixels, maxHeight); + + Log.d("HistoryEditorDialog", String.format("h=%d max_h=%d", height, maxHeight)); + + getDialog().getWindow().setLayout(width, height); + } + + @Override + public void onClick(DialogInterface dialog, int which) + { + dismiss(); + } + + public void setHabit(Habit habit) + { + this.habit = habit; + if(historyView != null) historyView.setHabit(habit); + } + + @Override + public void onPause() + { + super.onPause(); + if(listener != null) listener.onHistoryEditorClosed(); + } + + @Override + public void onSaveInstanceState(Bundle outState) + { + outState.putLong("habit", habit.getId()); + } + + public void setListener(Listener listener) + { + this.listener = listener; + } + + public interface Listener { + void onHistoryEditorClosed(); + } +} 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 c8b0e548c..00193800e 100644 --- a/app/src/main/java/org/isoron/uhabits/fragments/ShowHabitFragment.java +++ b/app/src/main/java/org/isoron/uhabits/fragments/ShowHabitFragment.java @@ -28,6 +28,7 @@ import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.widget.Button; import android.widget.TextView; import org.isoron.helpers.ColorHelper; @@ -35,6 +36,7 @@ import org.isoron.helpers.Command; import org.isoron.helpers.DialogHelper; import org.isoron.uhabits.R; import org.isoron.uhabits.ShowHabitActivity; +import org.isoron.uhabits.dialogs.HistoryEditorDialog; import org.isoron.uhabits.helpers.ReminderHelper; import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Score; @@ -43,10 +45,14 @@ import org.isoron.uhabits.views.HabitScoreView; import org.isoron.uhabits.views.HabitStreakView; import org.isoron.uhabits.views.RingView; -public class ShowHabitFragment extends Fragment implements DialogHelper.OnSavedListener +public class ShowHabitFragment extends Fragment + implements DialogHelper.OnSavedListener, HistoryEditorDialog.Listener { protected ShowHabitActivity activity; private Habit habit; + private HabitStreakView streakView; + private HabitScoreView scoreView; + private HabitHistoryView historyView; @Override public void onStart() @@ -64,6 +70,54 @@ public class ShowHabitFragment extends Fragment implements DialogHelper.OnSavedL habit.checkmarks.rebuild(); + Button btEditHistory = (Button) view.findViewById(R.id.btEditHistory); + streakView = (HabitStreakView) view.findViewById(R.id.streakView); + scoreView = (HabitScoreView) view.findViewById(R.id.scoreView); + historyView = (HabitHistoryView) view.findViewById(R.id.historyView); + + updateHeaders(view); + updateScoreRing(view); + + streakView.setHabit(habit); + scoreView.setHabit(habit); + historyView.setHabit(habit); + + btEditHistory.setOnClickListener(new View.OnClickListener() + { + @Override + public void onClick(View v) + { + HistoryEditorDialog frag = new HistoryEditorDialog(); + frag.setHabit(habit); + frag.setListener(ShowHabitFragment.this); + frag.show(getFragmentManager(), "historyEditor"); + } + }); + + if(savedInstanceState != null) + { + EditHabitFragment fragEdit = (EditHabitFragment) getFragmentManager() + .findFragmentByTag("editHabit"); + HistoryEditorDialog fragEditor = (HistoryEditorDialog) getFragmentManager() + .findFragmentByTag("historyEditor"); + + if(fragEdit != null) fragEdit.setOnSavedListener(this); + if(fragEditor != null) fragEditor.setListener(this); + } + + setHasOptionsMenu(true); + return view; + } + + private void updateScoreRing(View view) + { + RingView scoreRing = (RingView) view.findViewById(R.id.scoreRing); + scoreRing.setColor(habit.color); + scoreRing.setPercentage((float) habit.scores.getNewestValue() / Score.MAX_SCORE); + } + + private void updateHeaders(View view) + { if (android.os.Build.VERSION.SDK_INT >= 21) { int darkerHabitColor = ColorHelper.mixColors(habit.color, Color.BLACK, 0.75f); @@ -74,24 +128,10 @@ public class ShowHabitFragment extends Fragment implements DialogHelper.OnSavedL TextView tvOverview = (TextView) view.findViewById(R.id.tvOverview); TextView tvStrength = (TextView) view.findViewById(R.id.tvStrength); TextView tvStreaks = (TextView) view.findViewById(R.id.tvStreaks); - RingView scoreRing = (RingView) view.findViewById(R.id.scoreRing); - HabitStreakView streakView = (HabitStreakView) view.findViewById(R.id.streakView); - HabitScoreView scoreView = (HabitScoreView) view.findViewById(R.id.scoreView); - HabitHistoryView historyView = (HabitHistoryView) view.findViewById(R.id.historyView); - tvHistory.setTextColor(habit.color); tvOverview.setTextColor(habit.color); tvStrength.setTextColor(habit.color); tvStreaks.setTextColor(habit.color); - - scoreRing.setColor(habit.color); - scoreRing.setPercentage((float) habit.scores.getNewestValue() / Score.MAX_SCORE); - streakView.setHabit(habit); - scoreView.setHabit(habit); - historyView.setHabit(habit); - - setHasOptionsMenu(true); - return view; } @Override @@ -109,7 +149,7 @@ public class ShowHabitFragment extends Fragment implements DialogHelper.OnSavedL { EditHabitFragment frag = EditHabitFragment.editSingleHabitFragment(habit.getId()); frag.setOnSavedListener(this); - frag.show(getFragmentManager(), "dialog"); + frag.show(getFragmentManager(), "editHabit"); return true; } } @@ -128,4 +168,18 @@ public class ShowHabitFragment extends Fragment implements DialogHelper.OnSavedL ReminderHelper.createReminderAlarms(activity); activity.recreate(); } + + @Override + public void onHistoryEditorClosed() + { + refreshData(); + } + + private void refreshData() + { + streakView.refreshData(); + historyView.refreshData(); + scoreView.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 92d3b0cf0..3d9986dec 100644 --- a/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java +++ b/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java @@ -68,6 +68,8 @@ public class RepetitionList public void toggle(long timestamp) { + timestamp = DateHelper.getStartOfDay(timestamp); + if (contains(timestamp)) { delete(timestamp); diff --git a/app/src/main/java/org/isoron/uhabits/models/ScoreList.java b/app/src/main/java/org/isoron/uhabits/models/ScoreList.java index 2a34a4571..18b67758d 100644 --- a/app/src/main/java/org/isoron/uhabits/models/ScoreList.java +++ b/app/src/main/java/org/isoron/uhabits/models/ScoreList.java @@ -126,6 +126,9 @@ public class ScoreList public int[] getAllValues(Long fromTimestamp, Long toTimestamp, Integer divisor) { + // Force rebuild of the score table + getNewestValue(); + Long offset = toTimestamp - (divisor - 1) * DateHelper.millisecondsInOneDay; String query = "select ((timestamp - ?) / ?) as time, avg(score) from Score " + diff --git a/app/src/main/java/org/isoron/uhabits/views/HabitHistoryView.java b/app/src/main/java/org/isoron/uhabits/views/HabitHistoryView.java index d967db6e1..67191b417 100644 --- a/app/src/main/java/org/isoron/uhabits/views/HabitHistoryView.java +++ b/app/src/main/java/org/isoron/uhabits/views/HabitHistoryView.java @@ -25,10 +25,13 @@ import android.graphics.Color; import android.graphics.Paint; import android.graphics.Paint.Align; import android.graphics.Rect; +import android.os.AsyncTask; import android.util.AttributeSet; +import android.view.MotionEvent; import org.isoron.helpers.ColorHelper; import org.isoron.helpers.DateHelper; +import org.isoron.uhabits.R; import org.isoron.uhabits.models.Habit; import java.text.SimpleDateFormat; @@ -50,7 +53,6 @@ public class HabitHistoryView extends ScrollableDataView private int columnWidth; private int columnHeight; private int nColumns; - private int baseSize; private String wdays[]; private SimpleDateFormat dfMonth; @@ -65,11 +67,14 @@ public class HabitHistoryView extends ScrollableDataView private boolean isBackgroundTransparent; private int textColor; + private boolean isEditable; public HabitHistoryView(Context context, AttributeSet attrs) { super(context, attrs); this.primaryColor = ColorHelper.palette[7]; + this.checkmarks = new int[0]; + this.isEditable = false; init(); } @@ -77,12 +82,13 @@ public class HabitHistoryView extends ScrollableDataView { this.habit = habit; createColors(); - fetchData(); + refreshData(); postInvalidate(); } private void init() { + refreshData(); createPaints(); createColors(); @@ -96,6 +102,7 @@ public class HabitHistoryView extends ScrollableDataView private void updateDate() { baseDate = new GregorianCalendar(); + baseDate.setTimeInMillis(DateHelper.getLocalTime()); baseDate.add(Calendar.DAY_OF_YEAR, -(getDataOffset() - 1) * 7); nDays = (nColumns - 1) * 7; @@ -117,23 +124,42 @@ public class HabitHistoryView extends ScrollableDataView protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { if(height < 8) height = 200; - - baseSize = height / 8; + int baseSize = height / 8; setScrollerBucketSize(baseSize); - columnWidth = baseSize; - columnHeight = 8 * baseSize; - nColumns = width / baseSize; - squareSpacing = (int) Math.floor(baseSize / 15.0); - pSquareFg.setTextSize(baseSize * 0.5f); - pTextHeader.setTextSize(baseSize * 0.5f); + int maxTextSize = getResources().getDimensionPixelSize(R.dimen.history_max_font_size); + float textSize = Math.min(baseSize * 0.5f, maxTextSize); + + pSquareFg.setTextSize(textSize); + pTextHeader.setTextSize(textSize); squareTextOffset = pSquareFg.getFontSpacing() * 0.4f; headerTextOffset = pTextHeader.getFontSpacing() * 0.3f; + int rightLabelWidth = getWeekdayLabelWidth(); + int horizontalPadding = getPaddingRight() + getPaddingLeft(); + + columnWidth = baseSize; + columnHeight = 8 * baseSize; + nColumns = (width - rightLabelWidth - horizontalPadding) / baseSize + 1; + updateDate(); } + private int getWeekdayLabelWidth() + { + int width = 0; + Rect bounds = new Rect(); + + for(String w : wdays) + { + pSquareFg.getTextBounds(w, 0, w.length(), bounds); + width = Math.max(width, bounds.right); + } + + return width; + } + private void createColors() { if(habit != null) @@ -179,22 +205,18 @@ public class HabitHistoryView extends ScrollableDataView pSquareFg.setTextAlign(Align.CENTER); } - protected void fetchData() + public void refreshData() { if(isInEditMode()) generateRandomData(); else { - if(habit == null) - { - checkmarks = new int[0]; - return; - } - + if(habit == null) return; checkmarks = habit.checkmarks.getAllValues(); } updateDate(); + invalidate(); } private void generateRandomData() @@ -226,6 +248,7 @@ public class HabitHistoryView extends ScrollableDataView super.onDraw(canvas); baseLocation.set(0, 0, columnWidth - squareSpacing, columnWidth - squareSpacing); + baseLocation.offset(getPaddingLeft(), getPaddingTop()); previousMonth = ""; previousYear = ""; @@ -329,4 +352,59 @@ public class HabitHistoryView extends ScrollableDataView this.isBackgroundTransparent = isBackgroundTransparent; createColors(); } + + @Override + public boolean onSingleTapUp(MotionEvent e) + { + if(!isEditable) return false; + + int pointerId = e.getPointerId(0); + float x = e.getX(pointerId); + float y = e.getY(pointerId); + + final Long timestamp = positionToTimestamp(x, y); + if(timestamp == null) return false; + + new AsyncTask() + { + @Override + protected Void doInBackground(Void... params) + { + habit.repetitions.toggle(timestamp); + return null; + } + + @Override + protected void onPostExecute(Void aVoid) + { + refreshData(); + invalidate(); + } + }.execute(); + + return true; + } + + private Long positionToTimestamp(float x, float y) + { + int col = (int) (x / columnWidth); + int row = (int) (y / columnWidth); + + if(row == 0) return null; + if(col == nColumns - 1) return null; + + int offset = col * 7 + (row - 1); + Calendar date = (Calendar) baseDate.clone(); + date.add(Calendar.DAY_OF_YEAR, offset); + + if(DateHelper.getStartOfDay(date.getTimeInMillis()) > DateHelper.getStartOfToday()) + return null; + + return date.getTimeInMillis(); + } + + public void setIsEditable(boolean isEditable) + { + this.isEditable = isEditable; + } } diff --git a/app/src/main/java/org/isoron/uhabits/views/HabitScoreView.java b/app/src/main/java/org/isoron/uhabits/views/HabitScoreView.java index 6ec91bc6f..02f596e0b 100644 --- a/app/src/main/java/org/isoron/uhabits/views/HabitScoreView.java +++ b/app/src/main/java/org/isoron/uhabits/views/HabitScoreView.java @@ -71,6 +71,7 @@ public class HabitScoreView extends ScrollableDataView { super(context, attrs); this.primaryColor = ColorHelper.palette[7]; + this.scores = new int[0]; init(); } @@ -78,12 +79,13 @@ public class HabitScoreView extends ScrollableDataView { this.habit = habit; createColors(); - fetchData(); + refreshData(); postInvalidate(); } private void init() { + refreshData(); createPaints(); createColors(); @@ -162,21 +164,17 @@ public class HabitScoreView extends ScrollableDataView em = pText.getFontSpacing(); } - protected void fetchData() + public void refreshData() { if(isInEditMode()) generateRandomData(); else { - if (habit == null) - { - scores = new int[0]; - return; - } - + if (habit == null) return; scores = habit.scores.getAllValues(BUCKET_SIZE * DateHelper.millisecondsInOneDay); } + invalidate(); } private void generateRandomData() 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 1ac4f5a0c..1fc61cc0c 100644 --- a/app/src/main/java/org/isoron/uhabits/views/HabitStreakView.java +++ b/app/src/main/java/org/isoron/uhabits/views/HabitStreakView.java @@ -65,6 +65,7 @@ public class HabitStreakView extends ScrollableDataView { super(context, attrs); this.primaryColor = ColorHelper.palette[7]; + startTimes = endTimes = lengths = new long[0]; init(); } @@ -73,12 +74,13 @@ public class HabitStreakView extends ScrollableDataView this.habit = habit; createColors(); - fetchData(); + refreshData(); postInvalidate(); } private void init() { + refreshData(); createPaints(); createColors(); @@ -157,17 +159,13 @@ public class HabitStreakView extends ScrollableDataView pBar.setAntiAlias(true); } - protected void fetchData() + public void refreshData() { if(isInEditMode()) generateRandomData(); else { - if(habit == null) - { - startTimes = endTimes = lengths = new long[0]; - return; - } + if(habit == null) return; List streaks = habit.streaks.getAll(); int size = streaks.size(); @@ -187,6 +185,8 @@ public class HabitStreakView extends ScrollableDataView maxStreakLength = Math.max(maxStreakLength, s.length); } } + + invalidate(); } private void generateRandomData() diff --git a/app/src/main/java/org/isoron/uhabits/views/ScrollableDataView.java b/app/src/main/java/org/isoron/uhabits/views/ScrollableDataView.java index 7ee55f55d..8ad2be7c5 100644 --- a/app/src/main/java/org/isoron/uhabits/views/ScrollableDataView.java +++ b/app/src/main/java/org/isoron/uhabits/views/ScrollableDataView.java @@ -58,8 +58,6 @@ public abstract class ScrollableDataView extends View implements GestureDetector scrollAnimator.addUpdateListener(this); } - protected abstract void fetchData(); - @Override public boolean onTouchEvent(MotionEvent event) { diff --git a/app/src/main/res/layout/show_habit.xml b/app/src/main/res/layout/show_habit.xml index cfe845f68..8db43453e 100644 --- a/app/src/main/res/layout/show_habit.xml +++ b/app/src/main/res/layout/show_habit.xml @@ -18,8 +18,8 @@ --> - + - - + android:layout_height="160dp" /> + +