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