PagingIndicator.java revision 6cb24d438e700ceb0a8b7d5a93683b19eae6c1b5
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.AnimatorInflater;
21import android.animation.AnimatorSet;
22import android.content.Context;
23import android.content.res.Resources;
24import android.graphics.Bitmap;
25import android.graphics.BitmapFactory;
26import android.graphics.Canvas;
27import android.graphics.Color;
28import android.graphics.Matrix;
29import android.graphics.Paint;
30import android.graphics.Rect;
31import android.support.annotation.ColorInt;
32import android.support.annotation.VisibleForTesting;
33import android.support.v17.leanback.R;
34import android.util.AttributeSet;
35import android.view.View;
36
37import java.util.ArrayList;
38import java.util.List;
39
40/**
41 * A page indicator with dots.
42 * @hide
43 */
44public class PagingIndicator extends View {
45    // attribute
46    private boolean mIsLtr;
47    private final int mDotDiameter;
48    private final int mDotRadius;
49    private final int mDotGap;
50    private final int mArrowDiameter;
51    private final int mArrowRadius;
52    private final int mArrowGap;
53    private final int mShadowRadius;
54    private Dot[] mDots;
55    // X position when the dot is selected.
56    private int[] mDotSelectedX;
57    // X position when the dot is located to the left of the selected dot.
58    private int[] mDotSelectedPrevX;
59    // X position when the dot is located to the right of the selected dot.
60    private int[] mDotSelectedNextX;
61    private int mDotCenterY;
62
63    // state
64    private int mPageCount;
65    private int mCurrentPage;
66    private int mPreviousPage;
67
68    // drawing
69    @ColorInt
70    private final int mDotFgSelectColor;
71    private final Paint mBgPaint;
72    private final Paint mFgPaint;
73    private final Animator mShowAnimator;
74    private final Animator mHideAnimator;
75    private final AnimatorSet mAnimator = new AnimatorSet();
76    private Bitmap mArrow;
77    private final Rect mArrowRect;
78    private final float mArrowToBgRatio;
79
80    public PagingIndicator(Context context) {
81        this(context, null, 0);
82    }
83
84    public PagingIndicator(Context context, AttributeSet attrs) {
85        this(context, attrs, 0);
86    }
87
88    public PagingIndicator(Context context, AttributeSet attrs, int defStyle) {
89        super(context, attrs, defStyle);
90        Resources res = getResources();
91        mIsLtr = res.getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_LTR;
92        mDotRadius = res.getDimensionPixelSize(R.dimen.lb_page_indicator_dot_radius);
93        mDotDiameter = mDotRadius * 2;
94        mDotGap = res.getDimensionPixelSize(R.dimen.lb_page_indicator_dot_gap);
95        mArrowGap = res.getDimensionPixelSize(R.dimen.lb_page_indicator_arrow_gap);
96        mArrowDiameter = res.getDimensionPixelSize(R.dimen.lb_page_indicator_arrow_diameter);
97        mArrowRadius = mArrowDiameter / 2;
98        mBgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
99        mDotFgSelectColor = res.getColor(R.color.lb_page_indicator_arrow_background);
100        int bgColor = res.getColor(R.color.lb_page_indicator_dot);
101        int shadowColor = res.getColor(R.color.lb_page_indicator_arrow_shadow);
102        mBgPaint.setColor(bgColor);
103        mShadowRadius = res.getDimensionPixelSize(R.dimen.lb_page_indicator_arrow_shadow_radius);
104        mFgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
105        int shadowOffset = res.getDimensionPixelSize(R.dimen.lb_page_indicator_arrow_shadow_offset);
106        mFgPaint.setShadowLayer(mShadowRadius, shadowOffset, shadowOffset, shadowColor);
107        mArrow = loadArrow();
108        mArrowRect = new Rect(0, 0, mArrow.getWidth(), mArrow.getHeight());
109        mArrowToBgRatio = (float) mArrow.getWidth() / (float) mArrowDiameter;
110        // Initialize animations.
111        List<Animator> animators = new ArrayList<>();
112        mShowAnimator = AnimatorInflater.loadAnimator(context,
113                R.animator.lb_page_indicator_dot_show);
114        mHideAnimator = AnimatorInflater.loadAnimator(context,
115                R.animator.lb_page_indicator_dot_hide);
116        animators.add(mShowAnimator);
117        animators.add(mHideAnimator);
118        mAnimator.playTogether(animators);
119        // Use software layer to show shadows.
120        setLayerType(View.LAYER_TYPE_SOFTWARE, null);
121    }
122
123    private Bitmap loadArrow() {
124        Bitmap arrow = BitmapFactory.decodeResource(getResources(), R.drawable.lb_ic_nav_arrow);
125        if (mIsLtr) {
126            return arrow;
127        } else {
128            Matrix matrix = new Matrix();
129            matrix.preScale(-1, 1);
130            return Bitmap.createBitmap(arrow, 0, 0, arrow.getWidth(), arrow.getHeight(), matrix,
131                    false);
132        }
133    }
134
135    /**
136     * Sets the page count.
137     */
138    public void setPageCount(int pages) {
139        if (pages <= 0) {
140            throw new IllegalArgumentException("The page count should be a positive integer");
141        }
142        mPageCount = pages;
143        mDots = new Dot[mPageCount];
144        for (int i = 0; i < mPageCount; ++i) {
145            mDots[i] = new Dot();
146        }
147        calculateDotPositions();
148        setSelectedPage(0);
149    }
150
151    /**
152     * Called when the page has been selected.
153     */
154    public void onPageSelected(int pageIndex, boolean withAnimation) {
155        if (mCurrentPage == pageIndex) {
156            return;
157        }
158        if (mAnimator.isStarted()) {
159            mAnimator.end();
160        }
161        mPreviousPage = mCurrentPage;
162        if (withAnimation) {
163            mHideAnimator.setTarget(mDots[mPreviousPage]);
164            mShowAnimator.setTarget(mDots[pageIndex]);
165            mAnimator.start();
166        }
167        setSelectedPage(pageIndex);
168    }
169
170    private void calculateDotPositions() {
171        int left = getPaddingLeft();
172        int top = getPaddingTop();
173        int right = getWidth() - getPaddingRight();
174        int requiredWidth = getRequiredWidth();
175        int mid = (left + right) / 2;
176        mDotSelectedX = new int[mPageCount];
177        mDotSelectedPrevX = new int[mPageCount];
178        mDotSelectedNextX = new int[mPageCount];
179        if (mIsLtr) {
180            int startLeft = mid - requiredWidth / 2;
181            // mDotSelectedX[0] should be mDotSelectedPrevX[-1] + mArrowGap
182            mDotSelectedX[0] = startLeft + mDotRadius - mDotGap + mArrowGap;
183            mDotSelectedPrevX[0] = startLeft + mDotRadius;
184            mDotSelectedNextX[0] = startLeft + mDotRadius - 2 * mDotGap + 2 * mArrowGap;
185            for (int i = 1; i < mPageCount; i++) {
186                mDotSelectedX[i] = mDotSelectedPrevX[i - 1] + mArrowGap;
187                mDotSelectedPrevX[i] = mDotSelectedPrevX[i - 1] + mDotGap;
188                mDotSelectedNextX[i] = mDotSelectedX[i - 1] + mArrowGap;
189            }
190        } else {
191            int startRight = mid + requiredWidth / 2;
192            // mDotSelectedX[0] should be mDotSelectedPrevX[-1] - mArrowGap
193            mDotSelectedX[0] = startRight - mDotRadius + mDotGap - mArrowGap;
194            mDotSelectedPrevX[0] = startRight - mDotRadius;
195            mDotSelectedNextX[0] = startRight - mDotRadius + 2 * mDotGap - 2 * mArrowGap;
196            for (int i = 1; i < mPageCount; i++) {
197                mDotSelectedX[i] = mDotSelectedPrevX[i - 1] - mArrowGap;
198                mDotSelectedPrevX[i] = mDotSelectedPrevX[i - 1] - mDotGap;
199                mDotSelectedNextX[i] = mDotSelectedX[i - 1] - mArrowGap;
200            }
201        }
202        mDotCenterY = top + mArrowRadius;
203        adjustDotPosition();
204    }
205
206    @VisibleForTesting
207    int getPageCount() {
208        return mPageCount;
209    }
210
211    @VisibleForTesting
212    int[] getDotSelectedX() {
213        return mDotSelectedX;
214    }
215
216    @VisibleForTesting
217    int[] getDotSelectedLeftX() {
218        return mDotSelectedPrevX;
219    }
220
221    @VisibleForTesting
222    int[] getDotSelectedRightX() {
223        return mDotSelectedNextX;
224    }
225
226    @Override
227    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
228        int desiredHeight = getDesiredHeight();
229        int height;
230        switch (MeasureSpec.getMode(heightMeasureSpec)) {
231            case MeasureSpec.EXACTLY:
232                height = MeasureSpec.getSize(heightMeasureSpec);
233                break;
234            case MeasureSpec.AT_MOST:
235                height = Math.min(desiredHeight, MeasureSpec.getSize(heightMeasureSpec));
236                break;
237            case MeasureSpec.UNSPECIFIED:
238            default:
239                height = desiredHeight;
240                break;
241        }
242        int desiredWidth = getDesiredWidth();
243        int width;
244        switch (MeasureSpec.getMode(widthMeasureSpec)) {
245            case MeasureSpec.EXACTLY:
246                width = MeasureSpec.getSize(widthMeasureSpec);
247                break;
248            case MeasureSpec.AT_MOST:
249                width = Math.min(desiredWidth, MeasureSpec.getSize(widthMeasureSpec));
250                break;
251            case MeasureSpec.UNSPECIFIED:
252            default:
253                width = desiredWidth;
254                break;
255        }
256        setMeasuredDimension(width, height);
257    }
258
259    @Override
260    protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
261        setMeasuredDimension(width, height);
262        calculateDotPositions();
263    }
264
265    private int getDesiredHeight() {
266        return getPaddingTop() + mArrowDiameter + getPaddingBottom() + mShadowRadius;
267    }
268
269    private int getRequiredWidth() {
270        return 2 * mDotRadius + 2 * mArrowGap + (mPageCount - 3) * mDotGap;
271    }
272
273    private int getDesiredWidth() {
274        return getPaddingLeft() + getRequiredWidth() + getPaddingRight();
275    }
276
277    @Override
278    protected void onDraw(Canvas canvas) {
279        for (int i = 0; i < mPageCount; ++i) {
280            mDots[i].draw(canvas);
281        }
282    }
283
284    private void setSelectedPage(int now) {
285        if (now == mCurrentPage) {
286            return;
287        }
288
289        mCurrentPage = now;
290        adjustDotPosition();
291    }
292
293    private void adjustDotPosition() {
294        for (int i = 0; i < mCurrentPage; ++i) {
295            mDots[i].deselect();
296            mDots[i].mDirection = i == mPreviousPage ? Dot.LEFT : Dot.RIGHT;
297            mDots[i].mCenterX = mDotSelectedPrevX[i];
298        }
299        mDots[mCurrentPage].select();
300        mDots[mCurrentPage].mDirection = mPreviousPage < mCurrentPage ? Dot.LEFT : Dot.RIGHT;
301        mDots[mCurrentPage].mCenterX = mDotSelectedX[mCurrentPage];
302        for (int i = mCurrentPage + 1; i < mPageCount; ++i) {
303            mDots[i].deselect();
304            mDots[i].mDirection = Dot.RIGHT;
305            mDots[i].mCenterX = mDotSelectedNextX[i];
306        }
307    }
308
309    @Override
310    public void onRtlPropertiesChanged(int layoutDirection) {
311        super.onRtlPropertiesChanged(layoutDirection);
312        boolean isLtr = layoutDirection == View.LAYOUT_DIRECTION_LTR;
313        if (mIsLtr != isLtr) {
314            mIsLtr = isLtr;
315            mArrow = loadArrow();
316            if (mDots != null) {
317                for (Dot dot : mDots) {
318                    dot.onRtlPropertiesChanged();
319                }
320            }
321            calculateDotPositions();
322            invalidate();
323        }
324    }
325
326    public class Dot {
327        static final float LEFT = -1;
328        static final float RIGHT = 1;
329        static final float LTR = 1;
330        static final float RTL = -1;
331
332        float mAlpha;
333        @ColorInt
334        int mFgColor;
335        float mTranslationX;
336        float mCenterX;
337        float mDiameter;
338        float mRadius;
339        float mArrowImageRadius;
340        float mDirection = RIGHT;
341        float mLayoutDirection = mIsLtr ? LTR : RTL;
342
343        void select() {
344            mTranslationX = 0.0f;
345            mCenterX = 0.0f;
346            mDiameter = mArrowDiameter;
347            mRadius = mArrowRadius;
348            mArrowImageRadius = mRadius * mArrowToBgRatio;
349            mAlpha = 1.0f;
350            adjustAlpha();
351        }
352
353        void deselect() {
354            mTranslationX = 0.0f;
355            mCenterX = 0.0f;
356            mDiameter = mDotDiameter;
357            mRadius = mDotRadius;
358            mArrowImageRadius = mRadius * mArrowToBgRatio;
359            mAlpha = 0.0f;
360            adjustAlpha();
361        }
362
363        public void adjustAlpha() {
364            int alpha = Math.round(0xFF * mAlpha);
365            int red = Color.red(mDotFgSelectColor);
366            int green = Color.green(mDotFgSelectColor);
367            int blue = Color.blue(mDotFgSelectColor);
368            mFgColor = Color.argb(alpha, red, green, blue);
369        }
370
371        public float getAlpha() {
372            return mAlpha;
373        }
374
375        public void setAlpha(float alpha) {
376            this.mAlpha = alpha;
377            adjustAlpha();
378            invalidate();
379        }
380
381        public float getTranslationX() {
382            return mTranslationX;
383        }
384
385        public void setTranslationX(float translationX) {
386            this.mTranslationX = translationX * mDirection * mLayoutDirection;
387            invalidate();
388        }
389
390        public float getDiameter() {
391            return mDiameter;
392        }
393
394        public void setDiameter(float diameter) {
395            this.mDiameter = diameter;
396            this.mRadius = diameter / 2;
397            this.mArrowImageRadius = diameter / 2 * mArrowToBgRatio;
398            invalidate();
399        }
400
401        void draw(Canvas canvas) {
402            float centerX = mCenterX + mTranslationX;
403            canvas.drawCircle(centerX, mDotCenterY, mRadius, mBgPaint);
404            if (mAlpha > 0) {
405                mFgPaint.setColor(mFgColor);
406                canvas.drawCircle(centerX, mDotCenterY, mRadius, mFgPaint);
407                canvas.drawBitmap(mArrow, mArrowRect, new Rect((int) (centerX - mArrowImageRadius),
408                        (int) (mDotCenterY - mArrowImageRadius),
409                        (int) (centerX + mArrowImageRadius),
410                        (int) (mDotCenterY + mArrowImageRadius)), null);
411            }
412        }
413
414        void onRtlPropertiesChanged() {
415            mLayoutDirection = mIsLtr ? LTR : RTL;
416        }
417    }
418}
419