1 package com.android.launcher3.pageindicators;
2 
3 import android.animation.Animator;
4 import android.animation.AnimatorListenerAdapter;
5 import android.animation.ObjectAnimator;
6 import android.animation.ValueAnimator;
7 import android.content.Context;
8 import android.content.res.Resources;
9 import android.graphics.Canvas;
10 import android.graphics.Color;
11 import android.graphics.Paint;
12 import android.graphics.Rect;
13 import android.os.Handler;
14 import android.os.Looper;
15 import android.util.AttributeSet;
16 import android.util.Property;
17 import android.view.View;
18 import android.view.ViewConfiguration;
19 
20 import com.android.launcher3.Insettable;
21 import com.android.launcher3.Launcher;
22 import com.android.launcher3.R;
23 import com.android.launcher3.Utilities;
24 import com.android.launcher3.util.Themes;
25 
26 /**
27  * A PageIndicator that briefly shows a fraction of a line when moving between pages
28  *
29  * The fraction is 1 / number of pages and the position is based on the progress of the page scroll.
30  */
31 public class WorkspacePageIndicator extends View implements Insettable, PageIndicator {
32 
33     private static final int LINE_ANIMATE_DURATION = ViewConfiguration.getScrollBarFadeDuration();
34     private static final int LINE_FADE_DELAY = ViewConfiguration.getScrollDefaultDelay();
35     public static final int WHITE_ALPHA = (int) (0.70f * 255);
36     public static final int BLACK_ALPHA = (int) (0.65f * 255);
37 
38     private static final int LINE_ALPHA_ANIMATOR_INDEX = 0;
39     private static final int NUM_PAGES_ANIMATOR_INDEX = 1;
40     private static final int TOTAL_SCROLL_ANIMATOR_INDEX = 2;
41     private static final int ANIMATOR_COUNT = 3;
42 
43     private ValueAnimator[] mAnimators = new ValueAnimator[ANIMATOR_COUNT];
44 
45     private final Handler mDelayedLineFadeHandler = new Handler(Looper.getMainLooper());
46     private final Launcher mLauncher;
47 
48     private boolean mShouldAutoHide = true;
49 
50     // The alpha of the line when it is showing.
51     private int mActiveAlpha = 0;
52     // The alpha that the line is being animated to or already at (either 0 or mActiveAlpha).
53     private int mToAlpha;
54     // A float value representing the number of pages, to allow for an animation when it changes.
55     private float mNumPagesFloat;
56     private int mCurrentScroll;
57     private int mTotalScroll;
58     private Paint mLinePaint;
59     private final int mLineHeight;
60 
61     private static final Property<WorkspacePageIndicator, Integer> PAINT_ALPHA
62             = new Property<WorkspacePageIndicator, Integer>(Integer.class, "paint_alpha") {
63         @Override
64         public Integer get(WorkspacePageIndicator obj) {
65             return obj.mLinePaint.getAlpha();
66         }
67 
68         @Override
69         public void set(WorkspacePageIndicator obj, Integer alpha) {
70             obj.mLinePaint.setAlpha(alpha);
71             obj.invalidate();
72         }
73     };
74 
75     private static final Property<WorkspacePageIndicator, Float> NUM_PAGES
76             = new Property<WorkspacePageIndicator, Float>(Float.class, "num_pages") {
77         @Override
78         public Float get(WorkspacePageIndicator obj) {
79             return obj.mNumPagesFloat;
80         }
81 
82         @Override
83         public void set(WorkspacePageIndicator obj, Float numPages) {
84             obj.mNumPagesFloat = numPages;
85             obj.invalidate();
86         }
87     };
88 
89     private static final Property<WorkspacePageIndicator, Integer> TOTAL_SCROLL
90             = new Property<WorkspacePageIndicator, Integer>(Integer.class, "total_scroll") {
91         @Override
92         public Integer get(WorkspacePageIndicator obj) {
93             return obj.mTotalScroll;
94         }
95 
96         @Override
97         public void set(WorkspacePageIndicator obj, Integer totalScroll) {
98             obj.mTotalScroll = totalScroll;
99             obj.invalidate();
100         }
101     };
102 
103     private Runnable mHideLineRunnable = () -> animateLineToAlpha(0);
104 
WorkspacePageIndicator(Context context)105     public WorkspacePageIndicator(Context context) {
106         this(context, null);
107     }
108 
WorkspacePageIndicator(Context context, AttributeSet attrs)109     public WorkspacePageIndicator(Context context, AttributeSet attrs) {
110         this(context, attrs, 0);
111     }
112 
WorkspacePageIndicator(Context context, AttributeSet attrs, int defStyle)113     public WorkspacePageIndicator(Context context, AttributeSet attrs, int defStyle) {
114         super(context, attrs, defStyle);
115 
116         Resources res = context.getResources();
117         mLinePaint = new Paint();
118         mLinePaint.setAlpha(0);
119 
120         mLauncher = Launcher.getLauncher(context);
121         mLineHeight = res.getDimensionPixelSize(R.dimen.workspace_page_indicator_line_height);
122 
123         boolean darkText = Themes.getAttrBoolean(mLauncher, R.attr.isWorkspaceDarkText);
124         mActiveAlpha = darkText ? BLACK_ALPHA : WHITE_ALPHA;
125         mLinePaint.setColor(darkText ? Color.BLACK : Color.WHITE);
126     }
127 
128     @Override
onDraw(Canvas canvas)129     protected void onDraw(Canvas canvas) {
130         if (mTotalScroll == 0 || mNumPagesFloat == 0) {
131             return;
132         }
133 
134         // Compute and draw line rect.
135         float progress = Utilities.boundToRange(((float) mCurrentScroll) / mTotalScroll, 0f, 1f);
136         int availableWidth = getWidth();
137         int lineWidth = (int) (availableWidth / mNumPagesFloat);
138         int lineLeft = (int) (progress * (availableWidth - lineWidth));
139         int lineRight = lineLeft + lineWidth;
140 
141         canvas.drawRoundRect(lineLeft, getHeight() / 2 - mLineHeight / 2, lineRight,
142                 getHeight() / 2 + mLineHeight / 2, mLineHeight, mLineHeight, mLinePaint);
143     }
144 
145     @Override
setScroll(int currentScroll, int totalScroll)146     public void setScroll(int currentScroll, int totalScroll) {
147         if (getAlpha() == 0) {
148             return;
149         }
150         animateLineToAlpha(mActiveAlpha);
151 
152         mCurrentScroll = currentScroll;
153         if (mTotalScroll == 0) {
154             mTotalScroll = totalScroll;
155         } else if (mTotalScroll != totalScroll) {
156             animateToTotalScroll(totalScroll);
157         } else {
158             invalidate();
159         }
160 
161         if (mShouldAutoHide) {
162             hideAfterDelay();
163         }
164     }
165 
hideAfterDelay()166     private void hideAfterDelay() {
167         mDelayedLineFadeHandler.removeCallbacksAndMessages(null);
168         mDelayedLineFadeHandler.postDelayed(mHideLineRunnable, LINE_FADE_DELAY);
169     }
170 
171     @Override
setActiveMarker(int activePage)172     public void setActiveMarker(int activePage) { }
173 
174     @Override
setMarkersCount(int numMarkers)175     public void setMarkersCount(int numMarkers) {
176         if (Float.compare(numMarkers, mNumPagesFloat) != 0) {
177             setupAndRunAnimation(ObjectAnimator.ofFloat(this, NUM_PAGES, numMarkers),
178                     NUM_PAGES_ANIMATOR_INDEX);
179         } else {
180             if (mAnimators[NUM_PAGES_ANIMATOR_INDEX] != null) {
181                 mAnimators[NUM_PAGES_ANIMATOR_INDEX].cancel();
182                 mAnimators[NUM_PAGES_ANIMATOR_INDEX] = null;
183             }
184         }
185     }
186 
187     @Override
setShouldAutoHide(boolean shouldAutoHide)188     public void setShouldAutoHide(boolean shouldAutoHide) {
189         mShouldAutoHide = shouldAutoHide;
190         if (shouldAutoHide && mLinePaint.getAlpha() > 0) {
191             hideAfterDelay();
192         } else if (!shouldAutoHide) {
193             mDelayedLineFadeHandler.removeCallbacksAndMessages(null);
194         }
195     }
196 
animateLineToAlpha(int alpha)197     private void animateLineToAlpha(int alpha) {
198         if (alpha == mToAlpha) {
199             // Ignore the new animation if it is going to the same alpha as the current animation.
200             return;
201         }
202         mToAlpha = alpha;
203         setupAndRunAnimation(ObjectAnimator.ofInt(this, PAINT_ALPHA, alpha),
204                 LINE_ALPHA_ANIMATOR_INDEX);
205     }
206 
animateToTotalScroll(int totalScroll)207     private void animateToTotalScroll(int totalScroll) {
208         setupAndRunAnimation(ObjectAnimator.ofInt(this, TOTAL_SCROLL, totalScroll),
209                 TOTAL_SCROLL_ANIMATOR_INDEX);
210     }
211 
212     /**
213      * Starts the given animator and stores it in the provided index in {@link #mAnimators} until
214      * the animation ends.
215      *
216      * If an animator is already at the index (i.e. it is already playing), it is canceled and
217      * replaced with the new animator.
218      */
setupAndRunAnimation(ValueAnimator animator, final int animatorIndex)219     private void setupAndRunAnimation(ValueAnimator animator, final int animatorIndex) {
220         if (mAnimators[animatorIndex] != null) {
221             mAnimators[animatorIndex].cancel();
222         }
223         mAnimators[animatorIndex] = animator;
224         mAnimators[animatorIndex].addListener(new AnimatorListenerAdapter() {
225             @Override
226             public void onAnimationEnd(Animator animation) {
227                 mAnimators[animatorIndex] = null;
228             }
229         });
230         mAnimators[animatorIndex].setDuration(LINE_ANIMATE_DURATION);
231         mAnimators[animatorIndex].start();
232     }
233 
234     /**
235      * Pauses all currently running animations.
236      */
237     @Override
pauseAnimations()238     public void pauseAnimations() {
239         for (int i = 0; i < ANIMATOR_COUNT; i++) {
240             if (mAnimators[i] != null) {
241                 mAnimators[i].pause();
242             }
243         }
244     }
245 
246     /**
247      * Force-ends all currently running or paused animations.
248      */
249     @Override
skipAnimationsToEnd()250     public void skipAnimationsToEnd() {
251         for (int i = 0; i < ANIMATOR_COUNT; i++) {
252             if (mAnimators[i] != null) {
253                 mAnimators[i].end();
254             }
255         }
256     }
257 
258     /**
259      * We need to override setInsets to prevent InsettableFrameLayout from applying different
260      * margins on the page indicator.
261      */
262     @Override
setInsets(Rect insets)263     public void setInsets(Rect insets) {
264     }
265 }
266