SwipeProgressBar.java revision cb084a5eea8cc641096fc288cb7156e0bb866d81
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 = mBounds.top + height / 2; 141 boolean drawTriggerWhileFinishing = false; 142 int restoreCount = canvas.save(); 143 canvas.clipRect(mBounds); 144 145 if (mRunning || (mFinishTime > 0)) { 146 long now = AnimationUtils.currentAnimationTimeMillis(); 147 long elapsed = (now - mStartTime) % ANIMATION_DURATION_MS; 148 long iterations = (now - mStartTime) / ANIMATION_DURATION_MS; 149 float rawProgress = (elapsed / (ANIMATION_DURATION_MS / 100f)); 150 151 // If we're not running anymore, that means we're running through 152 // the finish animation. 153 if (!mRunning) { 154 // If the finish animation is done, don't draw anything, and 155 // don't repost. 156 if ((now - mFinishTime) >= FINISH_ANIMATION_DURATION_MS) { 157 mFinishTime = 0; 158 return; 159 } 160 161 // Otherwise, use a 0 opacity alpha layer to clear the animation 162 // from the inside out. This layer will prevent the circles from 163 // drawing within its bounds. 164 long finishElapsed = (now - mFinishTime) % FINISH_ANIMATION_DURATION_MS; 165 float finishProgress = (finishElapsed / (FINISH_ANIMATION_DURATION_MS / 100f)); 166 float pct = (finishProgress / 100f); 167 // Radius of the circle is half of the screen. 168 float clearRadius = width / 2 * INTERPOLATOR.getInterpolation(pct); 169 mClipRect.set(cx - clearRadius, 0, cx + clearRadius, height); 170 canvas.saveLayerAlpha(mClipRect, 0, 0); 171 // Only draw the trigger if there is a space in the center of 172 // this refreshing view that needs to be filled in by the 173 // trigger. If the progress view is just still animating, let it 174 // continue animating. 175 drawTriggerWhileFinishing = true; 176 } 177 178 // First fill in with the last color that would have finished drawing. 179 if (iterations == 0) { 180 canvas.drawColor(mColor1); 181 } else { 182 if (rawProgress >= 0 && rawProgress < 25) { 183 canvas.drawColor(mColor4); 184 } else if (rawProgress >= 25 && rawProgress < 50) { 185 canvas.drawColor(mColor1); 186 } else if (rawProgress >= 50 && rawProgress < 75) { 187 canvas.drawColor(mColor2); 188 } else { 189 canvas.drawColor(mColor3); 190 } 191 } 192 193 // Then draw up to 4 overlapping concentric circles of varying radii, based on how far 194 // along we are in the cycle. 195 // progress 0-50 draw mColor2 196 // progress 25-75 draw mColor3 197 // progress 50-100 draw mColor4 198 // progress 75 (wrap to 25) draw mColor1 199 if ((rawProgress >= 0 && rawProgress <= 25)) { 200 float pct = (((rawProgress + 25) * 2) / 100f); 201 drawCircle(canvas, cx, cy, mColor1, pct); 202 } 203 if (rawProgress >= 0 && rawProgress <= 50) { 204 float pct = ((rawProgress * 2) / 100f); 205 drawCircle(canvas, cx, cy, mColor2, pct); 206 } 207 if (rawProgress >= 25 && rawProgress <= 75) { 208 float pct = (((rawProgress - 25) * 2) / 100f); 209 drawCircle(canvas, cx, cy, mColor3, pct); 210 } 211 if (rawProgress >= 50 && rawProgress <= 100) { 212 float pct = (((rawProgress - 50) * 2) / 100f); 213 drawCircle(canvas, cx, cy, mColor4, pct); 214 } 215 if ((rawProgress >= 75 && rawProgress <= 100)) { 216 float pct = (((rawProgress - 75) * 2) / 100f); 217 drawCircle(canvas, cx, cy, mColor1, pct); 218 } 219 if (mTriggerPercentage > 0 && drawTriggerWhileFinishing) { 220 // There is some portion of trigger to draw. Restore the canvas, 221 // then draw the trigger. Otherwise, the trigger does not appear 222 // until after the bar has finished animating and appears to 223 // just jump in at a larger width than expected. 224 canvas.restoreToCount(restoreCount); 225 restoreCount = canvas.save(); 226 canvas.clipRect(mBounds); 227 drawTrigger(canvas, cx, cy); 228 } 229 // Keep running until we finish out the last cycle. 230 ViewCompat.postInvalidateOnAnimation(mParent); 231 } else { 232 // Otherwise if we're in the middle of a trigger, draw that. 233 if (mTriggerPercentage > 0 && mTriggerPercentage <= 1.0) { 234 drawTrigger(canvas, cx, cy); 235 } 236 } 237 canvas.restoreToCount(restoreCount); 238 } 239 240 private void drawTrigger(Canvas canvas, int cx, int cy) { 241 mPaint.setColor(mColor1); 242 canvas.drawCircle(cx, cy, cx * mTriggerPercentage, mPaint); 243 } 244 245 /** 246 * Draws a circle centered in the view. 247 * 248 * @param canvas the canvas to draw on 249 * @param cx the center x coordinate 250 * @param cy the center y coordinate 251 * @param color the color to draw 252 * @param pct the percentage of the view that the circle should cover 253 */ 254 private void drawCircle(Canvas canvas, float cx, float cy, int color, float pct) { 255 mPaint.setColor(color); 256 canvas.save(); 257 canvas.translate(cx, cy); 258 float radiusScale = INTERPOLATOR.getInterpolation(pct); 259 canvas.scale(radiusScale, radiusScale); 260 canvas.drawCircle(0, 0, cx, mPaint); 261 canvas.restore(); 262 } 263 264 /** 265 * Set the drawing bounds of this SwipeProgressBar. 266 */ 267 void setBounds(int left, int top, int right, int bottom) { 268 mBounds.left = left; 269 mBounds.top = top; 270 mBounds.right = right; 271 mBounds.bottom = bottom; 272 } 273} 274