PagingIndicator.java revision 6a8b0fd35afda4d204e3f46d192bf20f510185d9
1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.support.v17.leanback.widget;
18
19import android.animation.Animator;
20import android.animation.AnimatorSet;
21import android.animation.ObjectAnimator;
22import android.animation.TimeInterpolator;
23import android.content.Context;
24import android.content.res.Resources;
25import android.content.res.TypedArray;
26import android.graphics.Bitmap;
27import android.graphics.BitmapFactory;
28import android.graphics.Canvas;
29import android.graphics.Color;
30import android.graphics.Matrix;
31import android.graphics.Paint;
32import android.graphics.Rect;
33import android.support.annotation.ColorInt;
34import android.support.annotation.VisibleForTesting;
35import android.support.v17.leanback.R;
36import android.util.AttributeSet;
37import android.util.Property;
38import android.view.View;
39import android.view.animation.DecelerateInterpolator;
40
41/**
42 * A page indicator with dots.
43 * @hide
44 */
45public class PagingIndicator extends View {
46    private static final long DURATION_ALPHA = 167;
47    private static final long DURATION_DIAMETER = 417;
48    private static final long DURATION_TRANSLATION_X = DURATION_DIAMETER;
49    private static final TimeInterpolator DECELERATE_INTERPOLATOR = new DecelerateInterpolator();
50
51    private static final Property<Dot, Float> DOT_ALPHA
52            = new Property<Dot, Float>(Float.class, "alpha") {
53        @Override
54        public Float get(Dot dot) {
55            return dot.getAlpha();
56        }
57
58        @Override
59        public void set(Dot dot, Float value) {
60            dot.setAlpha(value);
61        }
62    };
63
64    private static final Property<Dot, Float> DOT_DIAMETER
65            = new Property<Dot, Float>(Float.class, "diameter") {
66        @Override
67        public Float get(Dot dot) {
68            return dot.getDiameter();
69        }
70
71        @Override
72        public void set(Dot dot, Float value) {
73            dot.setDiameter(value);
74        }
75    };
76
77    private static final Property<Dot, Float> DOT_TRANSLATION_X
78            = new Property<Dot, Float>(Float.class, "translation_x") {
79        @Override
80        public Float get(Dot dot) {
81            return dot.getTranslationX();
82        }
83
84        @Override
85        public void set(Dot dot, Float value) {
86            dot.setTranslationX(value);
87        }
88    };
89
90    // attribute
91    private boolean mIsLtr;
92    private final int mDotDiameter;
93    private final int mDotRadius;
94    private final int mDotGap;
95    private final int mArrowDiameter;
96    private final int mArrowRadius;
97    private final int mArrowGap;
98    private final int mShadowRadius;
99    private Dot[] mDots;
100    // X position when the dot is selected.
101    private int[] mDotSelectedX;
102    // X position when the dot is located to the left of the selected dot.
103    private int[] mDotSelectedPrevX;
104    // X position when the dot is located to the right of the selected dot.
105    private int[] mDotSelectedNextX;
106    private int mDotCenterY;
107
108    // state
109    private int mPageCount;
110    private int mCurrentPage;
111    private int mPreviousPage;
112
113    // drawing
114    @ColorInt
115    private final int mDotFgSelectColor;
116    private final Paint mBgPaint;
117    private final Paint mFgPaint;
118    private final AnimatorSet mShowAnimator;
119    private final AnimatorSet mHideAnimator;
120    private final AnimatorSet mAnimator = new AnimatorSet();
121    private Bitmap mArrow;
122    private final Rect mArrowRect;
123    private final float mArrowToBgRatio;
124
125    public PagingIndicator(Context context) {
126        this(context, null, 0);
127    }
128
129    public PagingIndicator(Context context, AttributeSet attrs) {
130        this(context, attrs, 0);
131    }
132
133    public PagingIndicator(Context context, AttributeSet attrs, int defStyle) {
134        super(context, attrs, defStyle);
135        Resources res = getResources();
136        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.PagingIndicator,
137                defStyle, 0);
138        mDotRadius = getDimensionFromTypedArray(typedArray, R.styleable.PagingIndicator_dotRadius,
139                R.dimen.lb_page_indicator_dot_radius);
140        mDotDiameter = mDotRadius * 2;
141        mArrowRadius = getDimensionFromTypedArray(typedArray,
142                R.styleable.PagingIndicator_arrowRadius, R.dimen.lb_page_indicator_arrow_radius);
143        mArrowDiameter = mArrowRadius * 2;
144        mDotGap = getDimensionFromTypedArray(typedArray, R.styleable.PagingIndicator_dotToDotGap,
145                R.dimen.lb_page_indicator_dot_gap);
146        mArrowGap = getDimensionFromTypedArray(typedArray,
147                R.styleable.PagingIndicator_dotToArrowGap, R.dimen.lb_page_indicator_arrow_gap);
148        int bgColor = getColorFromTypedArray(typedArray, R.styleable.PagingIndicator_dotBgColor,
149                R.color.lb_page_indicator_dot);
150        mBgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
151        mBgPaint.setColor(bgColor);
152        mDotFgSelectColor = getColorFromTypedArray(typedArray,
153                R.styleable.PagingIndicator_arrowBgColor,
154                R.color.lb_page_indicator_arrow_background);
155        typedArray.recycle();
156        mIsLtr = res.getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_LTR;
157        int shadowColor = res.getColor(R.color.lb_page_indicator_arrow_shadow);
158        mShadowRadius = res.getDimensionPixelSize(R.dimen.lb_page_indicator_arrow_shadow_radius);
159        mFgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
160        int shadowOffset = res.getDimensionPixelSize(R.dimen.lb_page_indicator_arrow_shadow_offset);
161        mFgPaint.setShadowLayer(mShadowRadius, shadowOffset, shadowOffset, shadowColor);
162        mArrow = loadArrow();
163        mArrowRect = new Rect(0, 0, mArrow.getWidth(), mArrow.getHeight());
164        mArrowToBgRatio = (float) mArrow.getWidth() / (float) mArrowDiameter;
165        // Initialize animations.
166        mShowAnimator = new AnimatorSet();
167        mShowAnimator.playTogether(createDotAlphaAnimator(0.0f, 1.0f),
168                createDotDiameterAnimator(mDotRadius * 2, mArrowRadius * 2),
169                createDotTranslationXAnimator());
170        mHideAnimator = new AnimatorSet();
171        mHideAnimator.playTogether(createDotAlphaAnimator(1.0f, 0.0f),
172                createDotDiameterAnimator(mArrowRadius * 2, mDotRadius * 2),
173                createDotTranslationXAnimator());
174        mAnimator.playTogether(mShowAnimator, mHideAnimator);
175        // Use software layer to show shadows.
176        setLayerType(View.LAYER_TYPE_SOFTWARE, null);
177    }
178
179    private int getDimensionFromTypedArray(TypedArray typedArray, int attr, int defaultId) {
180        return typedArray.getDimensionPixelOffset(attr,
181                getResources().getDimensionPixelOffset(defaultId));
182    }
183
184    private int getColorFromTypedArray(TypedArray typedArray, int attr, int defaultId) {
185        return typedArray.getColor(attr, getResources().getColor(defaultId));
186    }
187
188    private Bitmap loadArrow() {
189        Bitmap arrow = BitmapFactory.decodeResource(getResources(), R.drawable.lb_ic_nav_arrow);
190        if (mIsLtr) {
191            return arrow;
192        } else {
193            Matrix matrix = new Matrix();
194            matrix.preScale(-1, 1);
195            return Bitmap.createBitmap(arrow, 0, 0, arrow.getWidth(), arrow.getHeight(), matrix,
196                    false);
197        }
198    }
199
200    private Animator createDotAlphaAnimator(float from, float to) {
201        ObjectAnimator animator = ObjectAnimator.ofFloat(null, DOT_ALPHA, from, to);
202        animator.setDuration(DURATION_ALPHA);
203        animator.setInterpolator(DECELERATE_INTERPOLATOR);
204        return animator;
205    }
206
207    private Animator createDotDiameterAnimator(float from, float to) {
208        ObjectAnimator animator = ObjectAnimator.ofFloat(null, DOT_DIAMETER, from, to);
209        animator.setDuration(DURATION_DIAMETER);
210        animator.setInterpolator(DECELERATE_INTERPOLATOR);
211        return animator;
212    }
213
214    private Animator createDotTranslationXAnimator() {
215        // The direction is determined in the Dot.
216        ObjectAnimator animator = ObjectAnimator.ofFloat(null, DOT_TRANSLATION_X,
217                -mArrowGap + mDotGap, 0.0f);
218        animator.setDuration(DURATION_TRANSLATION_X);
219        animator.setInterpolator(DECELERATE_INTERPOLATOR);
220        return animator;
221    }
222
223    /**
224     * Sets the page count.
225     */
226    public void setPageCount(int pages) {
227        if (pages <= 0) {
228            throw new IllegalArgumentException("The page count should be a positive integer");
229        }
230        mPageCount = pages;
231        mDots = new Dot[mPageCount];
232        for (int i = 0; i < mPageCount; ++i) {
233            mDots[i] = new Dot();
234        }
235        calculateDotPositions();
236        setSelectedPage(0);
237    }
238
239    /**
240     * Called when the page has been selected.
241     */
242    public void onPageSelected(int pageIndex, boolean withAnimation) {
243        if (mCurrentPage == pageIndex) {
244            return;
245        }
246        if (mAnimator.isStarted()) {
247            mAnimator.end();
248        }
249        mPreviousPage = mCurrentPage;
250        if (withAnimation) {
251            mHideAnimator.setTarget(mDots[mPreviousPage]);
252            mShowAnimator.setTarget(mDots[pageIndex]);
253            mAnimator.start();
254        }
255        setSelectedPage(pageIndex);
256    }
257
258    private void calculateDotPositions() {
259        int left = getPaddingLeft();
260        int top = getPaddingTop();
261        int right = getWidth() - getPaddingRight();
262        int requiredWidth = getRequiredWidth();
263        int mid = (left + right) / 2;
264        mDotSelectedX = new int[mPageCount];
265        mDotSelectedPrevX = new int[mPageCount];
266        mDotSelectedNextX = new int[mPageCount];
267        if (mIsLtr) {
268            int startLeft = mid - requiredWidth / 2;
269            // mDotSelectedX[0] should be mDotSelectedPrevX[-1] + mArrowGap
270            mDotSelectedX[0] = startLeft + mDotRadius - mDotGap + mArrowGap;
271            mDotSelectedPrevX[0] = startLeft + mDotRadius;
272            mDotSelectedNextX[0] = startLeft + mDotRadius - 2 * mDotGap + 2 * mArrowGap;
273            for (int i = 1; i < mPageCount; i++) {
274                mDotSelectedX[i] = mDotSelectedPrevX[i - 1] + mArrowGap;
275                mDotSelectedPrevX[i] = mDotSelectedPrevX[i - 1] + mDotGap;
276                mDotSelectedNextX[i] = mDotSelectedX[i - 1] + mArrowGap;
277            }
278        } else {
279            int startRight = mid + requiredWidth / 2;
280            // mDotSelectedX[0] should be mDotSelectedPrevX[-1] - mArrowGap
281            mDotSelectedX[0] = startRight - mDotRadius + mDotGap - mArrowGap;
282            mDotSelectedPrevX[0] = startRight - mDotRadius;
283            mDotSelectedNextX[0] = startRight - mDotRadius + 2 * mDotGap - 2 * mArrowGap;
284            for (int i = 1; i < mPageCount; i++) {
285                mDotSelectedX[i] = mDotSelectedPrevX[i - 1] - mArrowGap;
286                mDotSelectedPrevX[i] = mDotSelectedPrevX[i - 1] - mDotGap;
287                mDotSelectedNextX[i] = mDotSelectedX[i - 1] - mArrowGap;
288            }
289        }
290        mDotCenterY = top + mArrowRadius;
291        adjustDotPosition();
292    }
293
294    @VisibleForTesting
295    int getPageCount() {
296        return mPageCount;
297    }
298
299    @VisibleForTesting
300    int[] getDotSelectedX() {
301        return mDotSelectedX;
302    }
303
304    @VisibleForTesting
305    int[] getDotSelectedLeftX() {
306        return mDotSelectedPrevX;
307    }
308
309    @VisibleForTesting
310    int[] getDotSelectedRightX() {
311        return mDotSelectedNextX;
312    }
313
314    @Override
315    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
316        int desiredHeight = getDesiredHeight();
317        int height;
318        switch (MeasureSpec.getMode(heightMeasureSpec)) {
319            case MeasureSpec.EXACTLY:
320                height = MeasureSpec.getSize(heightMeasureSpec);
321                break;
322            case MeasureSpec.AT_MOST:
323                height = Math.min(desiredHeight, MeasureSpec.getSize(heightMeasureSpec));
324                break;
325            case MeasureSpec.UNSPECIFIED:
326            default:
327                height = desiredHeight;
328                break;
329        }
330        int desiredWidth = getDesiredWidth();
331        int width;
332        switch (MeasureSpec.getMode(widthMeasureSpec)) {
333            case MeasureSpec.EXACTLY:
334                width = MeasureSpec.getSize(widthMeasureSpec);
335                break;
336            case MeasureSpec.AT_MOST:
337                width = Math.min(desiredWidth, MeasureSpec.getSize(widthMeasureSpec));
338                break;
339            case MeasureSpec.UNSPECIFIED:
340            default:
341                width = desiredWidth;
342                break;
343        }
344        setMeasuredDimension(width, height);
345    }
346
347    @Override
348    protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
349        setMeasuredDimension(width, height);
350        calculateDotPositions();
351    }
352
353    private int getDesiredHeight() {
354        return getPaddingTop() + mArrowDiameter + getPaddingBottom() + mShadowRadius;
355    }
356
357    private int getRequiredWidth() {
358        return 2 * mDotRadius + 2 * mArrowGap + (mPageCount - 3) * mDotGap;
359    }
360
361    private int getDesiredWidth() {
362        return getPaddingLeft() + getRequiredWidth() + getPaddingRight();
363    }
364
365    @Override
366    protected void onDraw(Canvas canvas) {
367        for (int i = 0; i < mPageCount; ++i) {
368            mDots[i].draw(canvas);
369        }
370    }
371
372    private void setSelectedPage(int now) {
373        if (now == mCurrentPage) {
374            return;
375        }
376
377        mCurrentPage = now;
378        adjustDotPosition();
379    }
380
381    private void adjustDotPosition() {
382        for (int i = 0; i < mCurrentPage; ++i) {
383            mDots[i].deselect();
384            mDots[i].mDirection = i == mPreviousPage ? Dot.LEFT : Dot.RIGHT;
385            mDots[i].mCenterX = mDotSelectedPrevX[i];
386        }
387        mDots[mCurrentPage].select();
388        mDots[mCurrentPage].mDirection = mPreviousPage < mCurrentPage ? Dot.LEFT : Dot.RIGHT;
389        mDots[mCurrentPage].mCenterX = mDotSelectedX[mCurrentPage];
390        for (int i = mCurrentPage + 1; i < mPageCount; ++i) {
391            mDots[i].deselect();
392            mDots[i].mDirection = Dot.RIGHT;
393            mDots[i].mCenterX = mDotSelectedNextX[i];
394        }
395    }
396
397    @Override
398    public void onRtlPropertiesChanged(int layoutDirection) {
399        super.onRtlPropertiesChanged(layoutDirection);
400        boolean isLtr = layoutDirection == View.LAYOUT_DIRECTION_LTR;
401        if (mIsLtr != isLtr) {
402            mIsLtr = isLtr;
403            mArrow = loadArrow();
404            if (mDots != null) {
405                for (Dot dot : mDots) {
406                    dot.onRtlPropertiesChanged();
407                }
408            }
409            calculateDotPositions();
410            invalidate();
411        }
412    }
413
414    public class Dot {
415        static final float LEFT = -1;
416        static final float RIGHT = 1;
417        static final float LTR = 1;
418        static final float RTL = -1;
419
420        float mAlpha;
421        @ColorInt
422        int mFgColor;
423        float mTranslationX;
424        float mCenterX;
425        float mDiameter;
426        float mRadius;
427        float mArrowImageRadius;
428        float mDirection = RIGHT;
429        float mLayoutDirection = mIsLtr ? LTR : RTL;
430
431        void select() {
432            mTranslationX = 0.0f;
433            mCenterX = 0.0f;
434            mDiameter = mArrowDiameter;
435            mRadius = mArrowRadius;
436            mArrowImageRadius = mRadius * mArrowToBgRatio;
437            mAlpha = 1.0f;
438            adjustAlpha();
439        }
440
441        void deselect() {
442            mTranslationX = 0.0f;
443            mCenterX = 0.0f;
444            mDiameter = mDotDiameter;
445            mRadius = mDotRadius;
446            mArrowImageRadius = mRadius * mArrowToBgRatio;
447            mAlpha = 0.0f;
448            adjustAlpha();
449        }
450
451        public void adjustAlpha() {
452            int alpha = Math.round(0xFF * mAlpha);
453            int red = Color.red(mDotFgSelectColor);
454            int green = Color.green(mDotFgSelectColor);
455            int blue = Color.blue(mDotFgSelectColor);
456            mFgColor = Color.argb(alpha, red, green, blue);
457        }
458
459        public float getAlpha() {
460            return mAlpha;
461        }
462
463        public void setAlpha(float alpha) {
464            this.mAlpha = alpha;
465            adjustAlpha();
466            invalidate();
467        }
468
469        public float getTranslationX() {
470            return mTranslationX;
471        }
472
473        public void setTranslationX(float translationX) {
474            this.mTranslationX = translationX * mDirection * mLayoutDirection;
475            invalidate();
476        }
477
478        public float getDiameter() {
479            return mDiameter;
480        }
481
482        public void setDiameter(float diameter) {
483            this.mDiameter = diameter;
484            this.mRadius = diameter / 2;
485            this.mArrowImageRadius = diameter / 2 * mArrowToBgRatio;
486            invalidate();
487        }
488
489        void draw(Canvas canvas) {
490            float centerX = mCenterX + mTranslationX;
491            canvas.drawCircle(centerX, mDotCenterY, mRadius, mBgPaint);
492            if (mAlpha > 0) {
493                mFgPaint.setColor(mFgColor);
494                canvas.drawCircle(centerX, mDotCenterY, mRadius, mFgPaint);
495                canvas.drawBitmap(mArrow, mArrowRect, new Rect((int) (centerX - mArrowImageRadius),
496                        (int) (mDotCenterY - mArrowImageRadius),
497                        (int) (centerX + mArrowImageRadius),
498                        (int) (mDotCenterY + mArrowImageRadius)), null);
499            }
500        }
501
502        void onRtlPropertiesChanged() {
503            mLayoutDirection = mIsLtr ? LTR : RTL;
504        }
505    }
506}
507