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