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