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