MaterialProgressDrawable.java revision 9a1b1c1316c08c832139a06bebce398e83263ac8
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 ring.setArrowScale(1 - interpolatedTime); 311 } 312 }; 313 finishRingAnimation.setInterpolator(EASE_INTERPOLATOR); 314 finishRingAnimation.setDuration(ANIMATION_DURATION/2); 315 finishRingAnimation.setAnimationListener(new Animation.AnimationListener() { 316 317 @Override 318 public void onAnimationStart(Animation animation) { 319 } 320 321 @Override 322 public void onAnimationEnd(Animation animation) { 323 ring.goToNextColor(); 324 ring.storeOriginals(); 325 ring.setShowArrow(false); 326 if (!mStopped) { 327 mParent.startAnimation(mAnimation); 328 } 329 } 330 331 @Override 332 public void onAnimationRepeat(Animation animation) { 333 } 334 }); 335 final Animation animation = new Animation() { 336 @Override 337 public void applyTransformation(float interpolatedTime, Transformation t) { 338 // The minProgressArc is calculated from 0 to create an angle that 339 // matches the stroke width. 340 final float minProgressArc = (float) Math.toRadians(ring.getStrokeWidth() 341 / (2 * Math.PI * ring.getCenterRadius())); 342 final float startingEndTrim = ring.getStartingEndTrim(); 343 final float startingTrim = ring.getStartingStartTrim(); 344 final float startingRotation = ring.getStartingRotation(); 345 346 // Offset the minProgressArc to where the endTrim is located. 347 final float minArc = MAX_PROGRESS_ARC - minProgressArc; 348 final float endTrim = startingEndTrim 349 + (minArc * START_CURVE_INTERPOLATOR.getInterpolation(interpolatedTime)); 350 ring.setEndTrim(endTrim); 351 352 final float startTrim = startingTrim 353 + (MAX_PROGRESS_ARC * END_CURVE_INTERPOLATOR 354 .getInterpolation(interpolatedTime)); 355 ring.setStartTrim(startTrim); 356 357 final float rotation = startingRotation + (0.25f * interpolatedTime); 358 ring.setRotation(rotation); 359 360 float groupRotation = ((720.0f / NUM_POINTS) * interpolatedTime) 361 + (720.0f * (mRotationCount / NUM_POINTS)); 362 setRotation(groupRotation); 363 } 364 }; 365 animation.setRepeatCount(Animation.INFINITE); 366 animation.setRepeatMode(Animation.RESTART); 367 animation.setInterpolator(LINEAR_INTERPOLATOR); 368 animation.setDuration(ANIMATION_DURATION); 369 animation.setAnimationListener(new Animation.AnimationListener() { 370 371 @Override 372 public void onAnimationStart(Animation animation) { 373 mRotationCount = 0; 374 } 375 376 @Override 377 public void onAnimationEnd(Animation animation) { 378 // do nothing 379 } 380 381 @Override 382 public void onAnimationRepeat(Animation animation) { 383 ring.storeOriginals(); 384 ring.goToNextColor(); 385 ring.setStartTrim(ring.getEndTrim()); 386 mRotationCount = (mRotationCount + 1) % (NUM_POINTS); 387 } 388 }); 389 mFinishAnimation = finishRingAnimation; 390 mAnimation = animation; 391 } 392 393 private final Callback mCallback = new Callback() { 394 @Override 395 public void invalidateDrawable(Drawable d) { 396 invalidateSelf(); 397 } 398 399 @Override 400 public void scheduleDrawable(Drawable d, Runnable what, long when) { 401 scheduleSelf(what, when); 402 } 403 404 @Override 405 public void unscheduleDrawable(Drawable d, Runnable what) { 406 unscheduleSelf(what); 407 } 408 }; 409 410 private static class Ring { 411 private final RectF mTempBounds = new RectF(); 412 private final Paint mPaint = new Paint(); 413 private final Paint mArrowPaint = new Paint(); 414 415 private final Callback mCallback; 416 417 private float mStartTrim = 0.0f; 418 private float mEndTrim = 0.0f; 419 private float mRotation = 0.0f; 420 private float mStrokeWidth = 5.0f; 421 private float mStrokeInset = 2.5f; 422 423 private int[] mColors; 424 // mColorIndex represents the offset into the available mColors that the 425 // progress circle should currently display. As the progress circle is 426 // animating, the mColorIndex moves by one to the next available color. 427 private int mColorIndex; 428 private float mStartingStartTrim; 429 private float mStartingEndTrim; 430 private float mStartingRotation; 431 private boolean mShowArrow; 432 private Path mArrow; 433 private float mArrowScale; 434 private double mRingCenterRadius; 435 private int mArrowWidth; 436 private int mArrowHeight; 437 private int mAlpha; 438 private final Paint mCirclePaint = new Paint(); 439 private int mBackgroundColor; 440 441 public Ring(Callback callback) { 442 mCallback = callback; 443 444 mPaint.setStrokeCap(Paint.Cap.SQUARE); 445 mPaint.setAntiAlias(true); 446 mPaint.setStyle(Style.STROKE); 447 448 mArrowPaint.setStyle(Paint.Style.FILL); 449 mArrowPaint.setAntiAlias(true); 450 } 451 452 public void setBackgroundColor(int color) { 453 mBackgroundColor = color; 454 } 455 456 /** 457 * Set the dimensions of the arrowhead. 458 * 459 * @param width Width of the hypotenuse of the arrow head 460 * @param height Height of the arrow point 461 */ 462 public void setArrowDimensions(float width, float height) { 463 mArrowWidth = (int) width; 464 mArrowHeight = (int) height; 465 } 466 467 /** 468 * Draw the progress spinner 469 */ 470 public void draw(Canvas c, Rect bounds) { 471 final RectF arcBounds = mTempBounds; 472 arcBounds.set(bounds); 473 arcBounds.inset(mStrokeInset, mStrokeInset); 474 475 final float startAngle = (mStartTrim + mRotation) * 360; 476 final float endAngle = (mEndTrim + mRotation) * 360; 477 float sweepAngle = endAngle - startAngle; 478 479 mPaint.setColor(mColors[mColorIndex]); 480 c.drawArc(arcBounds, startAngle, sweepAngle, false, mPaint); 481 482 drawTriangle(c, startAngle, sweepAngle, bounds); 483 484 if (mAlpha < 255) { 485 mCirclePaint.setColor(mBackgroundColor); 486 mCirclePaint.setAlpha(255 - mAlpha); 487 c.drawCircle(bounds.exactCenterX(), bounds.exactCenterY(), bounds.width() / 2, 488 mCirclePaint); 489 } 490 } 491 492 private void drawTriangle(Canvas c, float startAngle, float sweepAngle, Rect bounds) { 493 if (mShowArrow) { 494 if (mArrow == null) { 495 mArrow = new android.graphics.Path(); 496 mArrow.setFillType(android.graphics.Path.FillType.EVEN_ODD); 497 } else { 498 mArrow.reset(); 499 } 500 501 // Adjust the position of the triangle so that it is inset as 502 // much as the arc, but also centered on the arc. 503 float inset = (int) mStrokeInset / 2 * mArrowScale; 504 float x = (float) (mRingCenterRadius * Math.cos(0) + bounds.exactCenterX()); 505 float y = (float) (mRingCenterRadius * Math.sin(0) + bounds.exactCenterY()); 506 507 // Update the path each time. This works around an issue in SKIA 508 // where concatenating a rotation matrix to a scale matrix 509 // ignored a starting negative rotation. This appears to have 510 // been fixed as of API 21. 511 mArrow.moveTo(0, 0); 512 mArrow.lineTo(mArrowWidth * mArrowScale, 0); 513 mArrow.lineTo((mArrowWidth * mArrowScale / 2), (mArrowHeight 514 * mArrowScale)); 515 mArrow.offset(x - inset, y); 516 mArrow.close(); 517 // draw a triangle 518 mArrowPaint.setColor(mColors[mColorIndex]); 519 c.rotate(startAngle + sweepAngle - ARROW_OFFSET_ANGLE, bounds.exactCenterX(), 520 bounds.exactCenterY()); 521 c.drawPath(mArrow, mArrowPaint); 522 } 523 } 524 525 /** 526 * Set the colors the progress spinner alternates between. 527 * 528 * @param colors Array of integers describing the colors. Must be non-<code>null</code>. 529 */ 530 public void setColors(@NonNull int[] colors) { 531 mColors = colors; 532 // if colors are reset, make sure to reset the color index as well 533 setColorIndex(0); 534 } 535 536 /** 537 * @param index Index into the color array of the color to display in 538 * the progress spinner. 539 */ 540 public void setColorIndex(int index) { 541 mColorIndex = index; 542 } 543 544 /** 545 * Proceed to the next available ring color. This will automatically 546 * wrap back to the beginning of colors. 547 */ 548 public void goToNextColor() { 549 mColorIndex = (mColorIndex + 1) % (mColors.length); 550 } 551 552 public void setColorFilter(ColorFilter filter) { 553 mPaint.setColorFilter(filter); 554 invalidateSelf(); 555 } 556 557 /** 558 * @param alpha Set the alpha of the progress spinner and associated arrowhead. 559 */ 560 public void setAlpha(int alpha) { 561 mAlpha = alpha; 562 } 563 564 /** 565 * @return Current alpha of the progress spinner and arrowhead. 566 */ 567 public int getAlpha() { 568 return mAlpha; 569 } 570 571 /** 572 * @param strokeWidth Set the stroke width of the progress spinner in pixels. 573 */ 574 public void setStrokeWidth(float strokeWidth) { 575 mStrokeWidth = strokeWidth; 576 mPaint.setStrokeWidth(strokeWidth); 577 invalidateSelf(); 578 } 579 580 @SuppressWarnings("unused") 581 public float getStrokeWidth() { 582 return mStrokeWidth; 583 } 584 585 @SuppressWarnings("unused") 586 public void setStartTrim(float startTrim) { 587 mStartTrim = startTrim; 588 invalidateSelf(); 589 } 590 591 @SuppressWarnings("unused") 592 public float getStartTrim() { 593 return mStartTrim; 594 } 595 596 public float getStartingStartTrim() { 597 return mStartingStartTrim; 598 } 599 600 public float getStartingEndTrim() { 601 return mStartingEndTrim; 602 } 603 604 @SuppressWarnings("unused") 605 public void setEndTrim(float endTrim) { 606 mEndTrim = endTrim; 607 invalidateSelf(); 608 } 609 610 @SuppressWarnings("unused") 611 public float getEndTrim() { 612 return mEndTrim; 613 } 614 615 @SuppressWarnings("unused") 616 public void setRotation(float rotation) { 617 mRotation = rotation; 618 invalidateSelf(); 619 } 620 621 @SuppressWarnings("unused") 622 public float getRotation() { 623 return mRotation; 624 } 625 626 public void setInsets(int width, int height) { 627 final float minEdge = (float) Math.min(width, height); 628 float insets; 629 if (mRingCenterRadius <= 0 || minEdge < 0) { 630 insets = (float) Math.ceil(mStrokeWidth / 2.0f); 631 } else { 632 insets = (float) (minEdge / 2.0f - mRingCenterRadius); 633 } 634 mStrokeInset = insets; 635 } 636 637 @SuppressWarnings("unused") 638 public float getInsets() { 639 return mStrokeInset; 640 } 641 642 /** 643 * @param centerRadius Inner radius in px of the circle the progress 644 * spinner arc traces. 645 */ 646 public void setCenterRadius(double centerRadius) { 647 mRingCenterRadius = centerRadius; 648 } 649 650 public double getCenterRadius() { 651 return mRingCenterRadius; 652 } 653 654 /** 655 * @param show Set to true to show the arrow head on the progress spinner. 656 */ 657 public void setShowArrow(boolean show) { 658 if (mShowArrow != show) { 659 mShowArrow = show; 660 invalidateSelf(); 661 } 662 } 663 664 /** 665 * @param scale Set the scale of the arrowhead for the spinner. 666 */ 667 public void setArrowScale(float scale) { 668 if (scale != mArrowScale) { 669 mArrowScale = scale; 670 invalidateSelf(); 671 } 672 } 673 674 /** 675 * @return The amount the progress spinner is currently rotated, between [0..1]. 676 */ 677 public float getStartingRotation() { 678 return mStartingRotation; 679 } 680 681 /** 682 * If the start / end trim are offset to begin with, store them so that 683 * animation starts from that offset. 684 */ 685 public void storeOriginals() { 686 mStartingStartTrim = mStartTrim; 687 mStartingEndTrim = mEndTrim; 688 mStartingRotation = mRotation; 689 } 690 691 /** 692 * Reset the progress spinner to default rotation, start and end angles. 693 */ 694 public void resetOriginals() { 695 mStartingStartTrim = 0; 696 mStartingEndTrim = 0; 697 mStartingRotation = 0; 698 setStartTrim(0); 699 setEndTrim(0); 700 setRotation(0); 701 } 702 703 private void invalidateSelf() { 704 mCallback.invalidateDrawable(null); 705 } 706 } 707 708 /** 709 * Squishes the interpolation curve into the second half of the animation. 710 */ 711 private static class EndCurveInterpolator extends AccelerateDecelerateInterpolator { 712 @Override 713 public float getInterpolation(float input) { 714 return super.getInterpolation(Math.max(0, (input - 0.5f) * 2.0f)); 715 } 716 } 717 718 /** 719 * Squishes the interpolation curve into the first half of the animation. 720 */ 721 private static class StartCurveInterpolator extends AccelerateDecelerateInterpolator { 722 @Override 723 public float getInterpolation(float input) { 724 return super.getInterpolation(Math.min(1, input * 2.0f)); 725 } 726 } 727} 728