MaterialProgressDrawable.java revision 12ffe36178df269e0c2d3b33f7de360e74c63f71
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.Cap; 31import android.graphics.Paint.Style; 32import android.graphics.PixelFormat; 33import android.graphics.Rect; 34import android.graphics.RectF; 35import android.graphics.drawable.Drawable; 36import android.graphics.drawable.Animatable; 37import android.util.DisplayMetrics; 38import android.view.View; 39import java.util.ArrayList; 40 41/** 42 * Fancy progress indicator for Material theme. 43 */ 44class MaterialProgressDrawable extends Drawable implements Animatable { 45 private static final Interpolator LINEAR_INTERPOLATOR = new LinearInterpolator(); 46 private static final Interpolator END_CURVE_INTERPOLATOR = new EndCurveInterpolator(); 47 private static final Interpolator START_CURVE_INTERPOLATOR = new StartCurveInterpolator(); 48 49 // Maps to ProgressBar.Large style 50 static final int LARGE = 0; 51 // Maps to ProgressBar default style 52 static final int DEFAULT = 1; 53 // Maps to ProgressBar.Small style 54 static final int SMALL = 2; 55 56 // Maps to ProgressBar default style 57 private static final int CIRCLE_DIAMETER = 48; 58 private static final int INNER_RADIUS = 19; 59 private static final int STROKE_WIDTH = 4; 60 61 // Maps to ProgressBar.Large style 62 private static final int CIRCLE_DIAMETER_LARGE = 76; 63 private static final float INNER_RADIUS_LARGE = 30.1f; 64 private static final float STROKE_WIDTH_LARGE = 6.3f; 65 66 // Maps to ProgressBar.Small style 67 private static final int CIRCLE_DIAMETER_SMALL = 16; 68 private static final float INNER_RADIUS_SMALL = 6.3f; 69 private static final float STROKE_WIDTH_SMALL = 1.3f; 70 71 private final int[] COLORS = new int[] { 72 Color.BLACK 73 }; 74 75 /** The duration of a single progress spin in milliseconds. */ 76 private static final int ANIMATION_DURATION = 1000 * 80 / 60; 77 78 /** The number of points in the progress "star". */ 79 private static final float NUM_POINTS = 5f; 80 81 /** The list of animators operating on this drawable. */ 82 private final ArrayList<Animation> mAnimators = new ArrayList<Animation>(); 83 84 /** The indicator ring, used to manage animation state. */ 85 private final Ring mRing; 86 87 /** Canvas rotation in degrees. */ 88 private float mRotation; 89 90 private Resources mResources; 91 private int mColorIndex; 92 private View mParent; 93 private Animation mAnimation; 94 private float mRotationCount; 95 private int[] mColors; 96 private double mWidth; 97 private double mHeight; 98 private double mInnerRadius; 99 private double mStrokeWidth; 100 private Animation mFinishAnimation; 101 102 public MaterialProgressDrawable(Context context, View parent) { 103 mParent = parent; 104 mResources = context.getResources(); 105 106 mRing = new Ring(mCallback); 107 mColors = COLORS; 108 mRing.setColors(mColors); 109 110 initialize(CIRCLE_DIAMETER, CIRCLE_DIAMETER, INNER_RADIUS, STROKE_WIDTH); 111 setupAnimators(); 112 } 113 114 private void initialize(double progressCircleWidth, double progressCircleHeight, 115 double innerRadius, double strokeWidth) { 116 final Ring ring = mRing; 117 final DisplayMetrics metrics = mResources.getDisplayMetrics(); 118 final float screenDensity = metrics.density; 119 120 mWidth = progressCircleWidth * screenDensity; 121 mHeight = progressCircleHeight * screenDensity; 122 mInnerRadius = innerRadius * screenDensity; 123 mStrokeWidth = strokeWidth * screenDensity; 124 ring.setStrokeWidth((float) mStrokeWidth); 125 126 final int color = mColors[0]; 127 ring.setColor(color); 128 129 final float minEdge = (float) Math.min(mWidth, mHeight); 130 if (mInnerRadius <= 0 || minEdge < 0) { 131 ring.setInsets((int) Math.ceil(mStrokeWidth / 2.0f)); 132 } else { 133 float insets = (float) (minEdge / 2.0f - mInnerRadius); 134 ring.setInsets(insets); 135 } 136 } 137 138 public void updateSizes(int size) { 139 final DisplayMetrics metrics = mResources.getDisplayMetrics(); 140 final float screenDensity = metrics.density; 141 int progressCircleWidth; 142 int progressCircleHeight; 143 float innerRadius; 144 float strokeWidth; 145 146 if (size == LARGE) { 147 progressCircleWidth = progressCircleHeight = CIRCLE_DIAMETER_LARGE; 148 innerRadius = INNER_RADIUS_LARGE; 149 strokeWidth = STROKE_WIDTH_LARGE; 150 } else if (size == SMALL) { 151 progressCircleWidth = progressCircleHeight = CIRCLE_DIAMETER_SMALL; 152 innerRadius = INNER_RADIUS_SMALL; 153 strokeWidth = STROKE_WIDTH_SMALL; 154 } else { 155 progressCircleWidth = progressCircleHeight = CIRCLE_DIAMETER; 156 innerRadius = INNER_RADIUS; 157 strokeWidth = STROKE_WIDTH; 158 } 159 mWidth = progressCircleWidth * screenDensity; 160 mHeight = progressCircleHeight * screenDensity; 161 mInnerRadius = innerRadius * screenDensity; 162 mStrokeWidth = strokeWidth * screenDensity; 163 } 164 165 public void setStartEndTrim(float s, float e) { 166 mRing.setStartTrim(s); 167 mRing.setEndTrim(e); 168 } 169 170 public void setProgressRotation(float r) { 171 mRing.setRotation(r); 172 } 173 174 /** 175 * Set the colors used in the progress animation from color resources. 176 * The first color will also be the color of the bar that grows in response 177 * to a user swipe gesture. 178 * 179 * @param colors 180 */ 181 public void setColorSchemeColors(int... colors) { 182 mColors = colors; 183 mRing.setColors(mColors); 184 } 185 186 @Override 187 public int getIntrinsicHeight() { 188 return (int) mHeight; 189 } 190 191 @Override 192 public int getIntrinsicWidth() { 193 return (int) mWidth; 194 } 195 196 @Override 197 public void draw(Canvas c) { 198 final Rect bounds = getBounds(); 199 final int saveCount = c.save(); 200 c.rotate(mRotation, bounds.exactCenterX(), bounds.exactCenterY()); 201 mRing.draw(c, bounds); 202 c.restoreToCount(saveCount); 203 } 204 205 @Override 206 public void setAlpha(int alpha) { 207 mRing.setAlpha(alpha); 208 } 209 210 public int getAlpha() { 211 return mRing.getAlpha(); 212 } 213 214 @Override 215 public void setColorFilter(ColorFilter colorFilter) { 216 mRing.setColorFilter(colorFilter); 217 } 218 219 @SuppressWarnings("unused") 220 private void setRotation(float rotation) { 221 mRotation = rotation; 222 invalidateSelf(); 223 } 224 225 @SuppressWarnings("unused") 226 private float getRotation() { 227 return mRotation; 228 } 229 230 @Override 231 public int getOpacity() { 232 return PixelFormat.TRANSLUCENT; 233 } 234 235 @Override 236 public boolean isRunning() { 237 final ArrayList<Animation> animators = mAnimators; 238 final int N = animators.size(); 239 for (int i = 0; i < N; i++) { 240 final Animation animator = animators.get(i); 241 if (animator.hasStarted() && !animator.hasEnded()) { 242 return true; 243 } 244 } 245 return false; 246 } 247 248 @Override 249 public void start() { 250 mAnimation.reset(); 251 mRing.storeOriginals(); 252 if (mRing.getStartingStartTrim() != 0) { 253 mParent.startAnimation(mFinishAnimation); 254 } else { 255 mColorIndex = 0; 256 mRing.setColorIndex(mColorIndex); 257 mRing.resetOriginals(); 258 mParent.startAnimation(mAnimation); 259 } 260 } 261 262 @Override 263 public void stop() { 264 mParent.clearAnimation(); 265 setRotation(0); 266 mColorIndex = 0; 267 mRing.setColorIndex(mColorIndex); 268 mRing.resetOriginals(); 269 } 270 271 private void setupAnimators() { 272 final Ring ring = mRing; 273 final Animation finishRingAnimation = new Animation() { 274 public void applyTransformation(float interpolatedTime, Transformation t) { 275 // shrink back down and complete a full roation before starting other circles 276 float targetRotation = (float) (Math.floor(ring.getStartingRotation() / .75f) + 1f); 277 final float startTrim = ring.getStartingEndTrim() 278 + (ring.getStartingStartTrim() - ring.getStartingEndTrim()) 279 * interpolatedTime; 280 ring.setEndTrim(startTrim); 281 final float rotation = ring.getStartingRotation() 282 + ((targetRotation - ring.getStartingRotation()) * interpolatedTime); 283 ring.setRotation(rotation); 284 } 285 }; 286 finishRingAnimation.setInterpolator(LINEAR_INTERPOLATOR); 287 finishRingAnimation.setDuration(ANIMATION_DURATION / 2); 288 finishRingAnimation.setAnimationListener(new Animation.AnimationListener() { 289 290 @Override 291 public void onAnimationStart(Animation animation) { 292 } 293 294 @Override 295 public void onAnimationEnd(Animation animation) { 296 mColorIndex = (mColorIndex + 1) % (mColors.length); 297 ring.setColorIndex(mColorIndex); 298 ring.resetOriginals(); 299 mParent.startAnimation(mAnimation); 300 } 301 302 @Override 303 public void onAnimationRepeat(Animation animation) { 304 } 305 }); 306 final Animation animation = new Animation() { 307 @Override 308 public void applyTransformation(float interpolatedTime, Transformation t) { 309 final float endTrim = 310 0.75f * START_CURVE_INTERPOLATOR 311 .getInterpolation(interpolatedTime); 312 ring.setEndTrim(endTrim); 313 final float startTrim = 0.75f * END_CURVE_INTERPOLATOR 314 .getInterpolation(interpolatedTime); 315 ring.setStartTrim(startTrim); 316 final float rotation = 0.25f * interpolatedTime; 317 ring.setRotation(rotation); 318 float groupRotation = ((720.0f / NUM_POINTS) * interpolatedTime) 319 + (720.0f * (mRotationCount / NUM_POINTS)); 320 setRotation(groupRotation); 321 } 322 }; 323 animation.setRepeatCount(Animation.INFINITE); 324 animation.setRepeatMode(Animation.RESTART); 325 animation.setInterpolator(LINEAR_INTERPOLATOR); 326 animation.setDuration(ANIMATION_DURATION); 327 animation.setAnimationListener(new Animation.AnimationListener() { 328 329 @Override 330 public void onAnimationStart(Animation animation) { 331 mRotationCount = 0; 332 } 333 334 @Override 335 public void onAnimationEnd(Animation animation) { 336 // do nothing 337 } 338 339 @Override 340 public void onAnimationRepeat(Animation animation) { 341 mColorIndex = (mColorIndex + 1) % (mColors.length); 342 ring.setColorIndex(mColorIndex); 343 ring.resetOriginals(); 344 mRotationCount = (mRotationCount + 1) % (NUM_POINTS); 345 } 346 }); 347 mFinishAnimation = finishRingAnimation; 348 mAnimation = animation; 349 } 350 351 private final Callback mCallback = new Callback() { 352 @Override 353 public void invalidateDrawable(Drawable d) { 354 invalidateSelf(); 355 } 356 357 @Override 358 public void scheduleDrawable(Drawable d, Runnable what, long when) { 359 scheduleSelf(what, when); 360 } 361 362 @Override 363 public void unscheduleDrawable(Drawable d, Runnable what) { 364 unscheduleSelf(what); 365 } 366 }; 367 368 private static class Ring { 369 private final RectF mTempBounds = new RectF(); 370 private final Paint mPaint = new Paint(); 371 372 private final Callback mCallback; 373 374 private float mStartTrim = 0.0f; 375 private float mEndTrim = 0.0f; 376 private float mRotation = 0.0f; 377 private float mStrokeWidth = 5.0f; 378 private float mStrokeInset = 2.5f; 379 380 private int mAlpha = 0xFF; 381 private int mColor = Color.BLACK; 382 private int[] mColors; 383 private int mColorIndex; 384 private float mStartingStartTrim; 385 private float mStartingEndTrim; 386 private float mStartingRotation; 387 388 public Ring(Callback callback) { 389 mCallback = callback; 390 391 mPaint.setStrokeCap(Cap.ROUND); 392 mPaint.setAntiAlias(true); 393 mPaint.setStyle(Style.STROKE); 394 } 395 396 public float getStartingRotation() { 397 return mStartingRotation; 398 } 399 400 /** 401 * If the start / end trim are offset to begin with, store them so that 402 * animation starts from that offset. 403 */ 404 public void storeOriginals() { 405 mStartingStartTrim = mStartTrim; 406 mStartingEndTrim = mEndTrim; 407 mStartingRotation = mRotation; 408 } 409 410 public void resetOriginals() { 411 mStartingStartTrim = 0; 412 mStartingEndTrim = 0; 413 mStartingRotation = 0; 414 setStartTrim(0); 415 setEndTrim(0); 416 setRotation(0); 417 } 418 419 public void draw(Canvas c, Rect bounds) { 420 final RectF arcBounds = mTempBounds; 421 arcBounds.set(bounds); 422 arcBounds.inset(mStrokeInset, mStrokeInset); 423 424 final float startAngle = (mStartTrim + mRotation) * 360; 425 final float endAngle = (mEndTrim + mRotation) * 360; 426 float sweepAngle = endAngle - startAngle; 427 428 // Ensure the sweep angle isn't too small to draw. 429 final float diameter = Math.min(arcBounds.width(), arcBounds.height()); 430 final float minAngle = (float) (360.0 / (diameter * Math.PI)); 431 if (sweepAngle < minAngle && sweepAngle > -minAngle) { 432 sweepAngle = Math.signum(sweepAngle) * minAngle; 433 } 434 mPaint.setColor(mColors[mColorIndex]); 435 c.drawArc(arcBounds, startAngle, sweepAngle, false, mPaint); 436 } 437 438 public void setColors(int[] colors) { 439 mColors = colors; 440 } 441 442 public void setColorIndex(int index) { 443 mColorIndex = index; 444 } 445 446 public void setColorFilter(ColorFilter filter) { 447 mPaint.setColorFilter(filter); 448 invalidateSelf(); 449 } 450 451 public ColorFilter getColorFilter() { 452 return mPaint.getColorFilter(); 453 } 454 455 public void setAlpha(int alpha) { 456 mAlpha = alpha; 457 mPaint.setColor(mColor & 0xFFFFFF | alpha << 24); 458 invalidateSelf(); 459 } 460 461 public int getAlpha() { 462 return mAlpha; 463 } 464 465 public void setColor(int color) { 466 mColor = color; 467 mPaint.setColor(color & 0xFFFFFF | mAlpha << 24); 468 invalidateSelf(); 469 } 470 471 public int getColor() { 472 return mColor; 473 } 474 475 public void setStrokeWidth(float strokeWidth) { 476 mStrokeWidth = strokeWidth; 477 mPaint.setStrokeWidth(strokeWidth); 478 invalidateSelf(); 479 } 480 481 @SuppressWarnings("unused") 482 public float getStrokeWidth() { 483 return mStrokeWidth; 484 } 485 486 @SuppressWarnings("unused") 487 public void setStartTrim(float startTrim) { 488 mStartTrim = startTrim; 489 invalidateSelf(); 490 } 491 492 @SuppressWarnings("unused") 493 public float getStartTrim() { 494 return mStartTrim; 495 } 496 497 public float getStartingStartTrim() { 498 return mStartingStartTrim; 499 } 500 501 public float getStartingEndTrim() { 502 return mStartingEndTrim; 503 } 504 505 @SuppressWarnings("unused") 506 public void setEndTrim(float endTrim) { 507 mEndTrim = endTrim; 508 invalidateSelf(); 509 } 510 511 @SuppressWarnings("unused") 512 public float getEndTrim() { 513 return mEndTrim; 514 } 515 516 @SuppressWarnings("unused") 517 public void setRotation(float rotation) { 518 mRotation = rotation; 519 invalidateSelf(); 520 } 521 522 @SuppressWarnings("unused") 523 public float getRotation() { 524 return mRotation; 525 } 526 527 public void setInsets(float insets) { 528 mStrokeInset = insets; 529 } 530 531 @SuppressWarnings("unused") 532 public float getInsets() { 533 return mStrokeInset; 534 } 535 536 private void invalidateSelf() { 537 mCallback.invalidateDrawable(null); 538 } 539 } 540 541 /** 542 * Squishes the interpolation curve into the second half of the animation. 543 */ 544 private static class EndCurveInterpolator extends AccelerateDecelerateInterpolator { 545 @Override 546 public float getInterpolation(float input) { 547 return super.getInterpolation(Math.max(0, (input - 0.5f) * 2.0f)); 548 } 549 } 550 551 /** 552 * Squishes the interpolation curve into the first half of the animation. 553 */ 554 private static class StartCurveInterpolator extends AccelerateDecelerateInterpolator { 555 @Override 556 public float getInterpolation(float input) { 557 return super.getInterpolation(Math.min(1, input * 2.0f)); 558 } 559 } 560} 561