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