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