1 /* 2 * Copyright (C) 2017 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 android.support.wear.widget; 18 19 import android.animation.ObjectAnimator; 20 import android.animation.TimeInterpolator; 21 import android.animation.ValueAnimator; 22 import android.annotation.TargetApi; 23 import android.graphics.Canvas; 24 import android.graphics.ColorFilter; 25 import android.graphics.Paint; 26 import android.graphics.PixelFormat; 27 import android.graphics.RectF; 28 import android.graphics.drawable.Drawable; 29 import android.os.Build; 30 import android.support.annotation.RestrictTo; 31 import android.support.annotation.RestrictTo.Scope; 32 import android.util.Property; 33 import android.view.animation.LinearInterpolator; 34 35 /** 36 * Drawable for showing an indeterminate progress indicator. 37 * 38 * @hide 39 */ 40 @TargetApi(Build.VERSION_CODES.KITKAT_WATCH) 41 @RestrictTo(Scope.LIBRARY_GROUP) 42 class ProgressDrawable extends Drawable { 43 44 private static final Property<ProgressDrawable, Integer> LEVEL = 45 new Property<ProgressDrawable, Integer>(Integer.class, "level") { 46 @Override 47 public Integer get(ProgressDrawable drawable) { 48 return drawable.getLevel(); 49 } 50 51 @Override 52 public void set(ProgressDrawable drawable, Integer value) { 53 drawable.setLevel(value); 54 drawable.invalidateSelf(); 55 } 56 }; 57 /** 58 * Max level for a level drawable, as specified in developer docs for {@link Drawable}. 59 */ 60 private static final int MAX_LEVEL = 10000; 61 62 /** 63 * How many different sections are there, five gives us the material style star. * 64 */ 65 private static final int NUMBER_OF_SEGMENTS = 5; 66 67 private static final int LEVELS_PER_SEGMENT = MAX_LEVEL / NUMBER_OF_SEGMENTS; 68 private static final float STARTING_ANGLE = -90f; 69 private static final long ANIMATION_DURATION = 6000; 70 private static final int FULL_CIRCLE = 360; 71 private static final int MAX_SWEEP = 306; 72 private static final int CORRECTION_ANGLE = FULL_CIRCLE - MAX_SWEEP; 73 /** 74 * How far through each cycle does the bar stop growing and start shrinking, half way. * 75 */ 76 private static final float GROW_SHRINK_RATIO = 0.5f; 77 // TODO: replace this with BakedBezierInterpolator when its available in support library. 78 private static final TimeInterpolator sInterpolator = BezierSCurveInterpolator.INSTANCE; 79 80 private final RectF mInnerCircleBounds = new RectF(); 81 private final Paint mPaint = new Paint(); 82 private final ObjectAnimator mAnimator; 83 private float mCircleBorderWidth; 84 private int mCircleBorderColor; 85 ProgressDrawable()86 ProgressDrawable() { 87 mPaint.setAntiAlias(true); 88 mPaint.setStyle(Paint.Style.STROKE); 89 mAnimator = ObjectAnimator.ofInt(this, LEVEL, 0, MAX_LEVEL); 90 mAnimator.setRepeatCount(ValueAnimator.INFINITE); 91 mAnimator.setRepeatMode(ValueAnimator.RESTART); 92 mAnimator.setDuration(ANIMATION_DURATION); 93 mAnimator.setInterpolator(new LinearInterpolator()); 94 } 95 96 /** 97 * Returns the interpolation scalar (s) that satisfies the equation: 98 * {@code value = }lerp(a, b, s) 99 * 100 * <p>If {@code a == b}, then this function will return 0. 101 */ lerpInv(float a, float b, float value)102 private static float lerpInv(float a, float b, float value) { 103 return a != b ? ((value - a) / (b - a)) : 0.0f; 104 } 105 setRingColor(int color)106 public void setRingColor(int color) { 107 mCircleBorderColor = color; 108 } 109 setRingWidth(float width)110 public void setRingWidth(float width) { 111 mCircleBorderWidth = width; 112 } 113 startAnimation()114 public void startAnimation() { 115 if (!mAnimator.isStarted()) { 116 mAnimator.start(); 117 } 118 } 119 stopAnimation()120 public void stopAnimation() { 121 mAnimator.cancel(); 122 } 123 124 @Override draw(Canvas canvas)125 public void draw(Canvas canvas) { 126 canvas.save(); 127 mInnerCircleBounds.set(getBounds()); 128 mInnerCircleBounds.inset(mCircleBorderWidth / 2.0f, mCircleBorderWidth / 2.0f); 129 mPaint.setStrokeWidth(mCircleBorderWidth); 130 mPaint.setColor(mCircleBorderColor); 131 132 int level = getLevel(); 133 int currentSegment = level / LEVELS_PER_SEGMENT; 134 int offset = currentSegment * LEVELS_PER_SEGMENT; 135 float progress = (level - offset) / (float) LEVELS_PER_SEGMENT; 136 137 boolean growing = progress < GROW_SHRINK_RATIO; 138 float correctionAngle = CORRECTION_ANGLE * progress; 139 140 float sweepAngle; 141 142 if (growing) { 143 sweepAngle = MAX_SWEEP 144 * sInterpolator.getInterpolation(lerpInv(0f, GROW_SHRINK_RATIO, progress)); 145 } else { 146 sweepAngle = 147 MAX_SWEEP 148 * (1.0f - sInterpolator.getInterpolation( 149 lerpInv(GROW_SHRINK_RATIO, 1.0f, progress))); 150 } 151 152 sweepAngle = Math.max(1, sweepAngle); 153 154 canvas.rotate( 155 level * (1.0f / MAX_LEVEL) * 2 * FULL_CIRCLE + STARTING_ANGLE + correctionAngle, 156 mInnerCircleBounds.centerX(), 157 mInnerCircleBounds.centerY()); 158 canvas.drawArc( 159 mInnerCircleBounds, growing ? 0 : MAX_SWEEP - sweepAngle, sweepAngle, false, 160 mPaint); 161 canvas.restore(); 162 } 163 164 @Override 165 public void setAlpha(int i) { 166 // Not supported. 167 } 168 169 @Override 170 public void setColorFilter(ColorFilter colorFilter) { 171 // Not supported. 172 } 173 174 @Override 175 public int getOpacity() { 176 return PixelFormat.OPAQUE; 177 } 178 179 @Override 180 protected boolean onLevelChange(int level) { 181 return true; // Changing the level of this drawable does change its appearance. 182 } 183 } 184