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