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