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