1 /* 2 * Copyright 2018 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.swiperefreshlayout.widget; 18 19 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; 20 21 import android.animation.Animator; 22 import android.animation.ValueAnimator; 23 import android.content.Context; 24 import android.content.res.Resources; 25 import android.graphics.Canvas; 26 import android.graphics.Color; 27 import android.graphics.ColorFilter; 28 import android.graphics.Paint; 29 import android.graphics.Paint.Style; 30 import android.graphics.Path; 31 import android.graphics.PixelFormat; 32 import android.graphics.Rect; 33 import android.graphics.RectF; 34 import android.graphics.drawable.Animatable; 35 import android.graphics.drawable.Drawable; 36 import android.util.DisplayMetrics; 37 import android.view.animation.Interpolator; 38 import android.view.animation.LinearInterpolator; 39 40 import androidx.annotation.IntDef; 41 import androidx.annotation.NonNull; 42 import androidx.annotation.RestrictTo; 43 import androidx.core.util.Preconditions; 44 import androidx.interpolator.view.animation.FastOutSlowInInterpolator; 45 46 import java.lang.annotation.Retention; 47 import java.lang.annotation.RetentionPolicy; 48 49 /** 50 * Drawable that renders the animated indeterminate progress indicator in the Material design style 51 * without depending on API level 11. 52 * 53 * <p>While this may be used to draw an indeterminate spinner using {@link #start()} and {@link 54 * #stop()} methods, this may also be used to draw a progress arc using {@link 55 * #setStartEndTrim(float, float)} method. CircularProgressDrawable also supports adding an arrow 56 * at the end of the arc by {@link #setArrowEnabled(boolean)} and {@link #setArrowDimensions(float, 57 * float)} methods. 58 * 59 * <p>To use one of the pre-defined sizes instead of using your own, {@link #setStyle(int)} should 60 * be called with one of the {@link #DEFAULT} or {@link #LARGE} styles as its parameter. Doing it 61 * so will update the arrow dimensions, ring size and stroke width to fit the one specified. 62 * 63 * <p>If no center radius is set via {@link #setCenterRadius(float)} or {@link #setStyle(int)} 64 * methods, CircularProgressDrawable will fill the bounds set via {@link #setBounds(Rect)}. 65 */ 66 public class CircularProgressDrawable extends Drawable implements Animatable { 67 private static final Interpolator LINEAR_INTERPOLATOR = new LinearInterpolator(); 68 private static final Interpolator MATERIAL_INTERPOLATOR = new FastOutSlowInInterpolator(); 69 70 /** @hide */ 71 @RestrictTo(LIBRARY_GROUP) 72 @Retention(RetentionPolicy.SOURCE) 73 @IntDef({LARGE, DEFAULT}) 74 public @interface ProgressDrawableSize { 75 } 76 77 /** Maps to ProgressBar.Large style. */ 78 public static final int LARGE = 0; 79 80 private static final float CENTER_RADIUS_LARGE = 11f; 81 private static final float STROKE_WIDTH_LARGE = 3f; 82 private static final int ARROW_WIDTH_LARGE = 12; 83 private static final int ARROW_HEIGHT_LARGE = 6; 84 85 /** Maps to ProgressBar default style. */ 86 public static final int DEFAULT = 1; 87 88 private static final float CENTER_RADIUS = 7.5f; 89 private static final float STROKE_WIDTH = 2.5f; 90 private static final int ARROW_WIDTH = 10; 91 private static final int ARROW_HEIGHT = 5; 92 93 /** 94 * This is the default set of colors that's used in spinner. {@link 95 * #setColorSchemeColors(int...)} allows modifying colors. 96 */ 97 private static final int[] COLORS = new int[]{ 98 Color.BLACK 99 }; 100 101 /** 102 * The value in the linear interpolator for animating the drawable at which 103 * the color transition should start 104 */ 105 private static final float COLOR_CHANGE_OFFSET = 0.75f; 106 private static final float SHRINK_OFFSET = 0.5f; 107 108 /** The duration of a single progress spin in milliseconds. */ 109 private static final int ANIMATION_DURATION = 1332; 110 111 /** Full rotation that's done for the animation duration in degrees. */ 112 private static final float GROUP_FULL_ROTATION = 1080f / 5f; 113 114 /** The indicator ring, used to manage animation state. */ 115 private final Ring mRing; 116 117 /** Canvas rotation in degrees. */ 118 private float mRotation; 119 120 /** Maximum length of the progress arc during the animation. */ 121 private static final float MAX_PROGRESS_ARC = .8f; 122 /** Minimum length of the progress arc during the animation. */ 123 private static final float MIN_PROGRESS_ARC = .01f; 124 125 /** Rotation applied to ring during the animation, to complete it to a full circle. */ 126 private static final float RING_ROTATION = 1f - (MAX_PROGRESS_ARC - MIN_PROGRESS_ARC); 127 128 private Resources mResources; 129 private Animator mAnimator; 130 private float mRotationCount; 131 private boolean mFinishing; 132 133 /** 134 * @param context application context 135 */ 136 public CircularProgressDrawable(@NonNull Context context) { 137 mResources = Preconditions.checkNotNull(context).getResources(); 138 139 mRing = new Ring(); 140 mRing.setColors(COLORS); 141 142 setStrokeWidth(STROKE_WIDTH); 143 setupAnimators(); 144 } 145 146 /** Sets all parameters at once in dp. */ 147 private void setSizeParameters(float centerRadius, float strokeWidth, float arrowWidth, 148 float arrowHeight) { 149 final Ring ring = mRing; 150 final DisplayMetrics metrics = mResources.getDisplayMetrics(); 151 final float screenDensity = metrics.density; 152 153 ring.setStrokeWidth(strokeWidth * screenDensity); 154 ring.setCenterRadius(centerRadius * screenDensity); 155 ring.setColorIndex(0); 156 ring.setArrowDimensions(arrowWidth * screenDensity, arrowHeight * screenDensity); 157 } 158 159 /** 160 * Sets the overall size for the progress spinner. This updates the radius 161 * and stroke width of the ring, and arrow dimensions. 162 * 163 * @param size one of {@link #LARGE} or {@link #DEFAULT} 164 */ 165 public void setStyle(@ProgressDrawableSize int size) { 166 if (size == LARGE) { 167 setSizeParameters(CENTER_RADIUS_LARGE, STROKE_WIDTH_LARGE, ARROW_WIDTH_LARGE, 168 ARROW_HEIGHT_LARGE); 169 } else { 170 setSizeParameters(CENTER_RADIUS, STROKE_WIDTH, ARROW_WIDTH, ARROW_HEIGHT); 171 } 172 invalidateSelf(); 173 } 174 175 /** 176 * Returns the stroke width for the progress spinner in pixels. 177 * 178 * @return stroke width in pixels 179 */ 180 public float getStrokeWidth() { 181 return mRing.getStrokeWidth(); 182 } 183 184 /** 185 * Sets the stroke width for the progress spinner in pixels. 186 * 187 * @param strokeWidth stroke width in pixels 188 */ 189 public void setStrokeWidth(float strokeWidth) { 190 mRing.setStrokeWidth(strokeWidth); 191 invalidateSelf(); 192 } 193 194 /** 195 * Returns the center radius for the progress spinner in pixels. 196 * 197 * @return center radius in pixels 198 */ 199 public float getCenterRadius() { 200 return mRing.getCenterRadius(); 201 } 202 203 /** 204 * Sets the center radius for the progress spinner in pixels. If set to 0, this drawable will 205 * fill the bounds when drawn. 206 * 207 * @param centerRadius center radius in pixels 208 */ 209 public void setCenterRadius(float centerRadius) { 210 mRing.setCenterRadius(centerRadius); 211 invalidateSelf(); 212 } 213 214 /** 215 * Sets the stroke cap of the progress spinner. Default stroke cap is {@link Paint.Cap#SQUARE}. 216 * 217 * @param strokeCap stroke cap 218 */ 219 public void setStrokeCap(@NonNull Paint.Cap strokeCap) { 220 mRing.setStrokeCap(strokeCap); 221 invalidateSelf(); 222 } 223 224 /** 225 * Returns the stroke cap of the progress spinner. 226 * 227 * @return stroke cap 228 */ 229 @NonNull 230 public Paint.Cap getStrokeCap() { 231 return mRing.getStrokeCap(); 232 } 233 234 /** 235 * Returns the arrow width in pixels. 236 * 237 * @return arrow width in pixels 238 */ 239 public float getArrowWidth() { 240 return mRing.getArrowWidth(); 241 } 242 243 /** 244 * Returns the arrow height in pixels. 245 * 246 * @return arrow height in pixels 247 */ 248 public float getArrowHeight() { 249 return mRing.getArrowHeight(); 250 } 251 252 /** 253 * Sets the dimensions of the arrow at the end of the spinner in pixels. 254 * 255 * @param width width of the baseline of the arrow in pixels 256 * @param height distance from tip of the arrow to its baseline in pixels 257 */ 258 public void setArrowDimensions(float width, float height) { 259 mRing.setArrowDimensions(width, height); 260 invalidateSelf(); 261 } 262 263 /** 264 * Returns {@code true} if the arrow at the end of the spinner is shown. 265 * 266 * @return {@code true} if the arrow is shown, {@code false} otherwise. 267 */ 268 public boolean getArrowEnabled() { 269 return mRing.getShowArrow(); 270 } 271 272 /** 273 * Sets if the arrow at the end of the spinner should be shown. 274 * 275 * @param show {@code true} if the arrow should be drawn, {@code false} otherwise 276 */ 277 public void setArrowEnabled(boolean show) { 278 mRing.setShowArrow(show); 279 invalidateSelf(); 280 } 281 282 /** 283 * Returns the scale of the arrow at the end of the spinner. 284 * 285 * @return scale of the arrow 286 */ 287 public float getArrowScale() { 288 return mRing.getArrowScale(); 289 } 290 291 /** 292 * Sets the scale of the arrow at the end of the spinner. 293 * 294 * @param scale scaling that will be applied to the arrow's both width and height when drawing. 295 */ 296 public void setArrowScale(float scale) { 297 mRing.setArrowScale(scale); 298 invalidateSelf(); 299 } 300 301 /** 302 * Returns the start trim for the progress spinner arc 303 * 304 * @return start trim from [0..1] 305 */ 306 public float getStartTrim() { 307 return mRing.getStartTrim(); 308 } 309 310 /** 311 * Returns the end trim for the progress spinner arc 312 * 313 * @return end trim from [0..1] 314 */ 315 public float getEndTrim() { 316 return mRing.getEndTrim(); 317 } 318 319 /** 320 * Sets the start and end trim for the progress spinner arc. 0 corresponds to the geometric 321 * angle of 0 degrees (3 o'clock on a watch) and it increases clockwise, coming to a full circle 322 * at 1. 323 * 324 * @param start starting position of the arc from [0..1] 325 * @param end ending position of the arc from [0..1] 326 */ 327 public void setStartEndTrim(float start, float end) { 328 mRing.setStartTrim(start); 329 mRing.setEndTrim(end); 330 invalidateSelf(); 331 } 332 333 /** 334 * Returns the amount of rotation applied to the progress spinner. 335 * 336 * @return amount of rotation from [0..1] 337 */ 338 public float getProgressRotation() { 339 return mRing.getRotation(); 340 } 341 342 /** 343 * Sets the amount of rotation to apply to the progress spinner. 344 * 345 * @param rotation rotation from [0..1] 346 */ 347 public void setProgressRotation(float rotation) { 348 mRing.setRotation(rotation); 349 invalidateSelf(); 350 } 351 352 /** 353 * Returns the background color of the circle drawn inside the drawable. 354 * 355 * @return an ARGB color 356 */ 357 public int getBackgroundColor() { 358 return mRing.getBackgroundColor(); 359 } 360 361 /** 362 * Sets the background color of the circle inside the drawable. Calling {@link 363 * #setAlpha(int)} does not affect the visibility background color, so it should be set 364 * separately if it needs to be hidden or visible. 365 * 366 * @param color an ARGB color 367 */ 368 public void setBackgroundColor(int color) { 369 mRing.setBackgroundColor(color); 370 invalidateSelf(); 371 } 372 373 /** 374 * Returns the colors used in the progress animation 375 * 376 * @return list of ARGB colors 377 */ 378 @NonNull 379 public int[] getColorSchemeColors() { 380 return mRing.getColors(); 381 } 382 383 /** 384 * Sets the colors used in the progress animation from a color list. The first color will also 385 * be the color to be used if animation is not started yet. 386 * 387 * @param colors list of ARGB colors to be used in the spinner 388 */ 389 public void setColorSchemeColors(@NonNull int... colors) { 390 mRing.setColors(colors); 391 mRing.setColorIndex(0); 392 invalidateSelf(); 393 } 394 395 @Override 396 public void draw(Canvas canvas) { 397 final Rect bounds = getBounds(); 398 canvas.save(); 399 canvas.rotate(mRotation, bounds.exactCenterX(), bounds.exactCenterY()); 400 mRing.draw(canvas, bounds); 401 canvas.restore(); 402 } 403 404 @Override 405 public void setAlpha(int alpha) { 406 mRing.setAlpha(alpha); 407 invalidateSelf(); 408 } 409 410 @Override 411 public int getAlpha() { 412 return mRing.getAlpha(); 413 } 414 415 @Override 416 public void setColorFilter(ColorFilter colorFilter) { 417 mRing.setColorFilter(colorFilter); 418 invalidateSelf(); 419 } 420 421 private void setRotation(float rotation) { 422 mRotation = rotation; 423 } 424 425 private float getRotation() { 426 return mRotation; 427 } 428 429 @Override 430 public int getOpacity() { 431 return PixelFormat.TRANSLUCENT; 432 } 433 434 @Override 435 public boolean isRunning() { 436 return mAnimator.isRunning(); 437 } 438 439 /** 440 * Starts the animation for the spinner. 441 */ 442 @Override 443 public void start() { 444 mAnimator.cancel(); 445 mRing.storeOriginals(); 446 // Already showing some part of the ring 447 if (mRing.getEndTrim() != mRing.getStartTrim()) { 448 mFinishing = true; 449 mAnimator.setDuration(ANIMATION_DURATION / 2); 450 mAnimator.start(); 451 } else { 452 mRing.setColorIndex(0); 453 mRing.resetOriginals(); 454 mAnimator.setDuration(ANIMATION_DURATION); 455 mAnimator.start(); 456 } 457 } 458 459 /** 460 * Stops the animation for the spinner. 461 */ 462 @Override 463 public void stop() { 464 mAnimator.cancel(); 465 setRotation(0); 466 mRing.setShowArrow(false); 467 mRing.setColorIndex(0); 468 mRing.resetOriginals(); 469 invalidateSelf(); 470 } 471 472 // Adapted from ArgbEvaluator.java 473 private int evaluateColorChange(float fraction, int startValue, int endValue) { 474 int startA = (startValue >> 24) & 0xff; 475 int startR = (startValue >> 16) & 0xff; 476 int startG = (startValue >> 8) & 0xff; 477 int startB = startValue & 0xff; 478 479 int endA = (endValue >> 24) & 0xff; 480 int endR = (endValue >> 16) & 0xff; 481 int endG = (endValue >> 8) & 0xff; 482 int endB = endValue & 0xff; 483 484 return (startA + (int) (fraction * (endA - startA))) << 24 485 | (startR + (int) (fraction * (endR - startR))) << 16 486 | (startG + (int) (fraction * (endG - startG))) << 8 487 | (startB + (int) (fraction * (endB - startB))); 488 } 489 490 /** 491 * Update the ring color if this is within the last 25% of the animation. 492 * The new ring color will be a translation from the starting ring color to 493 * the next color. 494 */ 495 private void updateRingColor(float interpolatedTime, Ring ring) { 496 if (interpolatedTime > COLOR_CHANGE_OFFSET) { 497 ring.setColor(evaluateColorChange((interpolatedTime - COLOR_CHANGE_OFFSET) 498 / (1f - COLOR_CHANGE_OFFSET), ring.getStartingColor(), 499 ring.getNextColor())); 500 } else { 501 ring.setColor(ring.getStartingColor()); 502 } 503 } 504 505 /** 506 * Update the ring start and end trim if the animation is finishing (i.e. it started with 507 * already visible progress, so needs to shrink back down before starting the spinner). 508 */ 509 private void applyFinishTranslation(float interpolatedTime, Ring ring) { 510 // shrink back down and complete a full rotation before 511 // starting other circles 512 // Rotation goes between [0..1]. 513 updateRingColor(interpolatedTime, ring); 514 float targetRotation = (float) (Math.floor(ring.getStartingRotation() / MAX_PROGRESS_ARC) 515 + 1f); 516 final float startTrim = ring.getStartingStartTrim() 517 + (ring.getStartingEndTrim() - MIN_PROGRESS_ARC - ring.getStartingStartTrim()) 518 * interpolatedTime; 519 ring.setStartTrim(startTrim); 520 ring.setEndTrim(ring.getStartingEndTrim()); 521 final float rotation = ring.getStartingRotation() 522 + ((targetRotation - ring.getStartingRotation()) * interpolatedTime); 523 ring.setRotation(rotation); 524 } 525 526 /** 527 * Update the ring start and end trim according to current time of the animation. 528 */ 529 private void applyTransformation(float interpolatedTime, Ring ring, boolean lastFrame) { 530 if (mFinishing) { 531 applyFinishTranslation(interpolatedTime, ring); 532 // Below condition is to work around a ValueAnimator issue where onAnimationRepeat is 533 // called before last frame (1f). 534 } else if (interpolatedTime != 1f || lastFrame) { 535 final float startingRotation = ring.getStartingRotation(); 536 float startTrim, endTrim; 537 538 if (interpolatedTime < SHRINK_OFFSET) { // Expansion occurs on first half of animation 539 final float scaledTime = interpolatedTime / SHRINK_OFFSET; 540 startTrim = ring.getStartingStartTrim(); 541 endTrim = startTrim + ((MAX_PROGRESS_ARC - MIN_PROGRESS_ARC) 542 * MATERIAL_INTERPOLATOR.getInterpolation(scaledTime) + MIN_PROGRESS_ARC); 543 } else { // Shrinking occurs on second half of animation 544 float scaledTime = (interpolatedTime - SHRINK_OFFSET) / (1f - SHRINK_OFFSET); 545 endTrim = ring.getStartingStartTrim() + (MAX_PROGRESS_ARC - MIN_PROGRESS_ARC); 546 startTrim = endTrim - ((MAX_PROGRESS_ARC - MIN_PROGRESS_ARC) 547 * (1f - MATERIAL_INTERPOLATOR.getInterpolation(scaledTime)) 548 + MIN_PROGRESS_ARC); 549 } 550 551 final float rotation = startingRotation + (RING_ROTATION * interpolatedTime); 552 float groupRotation = GROUP_FULL_ROTATION * (interpolatedTime + mRotationCount); 553 554 ring.setStartTrim(startTrim); 555 ring.setEndTrim(endTrim); 556 ring.setRotation(rotation); 557 setRotation(groupRotation); 558 } 559 } 560 561 private void setupAnimators() { 562 final Ring ring = mRing; 563 final ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f); 564 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 565 @Override 566 public void onAnimationUpdate(ValueAnimator animation) { 567 float interpolatedTime = (float) animation.getAnimatedValue(); 568 updateRingColor(interpolatedTime, ring); 569 applyTransformation(interpolatedTime, ring, false); 570 invalidateSelf(); 571 } 572 }); 573 animator.setRepeatCount(ValueAnimator.INFINITE); 574 animator.setRepeatMode(ValueAnimator.RESTART); 575 animator.setInterpolator(LINEAR_INTERPOLATOR); 576 animator.addListener(new Animator.AnimatorListener() { 577 578 @Override 579 public void onAnimationStart(Animator animator) { 580 mRotationCount = 0; 581 } 582 583 @Override 584 public void onAnimationEnd(Animator animator) { 585 // do nothing 586 } 587 588 @Override 589 public void onAnimationCancel(Animator animation) { 590 // do nothing 591 } 592 593 @Override 594 public void onAnimationRepeat(Animator animator) { 595 applyTransformation(1f, ring, true); 596 ring.storeOriginals(); 597 ring.goToNextColor(); 598 if (mFinishing) { 599 // finished closing the last ring from the swipe gesture; go 600 // into progress mode 601 mFinishing = false; 602 animator.cancel(); 603 animator.setDuration(ANIMATION_DURATION); 604 animator.start(); 605 ring.setShowArrow(false); 606 } else { 607 mRotationCount = mRotationCount + 1; 608 } 609 } 610 }); 611 mAnimator = animator; 612 } 613 614 /** 615 * A private class to do all the drawing of CircularProgressDrawable, which includes background, 616 * progress spinner and the arrow. This class is to separate drawing from animation. 617 */ 618 private static class Ring { 619 final RectF mTempBounds = new RectF(); 620 final Paint mPaint = new Paint(); 621 final Paint mArrowPaint = new Paint(); 622 final Paint mCirclePaint = new Paint(); 623 624 float mStartTrim = 0f; 625 float mEndTrim = 0f; 626 float mRotation = 0f; 627 float mStrokeWidth = 5f; 628 629 int[] mColors; 630 // mColorIndex represents the offset into the available mColors that the 631 // progress circle should currently display. As the progress circle is 632 // animating, the mColorIndex moves by one to the next available color. 633 int mColorIndex; 634 float mStartingStartTrim; 635 float mStartingEndTrim; 636 float mStartingRotation; 637 boolean mShowArrow; 638 Path mArrow; 639 float mArrowScale = 1; 640 float mRingCenterRadius; 641 int mArrowWidth; 642 int mArrowHeight; 643 int mAlpha = 255; 644 int mCurrentColor; 645 646 Ring() { 647 mPaint.setStrokeCap(Paint.Cap.SQUARE); 648 mPaint.setAntiAlias(true); 649 mPaint.setStyle(Style.STROKE); 650 651 mArrowPaint.setStyle(Paint.Style.FILL); 652 mArrowPaint.setAntiAlias(true); 653 654 mCirclePaint.setColor(Color.TRANSPARENT); 655 } 656 657 /** 658 * Sets the dimensions of the arrowhead. 659 * 660 * @param width width of the hypotenuse of the arrow head 661 * @param height height of the arrow point 662 */ 663 void setArrowDimensions(float width, float height) { 664 mArrowWidth = (int) width; 665 mArrowHeight = (int) height; 666 } 667 668 void setStrokeCap(Paint.Cap strokeCap) { 669 mPaint.setStrokeCap(strokeCap); 670 } 671 672 Paint.Cap getStrokeCap() { 673 return mPaint.getStrokeCap(); 674 } 675 676 float getArrowWidth() { 677 return mArrowWidth; 678 } 679 680 float getArrowHeight() { 681 return mArrowHeight; 682 } 683 684 /** 685 * Draw the progress spinner 686 */ 687 void draw(Canvas c, Rect bounds) { 688 final RectF arcBounds = mTempBounds; 689 float arcRadius = mRingCenterRadius + mStrokeWidth / 2f; 690 if (mRingCenterRadius <= 0) { 691 // If center radius is not set, fill the bounds 692 arcRadius = Math.min(bounds.width(), bounds.height()) / 2f - Math.max( 693 (mArrowWidth * mArrowScale) / 2f, mStrokeWidth / 2f); 694 } 695 arcBounds.set(bounds.centerX() - arcRadius, 696 bounds.centerY() - arcRadius, 697 bounds.centerX() + arcRadius, 698 bounds.centerY() + arcRadius); 699 700 final float startAngle = (mStartTrim + mRotation) * 360; 701 final float endAngle = (mEndTrim + mRotation) * 360; 702 float sweepAngle = endAngle - startAngle; 703 704 mPaint.setColor(mCurrentColor); 705 mPaint.setAlpha(mAlpha); 706 707 // Draw the background first 708 float inset = mStrokeWidth / 2f; // Calculate inset to draw inside the arc 709 arcBounds.inset(inset, inset); // Apply inset 710 c.drawCircle(arcBounds.centerX(), arcBounds.centerY(), arcBounds.width() / 2f, 711 mCirclePaint); 712 arcBounds.inset(-inset, -inset); // Revert the inset 713 714 c.drawArc(arcBounds, startAngle, sweepAngle, false, mPaint); 715 716 drawTriangle(c, startAngle, sweepAngle, arcBounds); 717 } 718 719 void drawTriangle(Canvas c, float startAngle, float sweepAngle, RectF bounds) { 720 if (mShowArrow) { 721 if (mArrow == null) { 722 mArrow = new android.graphics.Path(); 723 mArrow.setFillType(android.graphics.Path.FillType.EVEN_ODD); 724 } else { 725 mArrow.reset(); 726 } 727 float centerRadius = Math.min(bounds.width(), bounds.height()) / 2f; 728 float inset = mArrowWidth * mArrowScale / 2f; 729 // Update the path each time. This works around an issue in SKIA 730 // where concatenating a rotation matrix to a scale matrix 731 // ignored a starting negative rotation. This appears to have 732 // been fixed as of API 21. 733 mArrow.moveTo(0, 0); 734 mArrow.lineTo(mArrowWidth * mArrowScale, 0); 735 mArrow.lineTo((mArrowWidth * mArrowScale / 2), (mArrowHeight 736 * mArrowScale)); 737 mArrow.offset(centerRadius + bounds.centerX() - inset, 738 bounds.centerY() + mStrokeWidth / 2f); 739 mArrow.close(); 740 // draw a triangle 741 mArrowPaint.setColor(mCurrentColor); 742 mArrowPaint.setAlpha(mAlpha); 743 c.save(); 744 c.rotate(startAngle + sweepAngle, bounds.centerX(), 745 bounds.centerY()); 746 c.drawPath(mArrow, mArrowPaint); 747 c.restore(); 748 } 749 } 750 751 /** 752 * Sets the colors the progress spinner alternates between. 753 * 754 * @param colors array of ARGB colors. Must be non-{@code null}. 755 */ 756 void setColors(@NonNull int[] colors) { 757 mColors = colors; 758 // if colors are reset, make sure to reset the color index as well 759 setColorIndex(0); 760 } 761 762 int[] getColors() { 763 return mColors; 764 } 765 766 /** 767 * Sets the absolute color of the progress spinner. This is should only 768 * be used when animating between current and next color when the 769 * spinner is rotating. 770 * 771 * @param color an ARGB color 772 */ 773 void setColor(int color) { 774 mCurrentColor = color; 775 } 776 777 /** 778 * Sets the background color of the circle inside the spinner. 779 */ 780 void setBackgroundColor(int color) { 781 mCirclePaint.setColor(color); 782 } 783 784 int getBackgroundColor() { 785 return mCirclePaint.getColor(); 786 } 787 788 /** 789 * @param index index into the color array of the color to display in 790 * the progress spinner. 791 */ 792 void setColorIndex(int index) { 793 mColorIndex = index; 794 mCurrentColor = mColors[mColorIndex]; 795 } 796 797 /** 798 * @return int describing the next color the progress spinner should use when drawing. 799 */ 800 int getNextColor() { 801 return mColors[getNextColorIndex()]; 802 } 803 804 int getNextColorIndex() { 805 return (mColorIndex + 1) % (mColors.length); 806 } 807 808 /** 809 * Proceed to the next available ring color. This will automatically 810 * wrap back to the beginning of colors. 811 */ 812 void goToNextColor() { 813 setColorIndex(getNextColorIndex()); 814 } 815 816 void setColorFilter(ColorFilter filter) { 817 mPaint.setColorFilter(filter); 818 } 819 820 /** 821 * @param alpha alpha of the progress spinner and associated arrowhead. 822 */ 823 void setAlpha(int alpha) { 824 mAlpha = alpha; 825 } 826 827 /** 828 * @return current alpha of the progress spinner and arrowhead 829 */ 830 int getAlpha() { 831 return mAlpha; 832 } 833 834 /** 835 * @param strokeWidth set the stroke width of the progress spinner in pixels. 836 */ 837 void setStrokeWidth(float strokeWidth) { 838 mStrokeWidth = strokeWidth; 839 mPaint.setStrokeWidth(strokeWidth); 840 } 841 842 float getStrokeWidth() { 843 return mStrokeWidth; 844 } 845 846 void setStartTrim(float startTrim) { 847 mStartTrim = startTrim; 848 } 849 850 float getStartTrim() { 851 return mStartTrim; 852 } 853 854 float getStartingStartTrim() { 855 return mStartingStartTrim; 856 } 857 858 float getStartingEndTrim() { 859 return mStartingEndTrim; 860 } 861 862 int getStartingColor() { 863 return mColors[mColorIndex]; 864 } 865 866 void setEndTrim(float endTrim) { 867 mEndTrim = endTrim; 868 } 869 870 float getEndTrim() { 871 return mEndTrim; 872 } 873 874 void setRotation(float rotation) { 875 mRotation = rotation; 876 } 877 878 float getRotation() { 879 return mRotation; 880 } 881 882 /** 883 * @param centerRadius inner radius in px of the circle the progress spinner arc traces 884 */ 885 void setCenterRadius(float centerRadius) { 886 mRingCenterRadius = centerRadius; 887 } 888 889 float getCenterRadius() { 890 return mRingCenterRadius; 891 } 892 893 /** 894 * @param show {@code true} if should show the arrow head on the progress spinner 895 */ 896 void setShowArrow(boolean show) { 897 if (mShowArrow != show) { 898 mShowArrow = show; 899 } 900 } 901 902 boolean getShowArrow() { 903 return mShowArrow; 904 } 905 906 /** 907 * @param scale scale of the arrowhead for the spinner 908 */ 909 void setArrowScale(float scale) { 910 if (scale != mArrowScale) { 911 mArrowScale = scale; 912 } 913 } 914 915 float getArrowScale() { 916 return mArrowScale; 917 } 918 919 /** 920 * @return The amount the progress spinner is currently rotated, between [0..1]. 921 */ 922 float getStartingRotation() { 923 return mStartingRotation; 924 } 925 926 /** 927 * If the start / end trim are offset to begin with, store them so that animation starts 928 * from that offset. 929 */ 930 void storeOriginals() { 931 mStartingStartTrim = mStartTrim; 932 mStartingEndTrim = mEndTrim; 933 mStartingRotation = mRotation; 934 } 935 936 /** 937 * Reset the progress spinner to default rotation, start and end angles. 938 */ 939 void resetOriginals() { 940 mStartingStartTrim = 0; 941 mStartingEndTrim = 0; 942 mStartingRotation = 0; 943 setStartTrim(0); 944 setEndTrim(0); 945 setRotation(0); 946 } 947 } 948 } 949