1/* 2 * Copyright (C) 2017 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.wear.widget.drawer; 18 19import android.animation.Animator; 20import android.content.Context; 21import android.content.res.TypedArray; 22import android.graphics.Canvas; 23import android.graphics.Color; 24import android.graphics.Paint; 25import android.graphics.Paint.Style; 26import android.graphics.RadialGradient; 27import android.graphics.Shader; 28import android.graphics.Shader.TileMode; 29import android.os.Build; 30import android.support.annotation.RequiresApi; 31import android.support.annotation.RestrictTo; 32import android.support.annotation.RestrictTo.Scope; 33import android.support.v4.view.PagerAdapter; 34import android.support.v4.view.ViewPager; 35import android.support.v4.view.ViewPager.OnPageChangeListener; 36import android.support.wear.R; 37import android.support.wear.widget.SimpleAnimatorListener; 38import android.util.AttributeSet; 39import android.view.View; 40 41import java.util.concurrent.TimeUnit; 42 43/** 44 * A page indicator for {@link ViewPager} based on {@link 45 * android.support.wear.view.DotsPageIndicator} which identifies the current page in relation to 46 * all available pages. Pages are represented as dots. The current page can be highlighted with a 47 * different color or size dot. 48 * 49 * <p>The default behavior is to fade out the dots when the pager is idle (not settling or being 50 * dragged). This can be changed with {@link #setDotFadeWhenIdle(boolean)}. 51 * 52 * <p>Use {@link #setPager(ViewPager)} to connect this view to a pager instance. 53 * 54 * @hide 55 */ 56@RequiresApi(Build.VERSION_CODES.M) 57@RestrictTo(Scope.LIBRARY_GROUP) 58public class PageIndicatorView extends View implements OnPageChangeListener { 59 60 private static final String TAG = "Dots"; 61 private final Paint mDotPaint; 62 private final Paint mDotPaintShadow; 63 private final Paint mDotPaintSelected; 64 private final Paint mDotPaintShadowSelected; 65 private int mDotSpacing; 66 private float mDotRadius; 67 private float mDotRadiusSelected; 68 private int mDotColor; 69 private int mDotColorSelected; 70 private boolean mDotFadeWhenIdle; 71 private int mDotFadeOutDelay; 72 private int mDotFadeOutDuration; 73 private int mDotFadeInDuration; 74 private float mDotShadowDx; 75 private float mDotShadowDy; 76 private float mDotShadowRadius; 77 private int mDotShadowColor; 78 private PagerAdapter mAdapter; 79 private int mNumberOfPositions; 80 private int mSelectedPosition; 81 private int mCurrentViewPagerState; 82 private boolean mVisible; 83 84 public PageIndicatorView(Context context) { 85 this(context, null); 86 } 87 88 public PageIndicatorView(Context context, AttributeSet attrs) { 89 this(context, attrs, 0); 90 } 91 92 public PageIndicatorView(Context context, AttributeSet attrs, int defStyleAttr) { 93 super(context, attrs, defStyleAttr); 94 95 final TypedArray a = 96 getContext() 97 .obtainStyledAttributes( 98 attrs, R.styleable.PageIndicatorView, defStyleAttr, 99 R.style.WsPageIndicatorViewStyle); 100 101 mDotSpacing = a.getDimensionPixelOffset( 102 R.styleable.PageIndicatorView_wsPageIndicatorDotSpacing, 0); 103 mDotRadius = a.getDimension(R.styleable.PageIndicatorView_wsPageIndicatorDotRadius, 0); 104 mDotRadiusSelected = 105 a.getDimension(R.styleable.PageIndicatorView_wsPageIndicatorDotRadiusSelected, 0); 106 mDotColor = a.getColor(R.styleable.PageIndicatorView_wsPageIndicatorDotColor, 0); 107 mDotColorSelected = a 108 .getColor(R.styleable.PageIndicatorView_wsPageIndicatorDotColorSelected, 0); 109 mDotFadeOutDelay = 110 a.getInt(R.styleable.PageIndicatorView_wsPageIndicatorDotFadeOutDelay, 0); 111 mDotFadeOutDuration = 112 a.getInt(R.styleable.PageIndicatorView_wsPageIndicatorDotFadeOutDuration, 0); 113 mDotFadeInDuration = 114 a.getInt(R.styleable.PageIndicatorView_wsPageIndicatorDotFadeInDuration, 0); 115 mDotFadeWhenIdle = 116 a.getBoolean(R.styleable.PageIndicatorView_wsPageIndicatorDotFadeWhenIdle, false); 117 mDotShadowDx = a.getDimension(R.styleable.PageIndicatorView_wsPageIndicatorDotShadowDx, 0); 118 mDotShadowDy = a.getDimension(R.styleable.PageIndicatorView_wsPageIndicatorDotShadowDy, 0); 119 mDotShadowRadius = 120 a.getDimension(R.styleable.PageIndicatorView_wsPageIndicatorDotShadowRadius, 0); 121 mDotShadowColor = 122 a.getColor(R.styleable.PageIndicatorView_wsPageIndicatorDotShadowColor, 0); 123 a.recycle(); 124 125 mDotPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 126 mDotPaint.setColor(mDotColor); 127 mDotPaint.setStyle(Style.FILL); 128 129 mDotPaintSelected = new Paint(Paint.ANTI_ALIAS_FLAG); 130 mDotPaintSelected.setColor(mDotColorSelected); 131 mDotPaintSelected.setStyle(Style.FILL); 132 mDotPaintShadow = new Paint(Paint.ANTI_ALIAS_FLAG); 133 mDotPaintShadowSelected = new Paint(Paint.ANTI_ALIAS_FLAG); 134 135 mCurrentViewPagerState = ViewPager.SCROLL_STATE_IDLE; 136 if (isInEditMode()) { 137 // When displayed in layout preview: 138 // Simulate 5 positions, currently on the 3rd position. 139 mNumberOfPositions = 5; 140 mSelectedPosition = 2; 141 mDotFadeWhenIdle = false; 142 } 143 144 if (mDotFadeWhenIdle) { 145 mVisible = false; 146 animate().alpha(0f).setStartDelay(2000).setDuration(mDotFadeOutDuration).start(); 147 } else { 148 animate().cancel(); 149 setAlpha(1.0f); 150 } 151 updateShadows(); 152 } 153 154 private void updateShadows() { 155 updateDotPaint( 156 mDotPaint, mDotPaintShadow, mDotRadius, mDotShadowRadius, mDotColor, 157 mDotShadowColor); 158 updateDotPaint( 159 mDotPaintSelected, 160 mDotPaintShadowSelected, 161 mDotRadiusSelected, 162 mDotShadowRadius, 163 mDotColorSelected, 164 mDotShadowColor); 165 } 166 167 private void updateDotPaint( 168 Paint dotPaint, 169 Paint shadowPaint, 170 float baseRadius, 171 float shadowRadius, 172 int color, 173 int shadowColor) { 174 float radius = baseRadius + shadowRadius; 175 float shadowStart = baseRadius / radius; 176 Shader gradient = 177 new RadialGradient( 178 0, 179 0, 180 radius, 181 new int[]{shadowColor, shadowColor, Color.TRANSPARENT}, 182 new float[]{0f, shadowStart, 1f}, 183 TileMode.CLAMP); 184 185 shadowPaint.setShader(gradient); 186 dotPaint.setColor(color); 187 dotPaint.setStyle(Style.FILL); 188 } 189 190 /** 191 * Supplies the ViewPager instance, and attaches this views {@link OnPageChangeListener} to the 192 * pager. 193 * 194 * @param pager the pager for the page indicator 195 */ 196 public void setPager(ViewPager pager) { 197 pager.addOnPageChangeListener(this); 198 setPagerAdapter(pager.getAdapter()); 199 mAdapter = pager.getAdapter(); 200 if (mAdapter != null && mAdapter.getCount() > 0) { 201 positionChanged(0); 202 } 203 } 204 205 /** 206 * Gets the center-to-center distance between page dots. 207 * 208 * @return the distance between page dots 209 */ 210 public float getDotSpacing() { 211 return mDotSpacing; 212 } 213 214 /** 215 * Sets the center-to-center distance between page dots. 216 * 217 * @param spacing the distance between page dots 218 */ 219 public void setDotSpacing(int spacing) { 220 if (mDotSpacing != spacing) { 221 mDotSpacing = spacing; 222 requestLayout(); 223 } 224 } 225 226 /** 227 * Gets the radius of the page dots. 228 * 229 * @return the radius of the page dots 230 */ 231 public float getDotRadius() { 232 return mDotRadius; 233 } 234 235 /** 236 * Sets the radius of the page dots. 237 * 238 * @param radius the radius of the page dots 239 */ 240 public void setDotRadius(int radius) { 241 if (mDotRadius != radius) { 242 mDotRadius = radius; 243 updateShadows(); 244 invalidate(); 245 } 246 } 247 248 /** 249 * Gets the radius of the page dot for the selected page. 250 * 251 * @return the radius of the selected page dot 252 */ 253 public float getDotRadiusSelected() { 254 return mDotRadiusSelected; 255 } 256 257 /** 258 * Sets the radius of the page dot for the selected page. 259 * 260 * @param radius the radius of the selected page dot 261 */ 262 public void setDotRadiusSelected(int radius) { 263 if (mDotRadiusSelected != radius) { 264 mDotRadiusSelected = radius; 265 updateShadows(); 266 invalidate(); 267 } 268 } 269 270 /** 271 * Returns the color used for dots other than the selected page. 272 * 273 * @return color the color used for dots other than the selected page 274 */ 275 public int getDotColor() { 276 return mDotColor; 277 } 278 279 /** 280 * Sets the color used for dots other than the selected page. 281 * 282 * @param color the color used for dots other than the selected page 283 */ 284 public void setDotColor(int color) { 285 if (mDotColor != color) { 286 mDotColor = color; 287 invalidate(); 288 } 289 } 290 291 /** 292 * Returns the color of the dot for the selected page. 293 * 294 * @return the color used for the selected page dot 295 */ 296 public int getDotColorSelected() { 297 return mDotColorSelected; 298 } 299 300 /** 301 * Sets the color of the dot for the selected page. 302 * 303 * @param color the color of the dot for the selected page 304 */ 305 public void setDotColorSelected(int color) { 306 if (mDotColorSelected != color) { 307 mDotColorSelected = color; 308 invalidate(); 309 } 310 } 311 312 /** 313 * Indicates if the dots fade out when the pager is idle. 314 * 315 * @return whether the dots fade out when idle 316 */ 317 public boolean getDotFadeWhenIdle() { 318 return mDotFadeWhenIdle; 319 } 320 321 /** 322 * Sets whether the dots fade out when the pager is idle. 323 * 324 * @param fade whether the dots fade out when idle 325 */ 326 public void setDotFadeWhenIdle(boolean fade) { 327 mDotFadeWhenIdle = fade; 328 if (!fade) { 329 fadeIn(); 330 } 331 } 332 333 /** 334 * Returns the duration of fade out animation, in milliseconds. 335 * 336 * @return the duration of the fade out animation, in milliseconds 337 */ 338 public int getDotFadeOutDuration() { 339 return mDotFadeOutDuration; 340 } 341 342 /** 343 * Sets the duration of the fade out animation. 344 * 345 * @param duration the duration of the fade out animation 346 */ 347 public void setDotFadeOutDuration(int duration, TimeUnit unit) { 348 mDotFadeOutDuration = (int) TimeUnit.MILLISECONDS.convert(duration, unit); 349 } 350 351 /** 352 * Returns the duration of the fade in duration, in milliseconds. 353 * 354 * @return the duration of the fade in duration, in milliseconds 355 */ 356 public int getDotFadeInDuration() { 357 return mDotFadeInDuration; 358 } 359 360 /** 361 * Sets the duration of the fade in animation. 362 * 363 * @param duration the duration of the fade in animation 364 */ 365 public void setDotFadeInDuration(int duration, TimeUnit unit) { 366 mDotFadeInDuration = (int) TimeUnit.MILLISECONDS.convert(duration, unit); 367 } 368 369 /** 370 * Sets the delay between the pager arriving at an idle state, and the fade out animation 371 * beginning, in milliseconds. 372 * 373 * @return the delay before the fade out animation begins, in milliseconds 374 */ 375 public int getDotFadeOutDelay() { 376 return mDotFadeOutDelay; 377 } 378 379 /** 380 * Sets the delay between the pager arriving at an idle state, and the fade out animation 381 * beginning, in milliseconds. 382 * 383 * @param delay the delay before the fade out animation begins, in milliseconds 384 */ 385 public void setDotFadeOutDelay(int delay) { 386 mDotFadeOutDelay = delay; 387 } 388 389 /** 390 * Sets the pixel radius of shadows drawn beneath the dots. 391 * 392 * @return the pixel radius of shadows rendered beneath the dots 393 */ 394 public float getDotShadowRadius() { 395 return mDotShadowRadius; 396 } 397 398 /** 399 * Sets the pixel radius of shadows drawn beneath the dots. 400 * 401 * @param radius the pixel radius of shadows rendered beneath the dots 402 */ 403 public void setDotShadowRadius(float radius) { 404 if (mDotShadowRadius != radius) { 405 mDotShadowRadius = radius; 406 updateShadows(); 407 invalidate(); 408 } 409 } 410 411 /** 412 * Returns the horizontal offset of shadows drawn beneath the dots. 413 * 414 * @return the horizontal offset of shadows drawn beneath the dots 415 */ 416 public float getDotShadowDx() { 417 return mDotShadowDx; 418 } 419 420 /** 421 * Sets the horizontal offset of shadows drawn beneath the dots. 422 * 423 * @param dx the horizontal offset of shadows drawn beneath the dots 424 */ 425 public void setDotShadowDx(float dx) { 426 mDotShadowDx = dx; 427 invalidate(); 428 } 429 430 /** 431 * Returns the vertical offset of shadows drawn beneath the dots. 432 * 433 * @return the vertical offset of shadows drawn beneath the dots 434 */ 435 public float getDotShadowDy() { 436 return mDotShadowDy; 437 } 438 439 /** 440 * Sets the vertical offset of shadows drawn beneath the dots. 441 * 442 * @param dy the vertical offset of shadows drawn beneath the dots 443 */ 444 public void setDotShadowDy(float dy) { 445 mDotShadowDy = dy; 446 invalidate(); 447 } 448 449 /** 450 * Returns the color of the shadows drawn beneath the dots. 451 * 452 * @return the color of the shadows drawn beneath the dots 453 */ 454 public int getDotShadowColor() { 455 return mDotShadowColor; 456 } 457 458 /** 459 * Sets the color of the shadows drawn beneath the dots. 460 * 461 * @param color the color of the shadows drawn beneath the dots 462 */ 463 public void setDotShadowColor(int color) { 464 mDotShadowColor = color; 465 updateShadows(); 466 invalidate(); 467 } 468 469 private void positionChanged(int position) { 470 mSelectedPosition = position; 471 invalidate(); 472 } 473 474 private void updateNumberOfPositions() { 475 int count = mAdapter.getCount(); 476 if (count != mNumberOfPositions) { 477 mNumberOfPositions = count; 478 requestLayout(); 479 } 480 } 481 482 private void fadeIn() { 483 mVisible = true; 484 animate().cancel(); 485 animate().alpha(1f).setStartDelay(0).setDuration(mDotFadeInDuration).start(); 486 } 487 488 private void fadeOut(long delayMillis) { 489 mVisible = false; 490 animate().cancel(); 491 animate().alpha(0f).setStartDelay(delayMillis).setDuration(mDotFadeOutDuration).start(); 492 } 493 494 private void fadeInOut() { 495 mVisible = true; 496 animate().cancel(); 497 animate() 498 .alpha(1f) 499 .setStartDelay(0) 500 .setDuration(mDotFadeInDuration) 501 .setListener( 502 new SimpleAnimatorListener() { 503 @Override 504 public void onAnimationComplete(Animator animator) { 505 mVisible = false; 506 animate() 507 .alpha(0f) 508 .setListener(null) 509 .setStartDelay(mDotFadeOutDelay) 510 .setDuration(mDotFadeOutDuration) 511 .start(); 512 } 513 }) 514 .start(); 515 } 516 517 @Override 518 public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { 519 if (mDotFadeWhenIdle) { 520 if (mCurrentViewPagerState == ViewPager.SCROLL_STATE_DRAGGING) { 521 if (positionOffset != 0) { 522 if (!mVisible) { 523 fadeIn(); 524 } 525 } else { 526 if (mVisible) { 527 fadeOut(0); 528 } 529 } 530 } 531 } 532 } 533 534 @Override 535 public void onPageSelected(int position) { 536 if (position != mSelectedPosition) { 537 positionChanged(position); 538 } 539 } 540 541 @Override 542 public void onPageScrollStateChanged(int state) { 543 if (mCurrentViewPagerState != state) { 544 mCurrentViewPagerState = state; 545 if (mDotFadeWhenIdle) { 546 if (state == ViewPager.SCROLL_STATE_IDLE) { 547 if (mVisible) { 548 fadeOut(mDotFadeOutDelay); 549 } else { 550 fadeInOut(); 551 } 552 } 553 } 554 } 555 } 556 557 /** 558 * Sets the {@link PagerAdapter}. 559 */ 560 public void setPagerAdapter(PagerAdapter adapter) { 561 mAdapter = adapter; 562 if (mAdapter != null) { 563 updateNumberOfPositions(); 564 if (mDotFadeWhenIdle) { 565 fadeInOut(); 566 } 567 } 568 } 569 570 @Override 571 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 572 int totalWidth; 573 if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) { 574 totalWidth = MeasureSpec.getSize(widthMeasureSpec); 575 } else { 576 int contentWidth = mNumberOfPositions * mDotSpacing; 577 totalWidth = contentWidth + getPaddingLeft() + getPaddingRight(); 578 } 579 int totalHeight; 580 if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY) { 581 totalHeight = MeasureSpec.getSize(heightMeasureSpec); 582 } else { 583 float maxRadius = 584 Math.max(mDotRadius + mDotShadowRadius, mDotRadiusSelected + mDotShadowRadius); 585 int contentHeight = (int) Math.ceil(maxRadius * 2); 586 contentHeight = (int) (contentHeight + mDotShadowDy); 587 totalHeight = contentHeight + getPaddingTop() + getPaddingBottom(); 588 } 589 setMeasuredDimension( 590 resolveSizeAndState(totalWidth, widthMeasureSpec, 0), 591 resolveSizeAndState(totalHeight, heightMeasureSpec, 0)); 592 } 593 594 @Override 595 protected void onDraw(Canvas canvas) { 596 super.onDraw(canvas); 597 598 if (mNumberOfPositions > 1) { 599 float dotCenterLeft = getPaddingLeft() + (mDotSpacing / 2f); 600 float dotCenterTop = getHeight() / 2f; 601 canvas.save(); 602 canvas.translate(dotCenterLeft, dotCenterTop); 603 for (int i = 0; i < mNumberOfPositions; i++) { 604 if (i == mSelectedPosition) { 605 float radius = mDotRadiusSelected + mDotShadowRadius; 606 canvas.drawCircle(mDotShadowDx, mDotShadowDy, radius, mDotPaintShadowSelected); 607 canvas.drawCircle(0, 0, mDotRadiusSelected, mDotPaintSelected); 608 } else { 609 float radius = mDotRadius + mDotShadowRadius; 610 canvas.drawCircle(mDotShadowDx, mDotShadowDy, radius, mDotPaintShadow); 611 canvas.drawCircle(0, 0, mDotRadius, mDotPaint); 612 } 613 canvas.translate(mDotSpacing, 0); 614 } 615 canvas.restore(); 616 } 617 } 618 619 /** 620 * Notifies the view that the data set has changed. 621 */ 622 public void notifyDataSetChanged() { 623 if (mAdapter != null && mAdapter.getCount() > 0) { 624 updateNumberOfPositions(); 625 } 626 } 627} 628