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 17 package android.support.wear.widget.drawer; 18 19 import android.animation.Animator; 20 import android.content.Context; 21 import android.content.res.TypedArray; 22 import android.graphics.Canvas; 23 import android.graphics.Color; 24 import android.graphics.Paint; 25 import android.graphics.Paint.Style; 26 import android.graphics.RadialGradient; 27 import android.graphics.Shader; 28 import android.graphics.Shader.TileMode; 29 import android.os.Build; 30 import android.support.annotation.RequiresApi; 31 import android.support.annotation.RestrictTo; 32 import android.support.annotation.RestrictTo.Scope; 33 import android.support.v4.view.PagerAdapter; 34 import android.support.v4.view.ViewPager; 35 import android.support.v4.view.ViewPager.OnPageChangeListener; 36 import android.support.wear.R; 37 import android.support.wear.widget.SimpleAnimatorListener; 38 import android.util.AttributeSet; 39 import android.view.View; 40 41 import 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) 58 public 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 PageIndicatorView(Context context)84 public PageIndicatorView(Context context) { 85 this(context, null); 86 } 87 PageIndicatorView(Context context, AttributeSet attrs)88 public PageIndicatorView(Context context, AttributeSet attrs) { 89 this(context, attrs, 0); 90 } 91 PageIndicatorView(Context context, AttributeSet attrs, int defStyleAttr)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 updateShadows()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 updateDotPaint( Paint dotPaint, Paint shadowPaint, float baseRadius, float shadowRadius, int color, int shadowColor)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 */ setPager(ViewPager pager)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 */ getDotSpacing()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 */ setDotSpacing(int spacing)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 */ getDotRadius()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 */ setDotRadius(int radius)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 */ getDotRadiusSelected()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 */ setDotRadiusSelected(int radius)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 */ getDotColor()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 */ setDotColor(int color)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 */ getDotColorSelected()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 */ setDotColorSelected(int color)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 */ getDotFadeWhenIdle()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 */ setDotFadeWhenIdle(boolean fade)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 */ getDotFadeOutDuration()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 */ setDotFadeOutDuration(int duration, TimeUnit unit)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 */ getDotFadeInDuration()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 */ setDotFadeInDuration(int duration, TimeUnit unit)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 */ getDotFadeOutDelay()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 */ setDotFadeOutDelay(int delay)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 */ getDotShadowRadius()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 */ setDotShadowRadius(float radius)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 */ getDotShadowDx()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 */ setDotShadowDx(float dx)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 */ getDotShadowDy()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 */ setDotShadowDy(float dy)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 */ getDotShadowColor()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 */ setDotShadowColor(int color)463 public void setDotShadowColor(int color) { 464 mDotShadowColor = color; 465 updateShadows(); 466 invalidate(); 467 } 468 positionChanged(int position)469 private void positionChanged(int position) { 470 mSelectedPosition = position; 471 invalidate(); 472 } 473 updateNumberOfPositions()474 private void updateNumberOfPositions() { 475 int count = mAdapter.getCount(); 476 if (count != mNumberOfPositions) { 477 mNumberOfPositions = count; 478 requestLayout(); 479 } 480 } 481 fadeIn()482 private void fadeIn() { 483 mVisible = true; 484 animate().cancel(); 485 animate().alpha(1f).setStartDelay(0).setDuration(mDotFadeInDuration).start(); 486 } 487 fadeOut(long delayMillis)488 private void fadeOut(long delayMillis) { 489 mVisible = false; 490 animate().cancel(); 491 animate().alpha(0f).setStartDelay(delayMillis).setDuration(mDotFadeOutDuration).start(); 492 } 493 fadeInOut()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 onPageScrolled(int position, float positionOffset, int positionOffsetPixels)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 onPageSelected(int position)535 public void onPageSelected(int position) { 536 if (position != mSelectedPosition) { 537 positionChanged(position); 538 } 539 } 540 541 @Override onPageScrollStateChanged(int state)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 */ setPagerAdapter(PagerAdapter adapter)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 onMeasure(int widthMeasureSpec, int heightMeasureSpec)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 onDraw(Canvas canvas)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 */ notifyDataSetChanged()622 public void notifyDataSetChanged() { 623 if (mAdapter != null && mAdapter.getCount() > 0) { 624 updateNumberOfPositions(); 625 } 626 } 627 } 628