diff --git a/app/src/main/java/org/isoron/uhabits/activities/common/views/BundleSavedState.java b/app/src/main/java/org/isoron/uhabits/activities/common/views/BundleSavedState.java index 16eb77d88..9f61d88f0 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/common/views/BundleSavedState.java +++ b/app/src/main/java/org/isoron/uhabits/activities/common/views/BundleSavedState.java @@ -40,7 +40,7 @@ public class BundleSavedState extends View.BaseSavedState } }; - final Bundle bundle; + public final Bundle bundle; public BundleSavedState(Parcelable superState, Bundle bundle) { diff --git a/app/src/main/java/org/isoron/uhabits/activities/common/views/ScrollableChart.java b/app/src/main/java/org/isoron/uhabits/activities/common/views/ScrollableChart.java index 5cbda9bd4..8b54b324f 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/common/views/ScrollableChart.java +++ b/app/src/main/java/org/isoron/uhabits/activities/common/views/ScrollableChart.java @@ -33,7 +33,9 @@ public abstract class ScrollableChart extends View private int dataOffset; - private int scrollerBucketSize; + private int scrollerBucketSize = 1; + + private int direction = 1; private GestureDetector detector; @@ -41,6 +43,10 @@ public abstract class ScrollableChart extends View private ValueAnimator scrollAnimator; + private ScrollController scrollController; + + private int maxDataOffset = 10000; + public ScrollableChart(Context context) { super(context); @@ -64,8 +70,7 @@ public abstract class ScrollableChart extends View if (!scroller.isFinished()) { scroller.computeScrollOffset(); - dataOffset = Math.max(0, scroller.getCurrX() / scrollerBucketSize); - postInvalidate(); + updateDataOffset(); } else { @@ -86,19 +91,44 @@ public abstract class ScrollableChart extends View float velocityY) { scroller.fling(scroller.getCurrX(), scroller.getCurrY(), - (int) velocityX / 2, 0, 0, 100000, 0, 0); + direction * ((int) velocityX) / 2, 0, 0, getMaxX(), 0, 0); invalidate(); scrollAnimator.setDuration(scroller.getDuration()); scrollAnimator.start(); - return false; } + private int getMaxX() + { + return maxDataOffset * scrollerBucketSize; + } + @Override - public void onLongPress(MotionEvent e) + public void onRestoreInstanceState(Parcelable state) { + BundleSavedState bss = (BundleSavedState) state; + int x = bss.bundle.getInt("x"); + int y = bss.bundle.getInt("y"); + direction = bss.bundle.getInt("direction"); + dataOffset = bss.bundle.getInt("dataOffset"); + maxDataOffset = bss.bundle.getInt("maxDataOffset"); + scroller.startScroll(0, 0, x, y, 0); + scroller.computeScrollOffset(); + super.onRestoreInstanceState(bss.getSuperState()); + } + @Override + public Parcelable onSaveInstanceState() + { + Parcelable superState = super.onSaveInstanceState(); + Bundle bundle = new Bundle(); + bundle.putInt("x", scroller.getCurrX()); + bundle.putInt("y", scroller.getCurrY()); + bundle.putInt("dataOffset", dataOffset); + bundle.putInt("direction", direction); + bundle.putInt("maxDataOffset", maxDataOffset); + return new BundleSavedState(superState, bundle); } @Override @@ -112,12 +142,14 @@ public abstract class ScrollableChart extends View if (parent != null) parent.requestDisallowInterceptTouchEvent(true); } - scroller.startScroll(scroller.getCurrX(), scroller.getCurrY(), - (int) -dx, (int) dy, 0); - scroller.computeScrollOffset(); - dataOffset = Math.max(0, scroller.getCurrX() / scrollerBucketSize); - postInvalidate(); + dx = - direction * dx; + dx = Math.min(dx, getMaxX() - scroller.getCurrX()); + scroller.startScroll(scroller.getCurrX(), scroller.getCurrY(), (int) dx, + (int) dy, 0); + + scroller.computeScrollOffset(); + updateDataOffset(); return true; } @@ -139,32 +171,35 @@ public abstract class ScrollableChart extends View return detector.onTouchEvent(event); } - public void setScrollerBucketSize(int scrollerBucketSize) + public void setDirection(int direction) { - this.scrollerBucketSize = scrollerBucketSize; + if (direction != 1 && direction != -1) + throw new IllegalArgumentException(); + this.direction = direction; } @Override - public void onRestoreInstanceState(Parcelable state) + public void onLongPress(MotionEvent e) { - BundleSavedState bss = (BundleSavedState) state; - int x = bss.bundle.getInt("x"); - int y = bss.bundle.getInt("y"); - dataOffset = bss.bundle.getInt("dataOffset"); - scroller.startScroll(0, 0, x, y, 0); - scroller.computeScrollOffset(); - super.onRestoreInstanceState(bss.getSuperState()); + } - @Override - public Parcelable onSaveInstanceState() + public void setMaxDataOffset(int maxDataOffset) { - Parcelable superState = super.onSaveInstanceState(); - Bundle bundle = new Bundle(); - bundle.putInt("x", scroller.getCurrX()); - bundle.putInt("y", scroller.getCurrY()); - bundle.putInt("dataOffset", dataOffset); - return new BundleSavedState(superState, bundle); + this.maxDataOffset = maxDataOffset; + this.dataOffset = Math.min(dataOffset, maxDataOffset); + scrollController.onDataOffsetChanged(this.dataOffset); + postInvalidate(); + } + + public void setScrollController(ScrollController scrollController) + { + this.scrollController = scrollController; + } + + public void setScrollerBucketSize(int scrollerBucketSize) + { + this.scrollerBucketSize = scrollerBucketSize; } private void init(Context context) @@ -173,5 +208,25 @@ public abstract class ScrollableChart extends View scroller = new Scroller(context, null, true); scrollAnimator = ValueAnimator.ofFloat(0, 1); scrollAnimator.addUpdateListener(this); + scrollController = new ScrollController() {}; + } + + private void updateDataOffset() + { + int newDataOffset = scroller.getCurrX() / scrollerBucketSize; + newDataOffset = Math.max(0, newDataOffset); + newDataOffset = Math.min(maxDataOffset, newDataOffset); + + if (newDataOffset != dataOffset) + { + dataOffset = newDataOffset; + scrollController.onDataOffsetChanged(dataOffset); + postInvalidate(); + } + } + + public interface ScrollController + { + default void onDataOffsetChanged(int newDataOffset) {} } } diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsRootView.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsRootView.java index d25d5d4fd..7a716c9cd 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsRootView.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsRootView.java @@ -28,6 +28,7 @@ import android.widget.*; import org.isoron.uhabits.R; import org.isoron.uhabits.activities.*; +import org.isoron.uhabits.activities.common.views.*; import org.isoron.uhabits.activities.habits.list.controllers.*; import org.isoron.uhabits.activities.habits.list.model.*; import org.isoron.uhabits.activities.habits.list.views.*; @@ -43,7 +44,7 @@ import butterknife.*; public class ListHabitsRootView extends BaseRootView implements ModelObservable.Listener, TaskRunner.Listener { - public static final int MAX_CHECKMARK_COUNT = 21; + public static final int MAX_CHECKMARK_COUNT = 60; @BindView(R.id.listView) HabitCardListView listView; @@ -132,6 +133,13 @@ public class ListHabitsRootView extends BaseRootView listController.setSelectionListener(menu); listView.setController(listController); menu.setListController(listController); + header.setScrollController(new ScrollableChart.ScrollController() { + @Override + public void onDataOffsetChanged(int newDataOffset) + { + listView.setDataOffset(newDataOffset); + } + }); } @Override @@ -156,6 +164,7 @@ public class ListHabitsRootView extends BaseRootView { int count = getCheckmarkCount(); header.setButtonCount(count); + header.setMaxDataOffset(Math.max(MAX_CHECKMARK_COUNT - count, 0)); listView.setCheckmarkCount(count); super.onSizeChanged(w, h, oldw, oldh); } diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/model/HabitCardListAdapter.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/model/HabitCardListAdapter.java index 55656b542..1245668a0 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/list/model/HabitCardListAdapter.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/model/HabitCardListAdapter.java @@ -178,6 +178,20 @@ public class HabitCardListAdapter listView.bindCardView(holder, habit, score, checkmarks, selected); } + @Override + public void onViewAttachedToWindow(@Nullable HabitCardViewHolder holder) + { + if (listView == null) return; + listView.attachCardView(holder); + } + + @Override + public void onViewDetachedFromWindow(@Nullable HabitCardViewHolder holder) + { + if (listView == null) return; + listView.detachCardView(holder); + } + @Override public HabitCardViewHolder onCreateViewHolder(ViewGroup parent, int viewType) diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkPanelView.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkPanelView.java index e5b47800c..b569f9208 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkPanelView.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkPanelView.java @@ -53,6 +53,8 @@ public class CheckmarkPanelView extends LinearLayout implements Preferences.List @NonNull private Habit habit; + private int dataOffset; + public CheckmarkPanelView(Context context) { super(context); @@ -75,19 +77,23 @@ public class CheckmarkPanelView extends LinearLayout implements Preferences.List return (CheckmarkButtonView) getChildAt(position); } - public void setCheckmarkValues(int[] checkmarkValues) + public void setButtonCount(int newButtonCount) { - this.checkmarkValues = checkmarkValues; - - if (this.nButtons != checkmarkValues.length) + if(nButtons != newButtonCount) { - this.nButtons = checkmarkValues.length; + nButtons = newButtonCount; addCheckmarkButtons(); } setupCheckmarkButtons(); } + public void setCheckmarkValues(int[] checkmarkValues) + { + this.checkmarkValues = checkmarkValues; + setupCheckmarkButtons(); + } + public void setColor(int color) { this.color = color; @@ -100,6 +106,12 @@ public class CheckmarkPanelView extends LinearLayout implements Preferences.List setupCheckmarkButtons(); } + public void setDataOffset(int dataOffset) + { + this.dataOffset = dataOffset; + setupCheckmarkButtons(); + } + public void setHabit(@NonNull Habit habit) { this.habit = habit; @@ -170,11 +182,13 @@ public class CheckmarkPanelView extends LinearLayout implements Preferences.List { long timestamp = DateUtils.getStartOfToday(); long day = DateUtils.millisecondsInOneDay; + timestamp -= day * dataOffset; for (int i = 0; i < nButtons; i++) { CheckmarkButtonView buttonView = indexToButton(i); - buttonView.setValue(checkmarkValues[i]); + if(i + dataOffset >= checkmarkValues.length) break; + buttonView.setValue(checkmarkValues[i + dataOffset]); buttonView.setColor(color); setupButtonControllers(timestamp, buttonView); timestamp -= day; diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListView.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListView.java index fab2680eb..bc784465e 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListView.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListView.java @@ -20,15 +20,17 @@ package org.isoron.uhabits.activities.habits.list.views; import android.content.*; +import android.os.*; import android.support.annotation.*; import android.support.v7.widget.*; import android.support.v7.widget.helper.*; import android.util.*; import android.view.*; -import org.isoron.uhabits.models.*; +import org.isoron.uhabits.activities.common.views.*; import org.isoron.uhabits.activities.habits.list.controllers.*; import org.isoron.uhabits.activities.habits.list.model.*; +import org.isoron.uhabits.models.*; import java.util.*; @@ -44,6 +46,10 @@ public class HabitCardListView extends RecyclerView private int checkmarkCount; + private int dataOffset; + + private LinkedList attachedHolders; + public HabitCardListView(Context context, AttributeSet attrs) { super(context, attrs); @@ -54,6 +60,13 @@ public class HabitCardListView extends RecyclerView TouchHelperCallback callback = new TouchHelperCallback(); touchHelper = new ItemTouchHelper(callback); touchHelper.attachToRecyclerView(this); + + attachedHolders = new LinkedList<>(); + } + + public void attachCardView(HabitCardViewHolder holder) + { + attachedHolders.add(holder); } /** @@ -75,13 +88,12 @@ public class HabitCardListView extends RecyclerView int[] checkmarks, boolean selected) { - int visibleCheckmarks[] = - Arrays.copyOfRange(checkmarks, 0, checkmarkCount); - HabitCardView cardView = (HabitCardView) holder.itemView; cardView.setHabit(habit); cardView.setSelected(selected); - cardView.setCheckmarkValues(visibleCheckmarks); + cardView.setCheckmarkValues(checkmarks); + cardView.setCheckmarkCount(checkmarkCount); + cardView.setDataOffset(dataOffset); cardView.setScore(score); if (controller != null) setupCardViewController(holder); return cardView; @@ -92,6 +104,11 @@ public class HabitCardListView extends RecyclerView return new HabitCardView(getContext()); } + public void detachCardView(HabitCardViewHolder holder) + { + attachedHolders.remove(holder); + } + @Override public void setAdapter(RecyclerView.Adapter adapter) { @@ -109,6 +126,16 @@ public class HabitCardListView extends RecyclerView this.controller = controller; } + public void setDataOffset(int dataOffset) + { + this.dataOffset = dataOffset; + for (HabitCardViewHolder holder : attachedHolders) + { + HabitCardView cardView = (HabitCardView) holder.itemView; + cardView.setDataOffset(dataOffset); + } + } + @Override protected void onAttachedToWindow() { @@ -123,6 +150,23 @@ public class HabitCardListView extends RecyclerView super.onDetachedFromWindow(); } + @Override + protected void onRestoreInstanceState(Parcelable state) + { + BundleSavedState bss = (BundleSavedState) state; + dataOffset = bss.bundle.getInt("dataOffset"); + super.onRestoreInstanceState(bss.getSuperState()); + } + + @Override + protected Parcelable onSaveInstanceState() + { + Parcelable superState = super.onSaveInstanceState(); + Bundle bundle = new Bundle(); + bundle.putInt("dataOffset", dataOffset); + return new BundleSavedState(superState, bundle); + } + protected void setupCardViewController(@NonNull HabitCardViewHolder holder) { HabitCardView cardView = (HabitCardView) holder.itemView; @@ -168,7 +212,7 @@ public class HabitCardListView extends RecyclerView { int position = holder.getAdapterPosition(); if (controller != null) controller.onItemLongClick(position); - if(adapter.isSortable()) touchHelper.startDrag(holder); + if (adapter.isSortable()) touchHelper.startDrag(holder); } @Override diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.java index bc2123c7c..d57e0a71c 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.java @@ -72,6 +72,8 @@ public class HabitCardView extends FrameLayout @Nullable private Habit habit; + private int dataOffset; + public HabitCardView(Context context) { super(context); @@ -90,6 +92,11 @@ public class HabitCardView extends FrameLayout new Handler(Looper.getMainLooper()).post(() -> refresh()); } + public void setCheckmarkCount(int checkmarkCount) + { + checkmarkPanel.setButtonCount(checkmarkCount); + } + public void setCheckmarkValues(int checkmarks[]) { checkmarkPanel.setCheckmarkValues(checkmarks); @@ -103,6 +110,12 @@ public class HabitCardView extends FrameLayout checkmarkPanel.setController(controller); } + public void setDataOffset(int dataOffset) + { + this.dataOffset = dataOffset; + checkmarkPanel.setDataOffset(dataOffset); + } + public void setHabit(@NonNull Habit habit) { if (this.habit != null) detachFromHabit(); @@ -134,7 +147,7 @@ public class HabitCardView extends FrameLayout { long today = DateUtils.getStartOfToday(); long day = DateUtils.millisecondsInOneDay; - int offset = (int) ((today - timestamp) / day); + int offset = (int) ((today - timestamp) / day) - dataOffset; CheckmarkButtonView button = checkmarkPanel.indexToButton(offset); float y = button.getHeight() / 2.0f; diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HeaderView.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HeaderView.java index 8f9d7c4d0..e4863cd54 100644 --- a/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HeaderView.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HeaderView.java @@ -20,22 +20,23 @@ package org.isoron.uhabits.activities.habits.list.views; import android.content.*; +import android.content.res.*; +import android.graphics.*; import android.support.annotation.*; +import android.text.*; import android.util.*; -import android.view.*; -import android.widget.*; import org.isoron.uhabits.*; +import org.isoron.uhabits.activities.common.views.*; import org.isoron.uhabits.activities.habits.list.*; import org.isoron.uhabits.preferences.*; import org.isoron.uhabits.utils.*; import java.util.*; -public class HeaderView extends LinearLayout +public class HeaderView extends ScrollableChart implements Preferences.Listener, MidnightTimer.MidnightListener { - private final Context context; private int buttonCount; @@ -45,10 +46,15 @@ public class HeaderView extends LinearLayout @Nullable private MidnightTimer midnightTimer; + private final TextPaint paint; + + private RectF rect; + + private int maxDataOffset; + public HeaderView(Context context, AttributeSet attrs) { super(context, attrs); - this.context = context; if (isInEditMode()) { @@ -67,24 +73,40 @@ public class HeaderView extends LinearLayout ListHabitsActivity activity = (ListHabitsActivity) context; midnightTimer = activity.getListHabitsComponent().getMidnightTimer(); } + + Resources res = context.getResources(); + setScrollerBucketSize((int) res.getDimension(R.dimen.checkmarkWidth)); + setDirection(shouldReverseCheckmarks() ? 1 : -1); + + StyledResources sr = new StyledResources(context); + paint = new TextPaint(); + paint.setColor(Color.BLACK); + paint.setAntiAlias(true); + paint.setTextSize(getResources().getDimension(R.dimen.tinyTextSize)); + paint.setTextAlign(Paint.Align.CENTER); + paint.setTypeface(Typeface.DEFAULT_BOLD); + paint.setColor(sr.getColor(R.attr.mediumContrastTextColor)); + + rect = new RectF(); } @Override public void atMidnight() { - post(() -> createButtons()); + post(() -> invalidate()); } @Override public void onCheckmarkOrderChanged() { - createButtons(); + setDirection(shouldReverseCheckmarks() ? 1 : -1); + postInvalidate(); } public void setButtonCount(int buttonCount) { this.buttonCount = buttonCount; - createButtons(); + postInvalidate(); } @Override @@ -103,23 +125,45 @@ public class HeaderView extends LinearLayout super.onDetachedFromWindow(); } - private void createButtons() + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) + { + int width = MeasureSpec.getSize(widthMeasureSpec); + int height = (int) getContext() + .getResources() + .getDimension(R.dimen.checkmarkHeight); + setMeasuredDimension(width, height); + } + + @Override + protected void onDraw(Canvas canvas) { - removeAllViews(); + super.onDraw(canvas); + GregorianCalendar day = DateUtils.getStartOfTodayCalendar(); + Resources res = getContext().getResources(); + float width = res.getDimension(R.dimen.checkmarkWidth); + float height = res.getDimension(R.dimen.checkmarkHeight); + boolean reverse = shouldReverseCheckmarks(); - for (int i = 0; i < buttonCount; i++) - addView( - inflate(context, R.layout.list_habits_header_checkmark, null)); + day.add(GregorianCalendar.DAY_OF_MONTH, -getDataOffset()); + float em = paint.measureText("m"); - for (int i = 0; i < getChildCount(); i++) + for (int i = 0; i < buttonCount; i++) { - int position = i; - if (shouldReverseCheckmarks()) position = getChildCount() - i - 1; + rect.set(0, 0, width, height); + rect.offset(canvas.getWidth(), 0); + if(reverse) rect.offset(- (i + 1) * width, 0); + else rect.offset((i - buttonCount) * width, 0); + + String text = DateUtils.formatHeaderDate(day).toUpperCase(); + String[] lines = text.split("\n"); + + int y1 = (int)(rect.centerY() - 0.25 * em); + int y2 = (int)(rect.centerY() + 1.25 * em); - View button = getChildAt(position); - TextView label = (TextView) button.findViewById(R.id.tvCheck); - label.setText(DateUtils.formatHeaderDate(day)); + canvas.drawText(lines[0], rect.centerX(), y1, paint); + canvas.drawText(lines[1], rect.centerX(), y2, paint); day.add(GregorianCalendar.DAY_OF_MONTH, -1); } }