• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package android.support.v17.leanback.widget;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorSet;
21 import android.animation.ObjectAnimator;
22 import android.animation.TimeInterpolator;
23 import android.content.Context;
24 import android.content.res.Resources;
25 import android.content.res.TypedArray;
26 import android.graphics.Bitmap;
27 import android.graphics.BitmapFactory;
28 import android.graphics.Canvas;
29 import android.graphics.Color;
30 import android.graphics.Matrix;
31 import android.graphics.Paint;
32 import android.graphics.Rect;
33 import android.support.annotation.ColorInt;
34 import android.support.annotation.VisibleForTesting;
35 import android.support.v17.leanback.R;
36 import android.util.AttributeSet;
37 import android.util.Property;
38 import android.view.View;
39 import android.view.animation.DecelerateInterpolator;
40 
41 /**
42  * A page indicator with dots.
43  * @hide
44  */
45 public 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 
PagingIndicator(Context context)125     public PagingIndicator(Context context) {
126         this(context, null, 0);
127     }
128 
PagingIndicator(Context context, AttributeSet attrs)129     public PagingIndicator(Context context, AttributeSet attrs) {
130         this(context, attrs, 0);
131     }
132 
PagingIndicator(Context context, AttributeSet attrs, int defStyle)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 
getDimensionFromTypedArray(TypedArray typedArray, int attr, int defaultId)179     private int getDimensionFromTypedArray(TypedArray typedArray, int attr, int defaultId) {
180         return typedArray.getDimensionPixelOffset(attr,
181                 getResources().getDimensionPixelOffset(defaultId));
182     }
183 
getColorFromTypedArray(TypedArray typedArray, int attr, int defaultId)184     private int getColorFromTypedArray(TypedArray typedArray, int attr, int defaultId) {
185         return typedArray.getColor(attr, getResources().getColor(defaultId));
186     }
187 
loadArrow()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 
createDotAlphaAnimator(float from, float to)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 
createDotDiameterAnimator(float from, float to)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 
createDotTranslationXAnimator()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      */
setPageCount(int pages)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      */
onPageSelected(int pageIndex, boolean withAnimation)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 
calculateDotPositions()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
getPageCount()295     int getPageCount() {
296         return mPageCount;
297     }
298 
299     @VisibleForTesting
getDotSelectedX()300     int[] getDotSelectedX() {
301         return mDotSelectedX;
302     }
303 
304     @VisibleForTesting
getDotSelectedLeftX()305     int[] getDotSelectedLeftX() {
306         return mDotSelectedPrevX;
307     }
308 
309     @VisibleForTesting
getDotSelectedRightX()310     int[] getDotSelectedRightX() {
311         return mDotSelectedNextX;
312     }
313 
314     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)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
onSizeChanged(int width, int height, int oldWidth, int oldHeight)348     protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
349         setMeasuredDimension(width, height);
350         calculateDotPositions();
351     }
352 
getDesiredHeight()353     private int getDesiredHeight() {
354         return getPaddingTop() + mArrowDiameter + getPaddingBottom() + mShadowRadius;
355     }
356 
getRequiredWidth()357     private int getRequiredWidth() {
358         return 2 * mDotRadius + 2 * mArrowGap + (mPageCount - 3) * mDotGap;
359     }
360 
getDesiredWidth()361     private int getDesiredWidth() {
362         return getPaddingLeft() + getRequiredWidth() + getPaddingRight();
363     }
364 
365     @Override
onDraw(Canvas canvas)366     protected void onDraw(Canvas canvas) {
367         for (int i = 0; i < mPageCount; ++i) {
368             mDots[i].draw(canvas);
369         }
370     }
371 
setSelectedPage(int now)372     private void setSelectedPage(int now) {
373         if (now == mCurrentPage) {
374             return;
375         }
376 
377         mCurrentPage = now;
378         adjustDotPosition();
379     }
380 
adjustDotPosition()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