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