PagingIndicator.java revision 6a8b0fd35afda4d204e3f46d192bf20f510185d9
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.AnimatorSet; 21import android.animation.ObjectAnimator; 22import android.animation.TimeInterpolator; 23import android.content.Context; 24import android.content.res.Resources; 25import android.content.res.TypedArray; 26import android.graphics.Bitmap; 27import android.graphics.BitmapFactory; 28import android.graphics.Canvas; 29import android.graphics.Color; 30import android.graphics.Matrix; 31import android.graphics.Paint; 32import android.graphics.Rect; 33import android.support.annotation.ColorInt; 34import android.support.annotation.VisibleForTesting; 35import android.support.v17.leanback.R; 36import android.util.AttributeSet; 37import android.util.Property; 38import android.view.View; 39import android.view.animation.DecelerateInterpolator; 40 41/** 42 * A page indicator with dots. 43 * @hide 44 */ 45public 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