MaterialProgressDrawable.java revision 3fba72ed900aa288256bdf29427df39753964ff0
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 17package android.support.v4.widget; 18 19import android.view.animation.AccelerateDecelerateInterpolator; 20import android.view.animation.Interpolator; 21import android.view.animation.Animation; 22import android.view.animation.LinearInterpolator; 23import android.view.animation.Transformation; 24import android.content.Context; 25import android.content.res.Resources; 26import android.graphics.Canvas; 27import android.graphics.Color; 28import android.graphics.ColorFilter; 29import android.graphics.Paint; 30import android.graphics.Paint.Style; 31import android.graphics.Path; 32import android.graphics.PixelFormat; 33import android.graphics.Rect; 34import android.graphics.RectF; 35import android.graphics.drawable.Drawable; 36import android.graphics.drawable.Animatable; 37import android.support.annotation.IntDef; 38import android.support.annotation.NonNull; 39import android.util.DisplayMetrics; 40import android.view.View; 41 42import java.lang.annotation.Retention; 43import java.lang.annotation.RetentionPolicy; 44import java.util.ArrayList; 45 46/** 47 * Fancy progress indicator for Material theme. 48 * 49 * @hide 50 */ 51class MaterialProgressDrawable extends Drawable implements Animatable { 52 private static final Interpolator LINEAR_INTERPOLATOR = new LinearInterpolator(); 53 private static final Interpolator END_CURVE_INTERPOLATOR = new EndCurveInterpolator(); 54 private static final Interpolator START_CURVE_INTERPOLATOR = new StartCurveInterpolator(); 55 private static final Interpolator EASE_INTERPOLATOR = new AccelerateDecelerateInterpolator(); 56 57 @Retention(RetentionPolicy.CLASS) 58 @IntDef({LARGE, DEFAULT}) 59 public @interface ProgressDrawableSize {} 60 // Maps to ProgressBar.Large style 61 static final int LARGE = 0; 62 // Maps to ProgressBar default style 63 static final int DEFAULT = 1; 64 65 // Maps to ProgressBar default style 66 private static final int CIRCLE_DIAMETER = 40; 67 private static final float CENTER_RADIUS = 8.75f; //should add up to 10 when + stroke_width 68 private static final float STROKE_WIDTH = 2.5f; 69 70 // Maps to ProgressBar.Large style 71 private static final int CIRCLE_DIAMETER_LARGE = 56; 72 private static final float CENTER_RADIUS_LARGE = 12.5f; 73 private static final float STROKE_WIDTH_LARGE = 3f; 74 75 private final int[] COLORS = new int[] { 76 Color.BLACK 77 }; 78 79 /** The duration of a single progress spin in milliseconds. */ 80 private static final int ANIMATION_DURATION = 1000 * 80 / 60; 81 82 /** The number of points in the progress "star". */ 83 private static final float NUM_POINTS = 5f; 84 /** The list of animators operating on this drawable. */ 85 private final ArrayList<Animation> mAnimators = new ArrayList<Animation>(); 86 87 /** The indicator ring, used to manage animation state. */ 88 private final Ring mRing; 89 90 /** Canvas rotation in degrees. */ 91 private float mRotation; 92 93 /** Layout info for the arrowhead in dp */ 94 private static final int ARROW_WIDTH = 10; 95 private static final int ARROW_HEIGHT = 5; 96 private static final float ARROW_OFFSET_ANGLE = 5; 97 98 /** Layout info for the arrowhead for the large spinner in dp */ 99 private static final int ARROW_WIDTH_LARGE = 12; 100 private static final int ARROW_HEIGHT_LARGE = 6; 101 private static final float MAX_PROGRESS_ARC = .8f; 102 103 private Resources mResources; 104 private View mParent; 105 private Animation mAnimation; 106 private float mRotationCount; 107 private double mWidth; 108 private double mHeight; 109 private Animation mFinishAnimation; 110 private boolean mStopped = true; 111 112 public MaterialProgressDrawable(Context context, View parent) { 113 mParent = parent; 114 mResources = context.getResources(); 115 116 mRing = new Ring(mCallback); 117 mRing.setColors(COLORS); 118 119 updateSizes(DEFAULT); 120 setupAnimators(); 121 } 122 123 private void setSizeParameters(double progressCircleWidth, double progressCircleHeight, 124 double centerRadius, double strokeWidth, float arrowWidth, float arrowHeight) { 125 final Ring ring = mRing; 126 final DisplayMetrics metrics = mResources.getDisplayMetrics(); 127 final float screenDensity = metrics.density; 128 129 mWidth = progressCircleWidth * screenDensity; 130 mHeight = progressCircleHeight * screenDensity; 131 ring.setStrokeWidth((float) strokeWidth * screenDensity); 132 ring.setCenterRadius(centerRadius * screenDensity); 133 ring.setColorIndex(0); 134 ring.setArrowDimensions(arrowWidth * screenDensity, arrowHeight * screenDensity); 135 ring.setInsets((int) mWidth, (int) mHeight); 136 } 137 138 /** 139 * Set the overall size for the progress spinner. This updates the radius 140 * and stroke width of the ring. 141 * 142 * @param size One of {@link MaterialProgressDrawable.LARGE} or 143 * {@link MaterialProgressDrawable.DEFAULT} 144 */ 145 public void updateSizes(@ProgressDrawableSize int size) { 146 if (size == LARGE) { 147 setSizeParameters(CIRCLE_DIAMETER_LARGE, CIRCLE_DIAMETER_LARGE, CENTER_RADIUS_LARGE, 148 STROKE_WIDTH_LARGE, ARROW_WIDTH_LARGE, ARROW_HEIGHT_LARGE); 149 } else { 150 setSizeParameters(CIRCLE_DIAMETER, CIRCLE_DIAMETER, CENTER_RADIUS, STROKE_WIDTH, 151 ARROW_WIDTH, ARROW_HEIGHT); 152 } 153 } 154 155 /** 156 * @param show Set to true to display the arrowhead on the progress spinner. 157 */ 158 public void showArrow(boolean show) { 159 mRing.setShowArrow(show); 160 } 161 162 /** 163 * @param scale Set the scale of the arrowhead for the spinner. 164 */ 165 public void setArrowScale(float scale) { 166 mRing.setArrowScale(scale); 167 } 168 169 /** 170 * Set the start and end trim for the progress spinner arc. 171 * 172 * @param startAngle start angle 173 * @param endAngle end angle 174 */ 175 public void setStartEndTrim(float startAngle, float endAngle) { 176 mRing.setStartTrim(startAngle); 177 mRing.setEndTrim(endAngle); 178 } 179 180 /** 181 * Set the amount of rotation to apply to the progress spinner. 182 * 183 * @param rotation Rotation is from [0..1] 184 */ 185 public void setProgressRotation(float rotation) { 186 mRing.setRotation(rotation); 187 } 188 189 /** 190 * Update the background color of the circle image view. 191 */ 192 public void setBackgroundColor(int color) { 193 mRing.setBackgroundColor(color); 194 } 195 196 /** 197 * Set the colors used in the progress animation from color resources. 198 * The first color will also be the color of the bar that grows in response 199 * to a user swipe gesture. 200 * 201 * @param colors 202 */ 203 public void setColorSchemeColors(int... colors) { 204 mRing.setColors(colors); 205 mRing.setColorIndex(0); 206 } 207 208 @Override 209 public int getIntrinsicHeight() { 210 return (int) mHeight; 211 } 212 213 @Override 214 public int getIntrinsicWidth() { 215 return (int) mWidth; 216 } 217 218 @Override 219 public void draw(Canvas c) { 220 final Rect bounds = getBounds(); 221 final int saveCount = c.save(); 222 c.rotate(mRotation, bounds.exactCenterX(), bounds.exactCenterY()); 223 mRing.draw(c, bounds); 224 c.restoreToCount(saveCount); 225 } 226 227 @Override 228 public void setAlpha(int alpha) { 229 mRing.setAlpha(alpha); 230 } 231 232 public int getAlpha() { 233 return mRing.getAlpha(); 234 } 235 236 @Override 237 public void setColorFilter(ColorFilter colorFilter) { 238 mRing.setColorFilter(colorFilter); 239 } 240 241 @SuppressWarnings("unused") 242 void setRotation(float rotation) { 243 mRotation = rotation; 244 invalidateSelf(); 245 } 246 247 @SuppressWarnings("unused") 248 private float getRotation() { 249 return mRotation; 250 } 251 252 @Override 253 public int getOpacity() { 254 return PixelFormat.TRANSLUCENT; 255 } 256 257 @Override 258 public boolean isRunning() { 259 final ArrayList<Animation> animators = mAnimators; 260 final int N = animators.size(); 261 for (int i = 0; i < N; i++) { 262 final Animation animator = animators.get(i); 263 if (animator.hasStarted() && !animator.hasEnded()) { 264 return true; 265 } 266 } 267 return false; 268 } 269 270 @Override 271 public void start() { 272 mStopped = false; 273 mAnimation.reset(); 274 mRing.storeOriginals(); 275 // Already showing some part of the ring 276 if (mRing.getEndTrim() != mRing.getStartTrim()) { 277 mParent.startAnimation(mFinishAnimation); 278 } else { 279 mRing.setColorIndex(0); 280 mRing.resetOriginals(); 281 mParent.startAnimation(mAnimation); 282 } 283 } 284 285 @Override 286 public void stop() { 287 mStopped = true; 288 mParent.clearAnimation(); 289 setRotation(0); 290 mRing.setShowArrow(false); 291 mRing.setColorIndex(0); 292 mRing.resetOriginals(); 293 } 294 295 private void setupAnimators() { 296 final Ring ring = mRing; 297 final Animation finishRingAnimation = new Animation() { 298 public void applyTransformation(float interpolatedTime, Transformation t) { 299 // shrink back down and complete a full rotation before starting other circles 300 // Rotation goes between [0..1]. 301 float targetRotation = (float) (Math.floor(ring.getStartingRotation() 302 / MAX_PROGRESS_ARC) + 1f); 303 final float startTrim = ring.getStartingStartTrim() 304 + (ring.getStartingEndTrim() - ring.getStartingStartTrim()) 305 * interpolatedTime; 306 ring.setStartTrim(startTrim); 307 final float rotation = ring.getStartingRotation() 308 + ((targetRotation - ring.getStartingRotation()) * interpolatedTime); 309 ring.setRotation(rotation); 310 } 311 }; 312 finishRingAnimation.setInterpolator(EASE_INTERPOLATOR); 313 finishRingAnimation.setDuration(ANIMATION_DURATION/2); 314 finishRingAnimation.setAnimationListener(new Animation.AnimationListener() { 315 316 @Override 317 public void onAnimationStart(Animation animation) { 318 } 319 320 @Override 321 public void onAnimationEnd(Animation animation) { 322 ring.goToNextColor(); 323 ring.storeOriginals(); 324 ring.setShowArrow(false); 325 if (!mStopped) { 326 mParent.startAnimation(mAnimation); 327 } 328 } 329 330 @Override 331 public void onAnimationRepeat(Animation animation) { 332 } 333 }); 334 final Animation animation = new Animation() { 335 @Override 336 public void applyTransformation(float interpolatedTime, Transformation t) { 337 // The minProgressArc is calculated from 0 to create an angle that 338 // matches the stroke width. 339 final float minProgressArc = (float) Math.toRadians(ring.getStrokeWidth() 340 / (2 * Math.PI * ring.getCenterRadius())); 341 final float startingEndTrim = ring.getStartingEndTrim(); 342 final float startingTrim = ring.getStartingStartTrim(); 343 final float startingRotation = ring.getStartingRotation(); 344 345 // Offset the minProgressArc to where the endTrim is located. 346 final float minArc = MAX_PROGRESS_ARC - minProgressArc; 347 final float endTrim = startingEndTrim 348 + (minArc * START_CURVE_INTERPOLATOR.getInterpolation(interpolatedTime)); 349 ring.setEndTrim(endTrim); 350 351 final float startTrim = startingTrim 352 + (MAX_PROGRESS_ARC * END_CURVE_INTERPOLATOR 353 .getInterpolation(interpolatedTime)); 354 ring.setStartTrim(startTrim); 355 356 final float rotation = startingRotation + (0.25f * interpolatedTime); 357 ring.setRotation(rotation); 358 359 float groupRotation = ((720.0f / NUM_POINTS) * interpolatedTime) 360 + (720.0f * (mRotationCount / NUM_POINTS)); 361 setRotation(groupRotation); 362 } 363 }; 364 animation.setRepeatCount(Animation.INFINITE); 365 animation.setRepeatMode(Animation.RESTART); 366 animation.setInterpolator(LINEAR_INTERPOLATOR); 367 animation.setDuration(ANIMATION_DURATION); 368 animation.setAnimationListener(new Animation.AnimationListener() { 369 370 @Override 371 public void onAnimationStart(Animation animation) { 372 mRotationCount = 0; 373 } 374 375 @Override 376 public void onAnimationEnd(Animation animation) { 377 // do nothing 378 } 379 380 @Override 381 public void onAnimationRepeat(Animation animation) { 382 ring.storeOriginals(); 383 ring.goToNextColor(); 384 ring.setStartTrim(ring.getEndTrim()); 385 mRotationCount = (mRotationCount + 1) % (NUM_POINTS); 386 } 387 }); 388 mFinishAnimation = finishRingAnimation; 389 mAnimation = animation; 390 } 391 392 private final Callback mCallback = new Callback() { 393 @Override 394 public void invalidateDrawable(Drawable d) { 395 invalidateSelf(); 396 } 397 398 @Override 399 public void scheduleDrawable(Drawable d, Runnable what, long when) { 400 scheduleSelf(what, when); 401 } 402 403 @Override 404 public void unscheduleDrawable(Drawable d, Runnable what) { 405 unscheduleSelf(what); 406 } 407 }; 408 409 private static class Ring { 410 private final RectF mTempBounds = new RectF(); 411 private final Paint mPaint = new Paint(); 412 private final Paint mArrowPaint = new Paint(); 413 414 private final Callback mCallback; 415 416 private float mStartTrim = 0.0f; 417 private float mEndTrim = 0.0f; 418 private float mRotation = 0.0f; 419 private float mStrokeWidth = 5.0f; 420 private float mStrokeInset = 2.5f; 421 422 private int[] mColors; 423 // mColorIndex represents the offset into the available mColors that the 424 // progress circle should currently display. As the progress circle is 425 // animating, the mColorIndex moves by one to the next available color. 426 private int mColorIndex; 427 private float mStartingStartTrim; 428 private float mStartingEndTrim; 429 private float mStartingRotation; 430 private boolean mShowArrow; 431 private Path mArrow; 432 private float mArrowScale; 433 private double mRingCenterRadius; 434 private int mArrowWidth; 435 private int mArrowHeight; 436 private int mAlpha; 437 private final Paint mCirclePaint = new Paint(); 438 private int mBackgroundColor; 439 440 public Ring(Callback callback) { 441 mCallback = callback; 442 443 mPaint.setStrokeCap(Paint.Cap.SQUARE); 444 mPaint.setAntiAlias(true); 445 mPaint.setStyle(Style.STROKE); 446 447 mArrowPaint.setStyle(Paint.Style.FILL); 448 mArrowPaint.setAntiAlias(true); 449 } 450 451 public void setBackgroundColor(int color) { 452 mBackgroundColor = color; 453 } 454 455 /** 456 * Set the dimensions of the arrowhead. 457 * 458 * @param width Width of the hypotenuse of the arrow head 459 * @param height Height of the arrow point 460 */ 461 public void setArrowDimensions(float width, float height) { 462 mArrowWidth = (int) width; 463 mArrowHeight = (int) height; 464 } 465 466 /** 467 * Draw the progress spinner 468 */ 469 public void draw(Canvas c, Rect bounds) { 470 final RectF arcBounds = mTempBounds; 471 arcBounds.set(bounds); 472 arcBounds.inset(mStrokeInset, mStrokeInset); 473 474 final float startAngle = (mStartTrim + mRotation) * 360; 475 final float endAngle = (mEndTrim + mRotation) * 360; 476 float sweepAngle = endAngle - startAngle; 477 478 mPaint.setColor(mColors[mColorIndex]); 479 c.drawArc(arcBounds, startAngle, sweepAngle, false, mPaint); 480 481 drawTriangle(c, startAngle, sweepAngle, bounds); 482 483 if (mAlpha < 255) { 484 mCirclePaint.setColor(mBackgroundColor); 485 mCirclePaint.setAlpha(255 - mAlpha); 486 c.drawCircle(bounds.exactCenterX(), bounds.exactCenterY(), bounds.width() / 2, 487 mCirclePaint); 488 } 489 } 490 491 private void drawTriangle(Canvas c, float startAngle, float sweepAngle, Rect bounds) { 492 if (mShowArrow) { 493 if (mArrow == null) { 494 mArrow = new android.graphics.Path(); 495 mArrow.setFillType(android.graphics.Path.FillType.EVEN_ODD); 496 } else { 497 mArrow.reset(); 498 } 499 500 // Adjust the position of the triangle so that it is inset as 501 // much as the arc, but also centered on the arc. 502 float inset = (int) mStrokeInset / 2 * mArrowScale; 503 float x = (float) (mRingCenterRadius * Math.cos(0) + bounds.exactCenterX()); 504 float y = (float) (mRingCenterRadius * Math.sin(0) + bounds.exactCenterY()); 505 506 // Update the path each time. This works around an issue in SKIA 507 // where concatenating a rotation matrix to a scale matrix 508 // ignored a starting negative rotation. This appears to have 509 // been fixed as of API 21. 510 mArrow.moveTo(0, 0); 511 mArrow.lineTo(mArrowWidth * mArrowScale, 0); 512 mArrow.lineTo((mArrowWidth * mArrowScale / 2), (mArrowHeight 513 * mArrowScale)); 514 mArrow.offset(x - inset, y); 515 mArrow.close(); 516 // draw a triangle 517 mArrowPaint.setColor(mColors[mColorIndex]); 518 c.rotate(startAngle + sweepAngle - ARROW_OFFSET_ANGLE, bounds.exactCenterX(), 519 bounds.exactCenterY()); 520 c.drawPath(mArrow, mArrowPaint); 521 } 522 } 523 524 /** 525 * Set the colors the progress spinner alternates between. 526 * 527 * @param colors Array of integers describing the colors. Must be non-<code>null</code>. 528 */ 529 public void setColors(@NonNull int[] colors) { 530 mColors = colors; 531 // if colors are reset, make sure to reset the color index as well 532 setColorIndex(0); 533 } 534 535 /** 536 * @param index Index into the color array of the color to display in 537 * the progress spinner. 538 */ 539 public void setColorIndex(int index) { 540 mColorIndex = index; 541 } 542 543 /** 544 * Proceed to the next available ring color. This will automatically 545 * wrap back to the beginning of colors. 546 */ 547 public void goToNextColor() { 548 mColorIndex = (mColorIndex + 1) % (mColors.length); 549 } 550 551 public void setColorFilter(ColorFilter filter) { 552 mPaint.setColorFilter(filter); 553 invalidateSelf(); 554 } 555 556 /** 557 * @param alpha Set the alpha of the progress spinner and associated arrowhead. 558 */ 559 public void setAlpha(int alpha) { 560 mAlpha = alpha; 561 } 562 563 /** 564 * @return Current alpha of the progress spinner and arrowhead. 565 */ 566 public int getAlpha() { 567 return mAlpha; 568 } 569 570 /** 571 * @param strokeWidth Set the stroke width of the progress spinner in pixels. 572 */ 573 public void setStrokeWidth(float strokeWidth) { 574 mStrokeWidth = strokeWidth; 575 mPaint.setStrokeWidth(strokeWidth); 576 invalidateSelf(); 577 } 578 579 @SuppressWarnings("unused") 580 public float getStrokeWidth() { 581 return mStrokeWidth; 582 } 583 584 @SuppressWarnings("unused") 585 public void setStartTrim(float startTrim) { 586 mStartTrim = startTrim; 587 invalidateSelf(); 588 } 589 590 @SuppressWarnings("unused") 591 public float getStartTrim() { 592 return mStartTrim; 593 } 594 595 public float getStartingStartTrim() { 596 return mStartingStartTrim; 597 } 598 599 public float getStartingEndTrim() { 600 return mStartingEndTrim; 601 } 602 603 @SuppressWarnings("unused") 604 public void setEndTrim(float endTrim) { 605 mEndTrim = endTrim; 606 invalidateSelf(); 607 } 608 609 @SuppressWarnings("unused") 610 public float getEndTrim() { 611 return mEndTrim; 612 } 613 614 @SuppressWarnings("unused") 615 public void setRotation(float rotation) { 616 mRotation = rotation; 617 invalidateSelf(); 618 } 619 620 @SuppressWarnings("unused") 621 public float getRotation() { 622 return mRotation; 623 } 624 625 public void setInsets(int width, int height) { 626 final float minEdge = (float) Math.min(width, height); 627 float insets; 628 if (mRingCenterRadius <= 0 || minEdge < 0) { 629 insets = (float) Math.ceil(mStrokeWidth / 2.0f); 630 } else { 631 insets = (float) (minEdge / 2.0f - mRingCenterRadius); 632 } 633 mStrokeInset = insets; 634 } 635 636 @SuppressWarnings("unused") 637 public float getInsets() { 638 return mStrokeInset; 639 } 640 641 /** 642 * @param centerRadius Inner radius in px of the circle the progress 643 * spinner arc traces. 644 */ 645 public void setCenterRadius(double centerRadius) { 646 mRingCenterRadius = centerRadius; 647 } 648 649 public double getCenterRadius() { 650 return mRingCenterRadius; 651 } 652 653 /** 654 * @param show Set to true to show the arrow head on the progress spinner. 655 */ 656 public void setShowArrow(boolean show) { 657 if (mShowArrow != show) { 658 mShowArrow = show; 659 invalidateSelf(); 660 } 661 } 662 663 /** 664 * @param scale Set the scale of the arrowhead for the spinner. 665 */ 666 public void setArrowScale(float scale) { 667 if (scale != mArrowScale) { 668 mArrowScale = scale; 669 invalidateSelf(); 670 } 671 } 672 673 /** 674 * @return The amount the progress spinner is currently rotated, between [0..1]. 675 */ 676 public float getStartingRotation() { 677 return mStartingRotation; 678 } 679 680 /** 681 * If the start / end trim are offset to begin with, store them so that 682 * animation starts from that offset. 683 */ 684 public void storeOriginals() { 685 mStartingStartTrim = mStartTrim; 686 mStartingEndTrim = mEndTrim; 687 mStartingRotation = mRotation; 688 } 689 690 /** 691 * Reset the progress spinner to default rotation, start and end angles. 692 */ 693 public void resetOriginals() { 694 mStartingStartTrim = 0; 695 mStartingEndTrim = 0; 696 mStartingRotation = 0; 697 setStartTrim(0); 698 setEndTrim(0); 699 setRotation(0); 700 } 701 702 private void invalidateSelf() { 703 mCallback.invalidateDrawable(null); 704 } 705 } 706 707 /** 708 * Squishes the interpolation curve into the second half of the animation. 709 */ 710 private static class EndCurveInterpolator extends AccelerateDecelerateInterpolator { 711 @Override 712 public float getInterpolation(float input) { 713 return super.getInterpolation(Math.max(0, (input - 0.5f) * 2.0f)); 714 } 715 } 716 717 /** 718 * Squishes the interpolation curve into the first half of the animation. 719 */ 720 private static class StartCurveInterpolator extends AccelerateDecelerateInterpolator { 721 @Override 722 public float getInterpolation(float input) { 723 return super.getInterpolation(Math.min(1, input * 2.0f)); 724 } 725 } 726} 727