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.os.Handler;
13 import android.os.Looper;
14 import android.support.v4.graphics.ColorUtils;
15 import android.util.AttributeSet;
16 import android.util.Log;
17 import android.util.Property;
18 import android.view.ViewConfiguration;
19 import android.widget.ImageView;
20 
21 import com.android.launcher3.Launcher;
22 import com.android.launcher3.R;
23 import com.android.launcher3.Utilities;
24 import com.android.launcher3.dynamicui.ExtractedColors;
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 PageIndicatorLineCaret extends PageIndicator {
32     private static final String TAG = "PageIndicatorLine";
33 
34     private static final int[] sTempCoords = new int[2];
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 
45     private ValueAnimator[] mAnimators = new ValueAnimator[3];
46 
47     private final Handler mDelayedLineFadeHandler = new Handler(Looper.getMainLooper());
48 
49     private boolean mShouldAutoHide = true;
50 
51     // The alpha of the line when it is showing.
52     private int mActiveAlpha = 0;
53     // The alpha that the line is being animated to or already at (either 0 or mActiveAlpha).
54     private int mToAlpha;
55     // A float value representing the number of pages, to allow for an animation when it changes.
56     private float mNumPagesFloat;
57     private int mCurrentScroll;
58     private int mTotalScroll;
59     private Paint mLinePaint;
60     private Launcher mLauncher;
61     private final int mLineHeight;
62     private ImageView mAllAppsHandle;
63 
64     private static final Property<PageIndicatorLineCaret, Integer> PAINT_ALPHA
65             = new Property<PageIndicatorLineCaret, Integer>(Integer.class, "paint_alpha") {
66         @Override
67         public Integer get(PageIndicatorLineCaret obj) {
68             return obj.mLinePaint.getAlpha();
69         }
70 
71         @Override
72         public void set(PageIndicatorLineCaret obj, Integer alpha) {
73             obj.mLinePaint.setAlpha(alpha);
74             obj.invalidate();
75         }
76     };
77 
78     private static final Property<PageIndicatorLineCaret, Float> NUM_PAGES
79             = new Property<PageIndicatorLineCaret, Float>(Float.class, "num_pages") {
80         @Override
81         public Float get(PageIndicatorLineCaret obj) {
82             return obj.mNumPagesFloat;
83         }
84 
85         @Override
86         public void set(PageIndicatorLineCaret obj, Float numPages) {
87             obj.mNumPagesFloat = numPages;
88             obj.invalidate();
89         }
90     };
91 
92     private static final Property<PageIndicatorLineCaret, Integer> TOTAL_SCROLL
93             = new Property<PageIndicatorLineCaret, Integer>(Integer.class, "total_scroll") {
94         @Override
95         public Integer get(PageIndicatorLineCaret obj) {
96             return obj.mTotalScroll;
97         }
98 
99         @Override
100         public void set(PageIndicatorLineCaret obj, Integer totalScroll) {
101             obj.mTotalScroll = totalScroll;
102             obj.invalidate();
103         }
104     };
105 
106     private Runnable mHideLineRunnable = new Runnable() {
107         @Override
108         public void run() {
109             animateLineToAlpha(0);
110         }
111     };
112 
PageIndicatorLineCaret(Context context)113     public PageIndicatorLineCaret(Context context) {
114         this(context, null);
115     }
116 
PageIndicatorLineCaret(Context context, AttributeSet attrs)117     public PageIndicatorLineCaret(Context context, AttributeSet attrs) {
118         this(context, attrs, 0);
119     }
120 
PageIndicatorLineCaret(Context context, AttributeSet attrs, int defStyle)121     public PageIndicatorLineCaret(Context context, AttributeSet attrs, int defStyle) {
122         super(context, attrs, defStyle);
123 
124         Resources res = context.getResources();
125         mLinePaint = new Paint();
126         mLinePaint.setAlpha(0);
127 
128         mLauncher = Launcher.getLauncher(context);
129         mLineHeight = res.getDimensionPixelSize(R.dimen.dynamic_grid_page_indicator_line_height);
130         setCaretDrawable(new CaretDrawable(context));
131     }
132 
133     @Override
onFinishInflate()134     protected void onFinishInflate() {
135         super.onFinishInflate();
136         mAllAppsHandle = (ImageView) findViewById(R.id.all_apps_handle);
137         mAllAppsHandle.setImageDrawable(getCaretDrawable());
138         mAllAppsHandle.setOnTouchListener(mLauncher.getHapticFeedbackTouchListener());
139         mAllAppsHandle.setOnClickListener(mLauncher);
140         mAllAppsHandle.setOnLongClickListener(mLauncher);
141         mAllAppsHandle.setOnFocusChangeListener(mLauncher.mFocusHandler);
142         mLauncher.setAllAppsButton(mAllAppsHandle);
143     }
144 
145     @Override
setAccessibilityDelegate(AccessibilityDelegate delegate)146     public void setAccessibilityDelegate(AccessibilityDelegate delegate) {
147         mAllAppsHandle.setAccessibilityDelegate(delegate);
148     }
149 
150     @Override
onDraw(Canvas canvas)151     protected void onDraw(Canvas canvas) {
152         if (mTotalScroll == 0 || mNumPagesFloat == 0) {
153             return;
154         }
155 
156         // Compute and draw line rect.
157         float progress = Utilities.boundToRange(((float) mCurrentScroll) / mTotalScroll, 0f, 1f);
158         int availableWidth = canvas.getWidth();
159         int lineWidth = (int) (availableWidth / mNumPagesFloat);
160         int lineLeft = (int) (progress * (availableWidth - lineWidth));
161         int lineRight = lineLeft + lineWidth;
162         canvas.drawRect(lineLeft, canvas.getHeight() - mLineHeight, lineRight, canvas.getHeight(),
163                 mLinePaint);
164     }
165 
166     @Override
setContentDescription(CharSequence contentDescription)167     public void setContentDescription(CharSequence contentDescription) {
168         mAllAppsHandle.setContentDescription(contentDescription);
169     }
170 
171     @Override
setScroll(int currentScroll, int totalScroll)172     public void setScroll(int currentScroll, int totalScroll) {
173         if (getAlpha() == 0) {
174             return;
175         }
176         animateLineToAlpha(mActiveAlpha);
177 
178         mCurrentScroll = currentScroll;
179         if (mTotalScroll == 0) {
180             mTotalScroll = totalScroll;
181         } else if (mTotalScroll != totalScroll) {
182             animateToTotalScroll(totalScroll);
183         } else {
184             invalidate();
185         }
186 
187         if (mShouldAutoHide) {
188             hideAfterDelay();
189         }
190     }
191 
hideAfterDelay()192     private void hideAfterDelay() {
193         mDelayedLineFadeHandler.removeCallbacksAndMessages(null);
194         mDelayedLineFadeHandler.postDelayed(mHideLineRunnable, LINE_FADE_DELAY);
195     }
196 
197     @Override
setActiveMarker(int activePage)198     public void setActiveMarker(int activePage) {
199     }
200 
201     @Override
onPageCountChanged()202     protected void onPageCountChanged() {
203         if (Float.compare(mNumPages, mNumPagesFloat) != 0) {
204             animateToNumPages(mNumPages);
205         }
206     }
207 
setShouldAutoHide(boolean shouldAutoHide)208     public void setShouldAutoHide(boolean shouldAutoHide) {
209         mShouldAutoHide = shouldAutoHide;
210         if (shouldAutoHide && mLinePaint.getAlpha() > 0) {
211             hideAfterDelay();
212         } else if (!shouldAutoHide) {
213             mDelayedLineFadeHandler.removeCallbacksAndMessages(null);
214         }
215     }
216 
217     /**
218      * The line's color will be:
219      * - mostly opaque white if the hotseat is white (ignoring alpha)
220      * - mostly opaque black if the hotseat is black (ignoring alpha)
221      */
updateColor(ExtractedColors extractedColors)222     public void updateColor(ExtractedColors extractedColors) {
223         int originalLineAlpha = mLinePaint.getAlpha();
224         int color = extractedColors.getColor(ExtractedColors.HOTSEAT_INDEX, Color.TRANSPARENT);
225         if (color != Color.TRANSPARENT) {
226             color = ColorUtils.setAlphaComponent(color, 255);
227             if (color == Color.BLACK) {
228                 mActiveAlpha = BLACK_ALPHA;
229             } else if (color == Color.WHITE) {
230                 mActiveAlpha = WHITE_ALPHA;
231             } else {
232                 Log.e(TAG, "Setting workspace page indicators to an unsupported color: #"
233                         + Integer.toHexString(color));
234             }
235             mLinePaint.setColor(color);
236             mLinePaint.setAlpha(originalLineAlpha);
237         }
238     }
239 
animateLineToAlpha(int alpha)240     private void animateLineToAlpha(int alpha) {
241         if (alpha == mToAlpha) {
242             // Ignore the new animation if it is going to the same alpha as the current animation.
243             return;
244         }
245         mToAlpha = alpha;
246         setupAndRunAnimation(ObjectAnimator.ofInt(this, PAINT_ALPHA, alpha),
247                 LINE_ALPHA_ANIMATOR_INDEX);
248     }
249 
animateToNumPages(int numPages)250     private void animateToNumPages(int numPages) {
251         setupAndRunAnimation(ObjectAnimator.ofFloat(this, NUM_PAGES, numPages),
252                 NUM_PAGES_ANIMATOR_INDEX);
253     }
254 
animateToTotalScroll(int totalScroll)255     private void animateToTotalScroll(int totalScroll) {
256         setupAndRunAnimation(ObjectAnimator.ofInt(this, TOTAL_SCROLL, totalScroll),
257                 TOTAL_SCROLL_ANIMATOR_INDEX);
258     }
259 
260     /**
261      * Starts the given animator and stores it in the provided index in {@link #mAnimators} until
262      * the animation ends.
263      *
264      * If an animator is already at the index (i.e. it is already playing), it is canceled and
265      * replaced with the new animator.
266      */
setupAndRunAnimation(ValueAnimator animator, final int animatorIndex)267     private void setupAndRunAnimation(ValueAnimator animator, final int animatorIndex) {
268         if (mAnimators[animatorIndex] != null) {
269             mAnimators[animatorIndex].cancel();
270         }
271         mAnimators[animatorIndex] = animator;
272         mAnimators[animatorIndex].addListener(new AnimatorListenerAdapter() {
273             @Override
274             public void onAnimationEnd(Animator animation) {
275                 mAnimators[animatorIndex] = null;
276             }
277         });
278         mAnimators[animatorIndex].setDuration(LINE_ANIMATE_DURATION);
279         mAnimators[animatorIndex].start();
280     }
281 }
282