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