| /* |
| * Copyright (C) 2013 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.support.v4.widget; |
| |
| import android.graphics.Canvas; |
| import android.graphics.Paint; |
| import android.graphics.Rect; |
| import android.graphics.RectF; |
| import android.support.v4.view.ViewCompat; |
| import android.support.v4.view.animation.FastOutSlowInInterpolator; |
| import android.view.View; |
| import android.view.animation.AnimationUtils; |
| import android.view.animation.Interpolator; |
| |
| |
| /** |
| * Custom progress bar that shows a cycle of colors as widening circles that |
| * overdraw each other. When finished, the bar is cleared from the inside out as |
| * the main cycle continues. Before running, this can also indicate how close |
| * the user is to triggering something (e.g. how far they need to pull down to |
| * trigger a refresh). |
| */ |
| final class SwipeProgressBar { |
| |
| // Default progress animation colors are grays. |
| private final static int COLOR1 = 0xB3000000; |
| private final static int COLOR2 = 0x80000000; |
| private final static int COLOR3 = 0x4d000000; |
| private final static int COLOR4 = 0x1a000000; |
| |
| // The duration of the animation cycle. |
| private static final int ANIMATION_DURATION_MS = 2000; |
| |
| // The duration of the animation to clear the bar. |
| private static final int FINISH_ANIMATION_DURATION_MS = 1000; |
| |
| // Interpolator for varying the speed of the animation. |
| private static final Interpolator INTERPOLATOR = new FastOutSlowInInterpolator(); |
| |
| private final Paint mPaint = new Paint(); |
| private final RectF mClipRect = new RectF(); |
| private float mTriggerPercentage; |
| private long mStartTime; |
| private long mFinishTime; |
| private boolean mRunning; |
| |
| // Colors used when rendering the animation, |
| private int mColor1; |
| private int mColor2; |
| private int mColor3; |
| private int mColor4; |
| private View mParent; |
| |
| private Rect mBounds = new Rect(); |
| |
| public SwipeProgressBar(View parent) { |
| mParent = parent; |
| mColor1 = COLOR1; |
| mColor2 = COLOR2; |
| mColor3 = COLOR3; |
| mColor4 = COLOR4; |
| } |
| |
| /** |
| * Set the four colors used in the progress animation. The first color will |
| * also be the color of the bar that grows in response to a user swipe |
| * gesture. |
| * |
| * @param color1 Integer representation of a color. |
| * @param color2 Integer representation of a color. |
| * @param color3 Integer representation of a color. |
| * @param color4 Integer representation of a color. |
| */ |
| void setColorScheme(int color1, int color2, int color3, int color4) { |
| mColor1 = color1; |
| mColor2 = color2; |
| mColor3 = color3; |
| mColor4 = color4; |
| } |
| |
| /** |
| * Update the progress the user has made toward triggering the swipe |
| * gesture. and use this value to update the percentage of the trigger that |
| * is shown. |
| */ |
| void setTriggerPercentage(float triggerPercentage) { |
| mTriggerPercentage = triggerPercentage; |
| mStartTime = 0; |
| ViewCompat.postInvalidateOnAnimation( |
| mParent, mBounds.left, mBounds.top, mBounds.right, mBounds.bottom); |
| } |
| |
| /** |
| * Start showing the progress animation. |
| */ |
| void start() { |
| if (!mRunning) { |
| mTriggerPercentage = 0; |
| mStartTime = AnimationUtils.currentAnimationTimeMillis(); |
| mRunning = true; |
| mParent.postInvalidate(); |
| } |
| } |
| |
| /** |
| * Stop showing the progress animation. |
| */ |
| void stop() { |
| if (mRunning) { |
| mTriggerPercentage = 0; |
| mFinishTime = AnimationUtils.currentAnimationTimeMillis(); |
| mRunning = false; |
| mParent.postInvalidate(); |
| } |
| } |
| |
| /** |
| * @return Return whether the progress animation is currently running. |
| */ |
| boolean isRunning() { |
| return mRunning || mFinishTime > 0; |
| } |
| |
| void draw(Canvas canvas) { |
| final int width = mBounds.width(); |
| final int height = mBounds.height(); |
| final int cx = width / 2; |
| final int cy = height / 2; |
| boolean drawTriggerWhileFinishing = false; |
| int restoreCount = canvas.save(); |
| canvas.clipRect(mBounds); |
| |
| if (mRunning || (mFinishTime > 0)) { |
| long now = AnimationUtils.currentAnimationTimeMillis(); |
| long elapsed = (now - mStartTime) % ANIMATION_DURATION_MS; |
| long iterations = (now - mStartTime) / ANIMATION_DURATION_MS; |
| float rawProgress = (elapsed / (ANIMATION_DURATION_MS / 100f)); |
| |
| // If we're not running anymore, that means we're running through |
| // the finish animation. |
| if (!mRunning) { |
| // If the finish animation is done, don't draw anything, and |
| // don't repost. |
| if ((now - mFinishTime) >= FINISH_ANIMATION_DURATION_MS) { |
| mFinishTime = 0; |
| return; |
| } |
| |
| // Otherwise, use a 0 opacity alpha layer to clear the animation |
| // from the inside out. This layer will prevent the circles from |
| // drawing within its bounds. |
| long finishElapsed = (now - mFinishTime) % FINISH_ANIMATION_DURATION_MS; |
| float finishProgress = (finishElapsed / (FINISH_ANIMATION_DURATION_MS / 100f)); |
| float pct = (finishProgress / 100f); |
| // Radius of the circle is half of the screen. |
| float clearRadius = width / 2 * INTERPOLATOR.getInterpolation(pct); |
| mClipRect.set(cx - clearRadius, 0, cx + clearRadius, height); |
| canvas.saveLayerAlpha(mClipRect, 0, 0); |
| // Only draw the trigger if there is a space in the center of |
| // this refreshing view that needs to be filled in by the |
| // trigger. If the progress view is just still animating, let it |
| // continue animating. |
| drawTriggerWhileFinishing = true; |
| } |
| |
| // First fill in with the last color that would have finished drawing. |
| if (iterations == 0) { |
| canvas.drawColor(mColor1); |
| } else { |
| if (rawProgress >= 0 && rawProgress < 25) { |
| canvas.drawColor(mColor4); |
| } else if (rawProgress >= 25 && rawProgress < 50) { |
| canvas.drawColor(mColor1); |
| } else if (rawProgress >= 50 && rawProgress < 75) { |
| canvas.drawColor(mColor2); |
| } else { |
| canvas.drawColor(mColor3); |
| } |
| } |
| |
| // Then draw up to 4 overlapping concentric circles of varying radii, based on how far |
| // along we are in the cycle. |
| // progress 0-50 draw mColor2 |
| // progress 25-75 draw mColor3 |
| // progress 50-100 draw mColor4 |
| // progress 75 (wrap to 25) draw mColor1 |
| if ((rawProgress >= 0 && rawProgress <= 25)) { |
| float pct = (((rawProgress + 25) * 2) / 100f); |
| drawCircle(canvas, cx, cy, mColor1, pct); |
| } |
| if (rawProgress >= 0 && rawProgress <= 50) { |
| float pct = ((rawProgress * 2) / 100f); |
| drawCircle(canvas, cx, cy, mColor2, pct); |
| } |
| if (rawProgress >= 25 && rawProgress <= 75) { |
| float pct = (((rawProgress - 25) * 2) / 100f); |
| drawCircle(canvas, cx, cy, mColor3, pct); |
| } |
| if (rawProgress >= 50 && rawProgress <= 100) { |
| float pct = (((rawProgress - 50) * 2) / 100f); |
| drawCircle(canvas, cx, cy, mColor4, pct); |
| } |
| if ((rawProgress >= 75 && rawProgress <= 100)) { |
| float pct = (((rawProgress - 75) * 2) / 100f); |
| drawCircle(canvas, cx, cy, mColor1, pct); |
| } |
| if (mTriggerPercentage > 0 && drawTriggerWhileFinishing) { |
| // There is some portion of trigger to draw. Restore the canvas, |
| // then draw the trigger. Otherwise, the trigger does not appear |
| // until after the bar has finished animating and appears to |
| // just jump in at a larger width than expected. |
| canvas.restoreToCount(restoreCount); |
| restoreCount = canvas.save(); |
| canvas.clipRect(mBounds); |
| drawTrigger(canvas, cx, cy); |
| } |
| // Keep running until we finish out the last cycle. |
| ViewCompat.postInvalidateOnAnimation( |
| mParent, mBounds.left, mBounds.top, mBounds.right, mBounds.bottom); |
| } else { |
| // Otherwise if we're in the middle of a trigger, draw that. |
| if (mTriggerPercentage > 0 && mTriggerPercentage <= 1.0) { |
| drawTrigger(canvas, cx, cy); |
| } |
| } |
| canvas.restoreToCount(restoreCount); |
| } |
| |
| private void drawTrigger(Canvas canvas, int cx, int cy) { |
| mPaint.setColor(mColor1); |
| canvas.drawCircle(cx, cy, cx * mTriggerPercentage, mPaint); |
| } |
| |
| /** |
| * Draws a circle centered in the view. |
| * |
| * @param canvas the canvas to draw on |
| * @param cx the center x coordinate |
| * @param cy the center y coordinate |
| * @param color the color to draw |
| * @param pct the percentage of the view that the circle should cover |
| */ |
| private void drawCircle(Canvas canvas, float cx, float cy, int color, float pct) { |
| mPaint.setColor(color); |
| canvas.save(); |
| canvas.translate(cx, cy); |
| float radiusScale = INTERPOLATOR.getInterpolation(pct); |
| canvas.scale(radiusScale, radiusScale); |
| canvas.drawCircle(0, 0, cx, mPaint); |
| canvas.restore(); |
| } |
| |
| /** |
| * Set the drawing bounds of this SwipeProgressBar. |
| */ |
| void setBounds(int left, int top, int right, int bottom) { |
| mBounds.left = left; |
| mBounds.top = top; |
| mBounds.right = right; |
| mBounds.bottom = bottom; |
| } |
| } |