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