Home | History | Annotate | Download | only in widget
      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 
    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