SwipeProgressBar.java revision e9a361cf082bf8fbe908d1abfdc327209ec01d82
1/* 2 * Copyright (C) 2013 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.graphics.Canvas; 20import android.graphics.Paint; 21import android.graphics.Rect; 22import android.graphics.RectF; 23import android.support.v4.view.ViewCompat; 24import android.view.View; 25import android.view.animation.AnimationUtils; 26import android.view.animation.Interpolator; 27 28 29/** 30 * Custom progress bar that shows a cycle of colors as widening circles that 31 * overdraw each other. When finished, the bar is cleared from the inside out as 32 * the main cycle continues. Before running, this can also indicate how close 33 * the user is to triggering something (e.g. how far they need to pull down to 34 * trigger a refresh). 35 */ 36final class SwipeProgressBar { 37 38 // Default progress animation colors are grays. 39 private final static int COLOR1 = 0xB3000000; 40 private final static int COLOR2 = 0x80000000; 41 private final static int COLOR3 = 0x4d000000; 42 private final static int COLOR4 = 0x1a000000; 43 44 // The duration of the animation cycle. 45 private static final int ANIMATION_DURATION_MS = 2000; 46 47 // The duration of the animation to clear the bar. 48 private static final int FINISH_ANIMATION_DURATION_MS = 1000; 49 50 // Interpolator for varying the speed of the animation. 51 private static final Interpolator INTERPOLATOR = BakedBezierInterpolator.getInstance(); 52 53 private final Paint mPaint = new Paint(); 54 private final RectF mClipRect = new RectF(); 55 private float mTriggerPercentage; 56 private long mStartTime; 57 private long mFinishTime; 58 private boolean mRunning; 59 60 // Colors used when rendering the animation, 61 private int mColor1; 62 private int mColor2; 63 private int mColor3; 64 private int mColor4; 65 private View mParent; 66 67 private Rect mBounds = new Rect(); 68 69 public SwipeProgressBar(View parent) { 70 mParent = parent; 71 mColor1 = COLOR1; 72 mColor2 = COLOR2; 73 mColor3 = COLOR3; 74 mColor4 = COLOR4; 75 } 76 77 /** 78 * Set the four colors used in the progress animation. The first color will 79 * also be the color of the bar that grows in response to a user swipe 80 * gesture. 81 * 82 * @param color1 Integer representation of a color. 83 * @param color2 Integer representation of a color. 84 * @param color3 Integer representation of a color. 85 * @param color4 Integer representation of a color. 86 */ 87 void setColorScheme(int color1, int color2, int color3, int color4) { 88 mColor1 = color1; 89 mColor2 = color2; 90 mColor3 = color3; 91 mColor4 = color4; 92 } 93 94 /** 95 * Update the progress the user has made toward triggering the swipe 96 * gesture. and use this value to update the percentage of the trigger that 97 * is shown. 98 */ 99 void setTriggerPercentage(float triggerPercentage) { 100 mTriggerPercentage = triggerPercentage; 101 mStartTime = 0; 102 ViewCompat.postInvalidateOnAnimation(mParent); 103 } 104 105 /** 106 * Start showing the progress animation. 107 */ 108 void start() { 109 if (!mRunning) { 110 mTriggerPercentage = 0; 111 mStartTime = AnimationUtils.currentAnimationTimeMillis(); 112 mRunning = true; 113 mParent.postInvalidate(); 114 } 115 } 116 117 /** 118 * Stop showing the progress animation. 119 */ 120 void stop() { 121 if (mRunning) { 122 mTriggerPercentage = 0; 123 mFinishTime = AnimationUtils.currentAnimationTimeMillis(); 124 mRunning = false; 125 mParent.postInvalidate(); 126 } 127 } 128 129 /** 130 * @return Return whether the progress animation is currently running. 131 */ 132 boolean isRunning() { 133 return mRunning || mFinishTime > 0; 134 } 135 136 void draw(Canvas canvas) { 137 final int width = mBounds.width(); 138 final int height = mBounds.height(); 139 final int cx = width / 2; 140 final int cy = height / 2; 141 int restoreCount = canvas.save(); 142 canvas.clipRect(mBounds); 143 144 if (mRunning || (mFinishTime > 0)) { 145 long now = AnimationUtils.currentAnimationTimeMillis(); 146 long elapsed = (now - mStartTime) % ANIMATION_DURATION_MS; 147 long iterations = (now - mStartTime) / ANIMATION_DURATION_MS; 148 float rawProgress = (elapsed / (ANIMATION_DURATION_MS / 100f)); 149 150 // If we're not running anymore, that means we're running through 151 // the finish animation. 152 if (!mRunning) { 153 // If the finish animation is done, don't draw anything, and 154 // don't repost. 155 if ((now - mFinishTime) >= FINISH_ANIMATION_DURATION_MS) { 156 mFinishTime = 0; 157 return; 158 } 159 160 // Otherwise, use a 0 opacity alpha layer to clear the animation 161 // from the inside out. This layer will prevent the circles from 162 // drawing within its bounds. 163 long finishElapsed = (now - mFinishTime) % FINISH_ANIMATION_DURATION_MS; 164 float finishProgress = (finishElapsed / (FINISH_ANIMATION_DURATION_MS / 100f)); 165 float pct = (finishProgress / 100f); 166 // Radius of the circle is half of the screen. 167 float clearRadius = width / 2 * INTERPOLATOR.getInterpolation(pct); 168 mClipRect.set(cx - clearRadius, 0, cx + clearRadius, height); 169 canvas.saveLayerAlpha(mClipRect, 0, 0); 170 } 171 172 // First fill in with the last color that would have finished drawing. 173 if (iterations == 0) { 174 canvas.drawColor(mColor1); 175 } else { 176 if (rawProgress >= 0 && rawProgress < 25) { 177 canvas.drawColor(mColor4); 178 } else if (rawProgress >= 25 && rawProgress < 50) { 179 canvas.drawColor(mColor1); 180 } else if (rawProgress >= 50 && rawProgress < 75) { 181 canvas.drawColor(mColor2); 182 } else { 183 canvas.drawColor(mColor3); 184 } 185 } 186 187 // Then draw up to 4 overlapping concentric circles of varying radii, based on how far 188 // along we are in the cycle. 189 // progress 0-50 draw mColor2 190 // progress 25-75 draw mColor3 191 // progress 50-100 draw mColor4 192 // progress 75 (wrap to 25) draw mColor1 193 if ((rawProgress >= 0 && rawProgress <= 25)) { 194 float pct = (((rawProgress + 25) * 2) / 100f); 195 drawCircle(canvas, cx, cy, mColor1, pct); 196 } 197 if (rawProgress >= 0 && rawProgress <= 50) { 198 float pct = ((rawProgress * 2) / 100f); 199 drawCircle(canvas, cx, cy, mColor2, pct); 200 } 201 if (rawProgress >= 25 && rawProgress <= 75) { 202 float pct = (((rawProgress - 25) * 2) / 100f); 203 drawCircle(canvas, cx, cy, mColor3, pct); 204 } 205 if (rawProgress >= 50 && rawProgress <= 100) { 206 float pct = (((rawProgress - 50) * 2) / 100f); 207 drawCircle(canvas, cx, cy, mColor4, pct); 208 } 209 if ((rawProgress >= 75 && rawProgress <= 100)) { 210 float pct = (((rawProgress - 75) * 2) / 100f); 211 drawCircle(canvas, cx, cy, mColor1, pct); 212 } 213 if (mTriggerPercentage > 0) { 214 // There is some portion of trigger to draw. Restore the canvas, 215 // then draw the trigger. Otherwise, the trigger does not appear 216 // until after the bar has finished animating and appears to 217 // just jump in at a larger width than expected. 218 canvas.restoreToCount(restoreCount); 219 restoreCount = canvas.save(); 220 canvas.clipRect(mBounds); 221 drawTrigger(canvas, cx, cy); 222 } 223 // Keep running until we finish out the last cycle. 224 ViewCompat.postInvalidateOnAnimation(mParent); 225 } else { 226 // Otherwise if we're in the middle of a trigger, draw that. 227 if (mTriggerPercentage > 0 && mTriggerPercentage <= 1.0) { 228 drawTrigger(canvas, cx, cy); 229 } 230 } 231 canvas.restoreToCount(restoreCount); 232 } 233 234 private void drawTrigger(Canvas canvas, int cx, int cy) { 235 mPaint.setColor(mColor1); 236 canvas.drawCircle(cx, cy, cx * mTriggerPercentage, mPaint); 237 } 238 239 /** 240 * Draws a circle centered in the view. 241 * 242 * @param canvas the canvas to draw on 243 * @param cx the center x coordinate 244 * @param cy the center y coordinate 245 * @param color the color to draw 246 * @param pct the percentage of the view that the circle should cover 247 */ 248 private void drawCircle(Canvas canvas, float cx, float cy, int color, float pct) { 249 mPaint.setColor(color); 250 canvas.save(); 251 canvas.translate(cx, cy); 252 float radiusScale = INTERPOLATOR.getInterpolation(pct); 253 canvas.scale(radiusScale, radiusScale); 254 canvas.drawCircle(0, 0, cx, mPaint); 255 canvas.restore(); 256 } 257 258 /** 259 * Set the drawing bounds of this SwipeProgressBar. 260 */ 261 void setBounds(int left, int top, int right, int bottom) { 262 mBounds.left = left; 263 mBounds.top = top; 264 mBounds.right = right; 265 mBounds.bottom = bottom; 266 } 267}