michael@0: /* michael@0: * Copyright (C) 2013 The Android Open Source Project michael@0: * michael@0: * Licensed under the Apache License, Version 2.0 (the "License"); michael@0: * you may not use this file except in compliance with the License. michael@0: * You may obtain a copy of the License at michael@0: * michael@0: * http://www.apache.org/licenses/LICENSE-2.0 michael@0: * michael@0: * Unless required by applicable law or agreed to in writing, software michael@0: * distributed under the License is distributed on an "AS IS" BASIS, michael@0: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. michael@0: * See the License for the specific language governing permissions and michael@0: * limitations under the License. michael@0: */ michael@0: michael@0: package org.mozilla.gecko.widget; michael@0: michael@0: import android.content.Context; michael@0: import android.content.res.Resources; michael@0: import android.content.res.TypedArray; michael@0: import android.graphics.Canvas; michael@0: import android.graphics.Paint; michael@0: import android.graphics.Rect; michael@0: import android.graphics.RectF; michael@0: import android.support.v4.view.ViewCompat; michael@0: import android.util.AttributeSet; michael@0: import android.util.DisplayMetrics; michael@0: import android.view.MotionEvent; michael@0: import android.view.View; michael@0: import android.view.ViewConfiguration; michael@0: import android.view.ViewGroup; michael@0: import android.view.animation.AccelerateInterpolator; michael@0: import android.view.animation.Animation; michael@0: import android.view.animation.Animation.AnimationListener; michael@0: import android.view.animation.AnimationUtils; michael@0: import android.view.animation.DecelerateInterpolator; michael@0: import android.view.animation.Interpolator; michael@0: import android.view.animation.Transformation; michael@0: import android.widget.AbsListView; michael@0: michael@0: /** michael@0: * GeckoSwipeRefreshLayout is mostly lifted from Android's support library (v4) with these michael@0: * modfications: michael@0: * - Removes elastic "rubber banding" effect when overscrolling the child view. michael@0: * - Changes the height of the progress bar to match the height of HomePager's page indicator. michael@0: * - Uses a rectangle rather than a circle for the SwipeProgressBar indicator. michael@0: * michael@0: * This class also embeds package-access dependent classes SwipeProgressBar and michael@0: * BakedBezierInterpolator. michael@0: * michael@0: * Original source: https://android.googlesource.com/platform/frameworks/support/+/ michael@0: * android-support-lib-19.1.0/v4/java/android/support/v4/widget/SwipeRefreshLayout.java michael@0: */ michael@0: public class GeckoSwipeRefreshLayout extends ViewGroup { michael@0: private static final long RETURN_TO_ORIGINAL_POSITION_TIMEOUT = 300; michael@0: private static final float ACCELERATE_INTERPOLATION_FACTOR = 1.5f; michael@0: private static final float DECELERATE_INTERPOLATION_FACTOR = 2f; michael@0: // Reduce the height (from 4 to 3) of the progress bar to match HomePager's page indicator. michael@0: private static final float PROGRESS_BAR_HEIGHT = 3; michael@0: private static final float MAX_SWIPE_DISTANCE_FACTOR = .6f; michael@0: private static final int REFRESH_TRIGGER_DISTANCE = 120; michael@0: michael@0: private SwipeProgressBar mProgressBar; //the thing that shows progress is going michael@0: private View mTarget; //the content that gets pulled down michael@0: private int mOriginalOffsetTop; michael@0: private OnRefreshListener mListener; michael@0: private MotionEvent mDownEvent; michael@0: private int mFrom; michael@0: private boolean mRefreshing = false; michael@0: private int mTouchSlop; michael@0: private float mDistanceToTriggerSync = -1; michael@0: private float mPrevY; michael@0: private int mMediumAnimationDuration; michael@0: private float mFromPercentage = 0; michael@0: private float mCurrPercentage = 0; michael@0: private int mProgressBarHeight; michael@0: private int mCurrentTargetOffsetTop; michael@0: // Target is returning to its start offset because it was cancelled or a michael@0: // refresh was triggered. michael@0: private boolean mReturningToStart; michael@0: private final DecelerateInterpolator mDecelerateInterpolator; michael@0: private final AccelerateInterpolator mAccelerateInterpolator; michael@0: private static final int[] LAYOUT_ATTRS = new int[] { michael@0: android.R.attr.enabled michael@0: }; michael@0: michael@0: private final Animation mAnimateToStartPosition = new Animation() { michael@0: @Override michael@0: public void applyTransformation(float interpolatedTime, Transformation t) { michael@0: int targetTop = 0; michael@0: if (mFrom != mOriginalOffsetTop) { michael@0: targetTop = (mFrom + (int)((mOriginalOffsetTop - mFrom) * interpolatedTime)); michael@0: } michael@0: int offset = targetTop - mTarget.getTop(); michael@0: final int currentTop = mTarget.getTop(); michael@0: if (offset + currentTop < 0) { michael@0: offset = 0 - currentTop; michael@0: } michael@0: setTargetOffsetTopAndBottom(offset); michael@0: } michael@0: }; michael@0: michael@0: private Animation mShrinkTrigger = new Animation() { michael@0: @Override michael@0: public void applyTransformation(float interpolatedTime, Transformation t) { michael@0: float percent = mFromPercentage + ((0 - mFromPercentage) * interpolatedTime); michael@0: mProgressBar.setTriggerPercentage(percent); michael@0: } michael@0: }; michael@0: michael@0: private final AnimationListener mReturnToStartPositionListener = new BaseAnimationListener() { michael@0: @Override michael@0: public void onAnimationEnd(Animation animation) { michael@0: // Once the target content has returned to its start position, reset michael@0: // the target offset to 0 michael@0: mCurrentTargetOffsetTop = 0; michael@0: } michael@0: }; michael@0: michael@0: private final AnimationListener mShrinkAnimationListener = new BaseAnimationListener() { michael@0: @Override michael@0: public void onAnimationEnd(Animation animation) { michael@0: mCurrPercentage = 0; michael@0: } michael@0: }; michael@0: michael@0: private final Runnable mReturnToStartPosition = new Runnable() { michael@0: michael@0: @Override michael@0: public void run() { michael@0: mReturningToStart = true; michael@0: animateOffsetToStartPosition(mCurrentTargetOffsetTop + getPaddingTop(), michael@0: mReturnToStartPositionListener); michael@0: } michael@0: michael@0: }; michael@0: michael@0: // Cancel the refresh gesture and animate everything back to its original state. michael@0: private final Runnable mCancel = new Runnable() { michael@0: michael@0: @Override michael@0: public void run() { michael@0: mReturningToStart = true; michael@0: // Timeout fired since the user last moved their finger; animate the michael@0: // trigger to 0 and put the target back at its original position michael@0: if (mProgressBar != null) { michael@0: mFromPercentage = mCurrPercentage; michael@0: mShrinkTrigger.setDuration(mMediumAnimationDuration); michael@0: mShrinkTrigger.setAnimationListener(mShrinkAnimationListener); michael@0: mShrinkTrigger.reset(); michael@0: mShrinkTrigger.setInterpolator(mDecelerateInterpolator); michael@0: startAnimation(mShrinkTrigger); michael@0: } michael@0: animateOffsetToStartPosition(mCurrentTargetOffsetTop + getPaddingTop(), michael@0: mReturnToStartPositionListener); michael@0: } michael@0: michael@0: }; michael@0: michael@0: /** michael@0: * Simple constructor to use when creating a GeckoSwipeRefreshLayout from code. michael@0: * @param context michael@0: */ michael@0: public GeckoSwipeRefreshLayout(Context context) { michael@0: this(context, null); michael@0: } michael@0: michael@0: /** michael@0: * Constructor that is called when inflating GeckoSwipeRefreshLayout from XML. michael@0: * @param context michael@0: * @param attrs michael@0: */ michael@0: public GeckoSwipeRefreshLayout(Context context, AttributeSet attrs) { michael@0: super(context, attrs); michael@0: michael@0: mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); michael@0: michael@0: mMediumAnimationDuration = getResources().getInteger( michael@0: android.R.integer.config_mediumAnimTime); michael@0: michael@0: setWillNotDraw(false); michael@0: mProgressBar = new SwipeProgressBar(this); michael@0: final DisplayMetrics metrics = getResources().getDisplayMetrics(); michael@0: mProgressBarHeight = (int) (metrics.density * PROGRESS_BAR_HEIGHT); michael@0: mDecelerateInterpolator = new DecelerateInterpolator(DECELERATE_INTERPOLATION_FACTOR); michael@0: mAccelerateInterpolator = new AccelerateInterpolator(ACCELERATE_INTERPOLATION_FACTOR); michael@0: michael@0: final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS); michael@0: setEnabled(a.getBoolean(0, true)); michael@0: a.recycle(); michael@0: } michael@0: michael@0: @Override michael@0: public void onAttachedToWindow() { michael@0: super.onAttachedToWindow(); michael@0: removeCallbacks(mCancel); michael@0: removeCallbacks(mReturnToStartPosition); michael@0: } michael@0: michael@0: @Override michael@0: public void onDetachedFromWindow() { michael@0: super.onDetachedFromWindow(); michael@0: removeCallbacks(mReturnToStartPosition); michael@0: removeCallbacks(mCancel); michael@0: } michael@0: michael@0: private void animateOffsetToStartPosition(int from, AnimationListener listener) { michael@0: mFrom = from; michael@0: mAnimateToStartPosition.reset(); michael@0: mAnimateToStartPosition.setDuration(mMediumAnimationDuration); michael@0: mAnimateToStartPosition.setAnimationListener(listener); michael@0: mAnimateToStartPosition.setInterpolator(mDecelerateInterpolator); michael@0: mTarget.startAnimation(mAnimateToStartPosition); michael@0: } michael@0: michael@0: /** michael@0: * Set the listener to be notified when a refresh is triggered via the swipe michael@0: * gesture. michael@0: */ michael@0: public void setOnRefreshListener(OnRefreshListener listener) { michael@0: mListener = listener; michael@0: } michael@0: michael@0: private void setTriggerPercentage(float percent) { michael@0: if (percent == 0f) { michael@0: // No-op. A null trigger means it's uninitialized, and setting it to zero-percent michael@0: // means we're trying to reset state, so there's nothing to reset in this case. michael@0: mCurrPercentage = 0; michael@0: return; michael@0: } michael@0: mCurrPercentage = percent; michael@0: mProgressBar.setTriggerPercentage(percent); michael@0: } michael@0: michael@0: /** michael@0: * Notify the widget that refresh state has changed. Do not call this when michael@0: * refresh is triggered by a swipe gesture. michael@0: * michael@0: * @param refreshing Whether or not the view should show refresh progress. michael@0: */ michael@0: public void setRefreshing(boolean refreshing) { michael@0: if (mRefreshing != refreshing) { michael@0: ensureTarget(); michael@0: mCurrPercentage = 0; michael@0: mRefreshing = refreshing; michael@0: if (mRefreshing) { michael@0: mProgressBar.start(); michael@0: } else { michael@0: mProgressBar.stop(); michael@0: } michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Set the four colors used in the progress animation. The first color will michael@0: * also be the color of the bar that grows in response to a user swipe michael@0: * gesture. michael@0: * michael@0: * @param colorRes1 Color resource. michael@0: * @param colorRes2 Color resource. michael@0: * @param colorRes3 Color resource. michael@0: * @param colorRes4 Color resource. michael@0: */ michael@0: public void setColorScheme(int colorRes1, int colorRes2, int colorRes3, int colorRes4) { michael@0: ensureTarget(); michael@0: final Resources res = getResources(); michael@0: final int color1 = res.getColor(colorRes1); michael@0: final int color2 = res.getColor(colorRes2); michael@0: final int color3 = res.getColor(colorRes3); michael@0: final int color4 = res.getColor(colorRes4); michael@0: mProgressBar.setColorScheme(color1, color2, color3,color4); michael@0: } michael@0: michael@0: /** michael@0: * @return Whether the SwipeRefreshWidget is actively showing refresh michael@0: * progress. michael@0: */ michael@0: public boolean isRefreshing() { michael@0: return mRefreshing; michael@0: } michael@0: michael@0: private void ensureTarget() { michael@0: // Don't bother getting the parent height if the parent hasn't been laid out yet. michael@0: if (mTarget == null) { michael@0: if (getChildCount() > 1 && !isInEditMode()) { michael@0: throw new IllegalStateException( michael@0: "GeckoSwipeRefreshLayout can host only one direct child"); michael@0: } michael@0: mTarget = getChildAt(0); michael@0: mOriginalOffsetTop = mTarget.getTop() + getPaddingTop(); michael@0: } michael@0: if (mDistanceToTriggerSync == -1) { michael@0: if (getParent() != null && ((View)getParent()).getHeight() > 0) { michael@0: final DisplayMetrics metrics = getResources().getDisplayMetrics(); michael@0: mDistanceToTriggerSync = (int) Math.min( michael@0: ((View) getParent()) .getHeight() * MAX_SWIPE_DISTANCE_FACTOR, michael@0: REFRESH_TRIGGER_DISTANCE * metrics.density); michael@0: } michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public void draw(Canvas canvas) { michael@0: super.draw(canvas); michael@0: mProgressBar.draw(canvas); michael@0: } michael@0: michael@0: @Override michael@0: protected void onLayout(boolean changed, int left, int top, int right, int bottom) { michael@0: final int width = getMeasuredWidth(); michael@0: final int height = getMeasuredHeight(); michael@0: mProgressBar.setBounds(0, 0, width, mProgressBarHeight); michael@0: if (getChildCount() == 0) { michael@0: return; michael@0: } michael@0: final View child = getChildAt(0); michael@0: final int childLeft = getPaddingLeft(); michael@0: final int childTop = mCurrentTargetOffsetTop + getPaddingTop(); michael@0: final int childWidth = width - getPaddingLeft() - getPaddingRight(); michael@0: final int childHeight = height - getPaddingTop() - getPaddingBottom(); michael@0: child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight); michael@0: } michael@0: michael@0: @Override michael@0: public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { michael@0: super.onMeasure(widthMeasureSpec, heightMeasureSpec); michael@0: if (getChildCount() > 1 && !isInEditMode()) { michael@0: throw new IllegalStateException("GeckoSwipeRefreshLayout can host only one direct child"); michael@0: } michael@0: if (getChildCount() > 0) { michael@0: getChildAt(0).measure( michael@0: MeasureSpec.makeMeasureSpec( michael@0: getMeasuredWidth() - getPaddingLeft() - getPaddingRight(), michael@0: MeasureSpec.EXACTLY), michael@0: MeasureSpec.makeMeasureSpec( michael@0: getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), michael@0: MeasureSpec.EXACTLY)); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * @return Whether it is possible for the child view of this layout to michael@0: * scroll up. Override this if the child view is a custom view. michael@0: */ michael@0: public boolean canChildScrollUp() { michael@0: if (android.os.Build.VERSION.SDK_INT < 14) { michael@0: if (mTarget instanceof AbsListView) { michael@0: final AbsListView absListView = (AbsListView) mTarget; michael@0: return absListView.getChildCount() > 0 michael@0: && (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0) michael@0: .getTop() < absListView.getPaddingTop()); michael@0: } else { michael@0: return mTarget.getScrollY() > 0; michael@0: } michael@0: } else { michael@0: return ViewCompat.canScrollVertically(mTarget, -1); michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public boolean onInterceptTouchEvent(MotionEvent ev) { michael@0: ensureTarget(); michael@0: boolean handled = false; michael@0: if (mReturningToStart && ev.getAction() == MotionEvent.ACTION_DOWN) { michael@0: mReturningToStart = false; michael@0: } michael@0: if (isEnabled() && !mReturningToStart && !canChildScrollUp()) { michael@0: handled = onTouchEvent(ev); michael@0: } michael@0: return !handled ? super.onInterceptTouchEvent(ev) : handled; michael@0: } michael@0: michael@0: @Override michael@0: public void requestDisallowInterceptTouchEvent(boolean b) { michael@0: // Nope. michael@0: } michael@0: michael@0: @Override michael@0: public boolean onTouchEvent(MotionEvent event) { michael@0: final int action = event.getAction(); michael@0: boolean handled = false; michael@0: switch (action) { michael@0: case MotionEvent.ACTION_DOWN: michael@0: mCurrPercentage = 0; michael@0: mDownEvent = MotionEvent.obtain(event); michael@0: mPrevY = mDownEvent.getY(); michael@0: break; michael@0: case MotionEvent.ACTION_MOVE: michael@0: if (mDownEvent != null && !mReturningToStart) { michael@0: final float eventY = event.getY(); michael@0: float yDiff = eventY - mDownEvent.getY(); michael@0: if (yDiff > mTouchSlop) { michael@0: // User velocity passed min velocity; trigger a refresh michael@0: if (yDiff > mDistanceToTriggerSync) { michael@0: // User movement passed distance; trigger a refresh michael@0: startRefresh(); michael@0: handled = true; michael@0: break; michael@0: } else { michael@0: // Just track the user's movement michael@0: setTriggerPercentage( michael@0: mAccelerateInterpolator.getInterpolation( michael@0: yDiff / mDistanceToTriggerSync)); michael@0: float offsetTop = yDiff; michael@0: if (mPrevY > eventY) { michael@0: offsetTop = yDiff - mTouchSlop; michael@0: } michael@0: // Removed this call to disable "rubber-band" overscroll effect. michael@0: // updateContentOffsetTop((int) offsetTop); michael@0: if (mPrevY > eventY && (mTarget.getTop() < mTouchSlop)) { michael@0: // If the user puts the view back at the top, we michael@0: // don't need to. This shouldn't be considered michael@0: // cancelling the gesture as the user can restart from the top. michael@0: removeCallbacks(mCancel); michael@0: } else { michael@0: updatePositionTimeout(); michael@0: } michael@0: mPrevY = event.getY(); michael@0: handled = true; michael@0: } michael@0: } michael@0: } michael@0: break; michael@0: case MotionEvent.ACTION_UP: michael@0: case MotionEvent.ACTION_CANCEL: michael@0: if (mDownEvent != null) { michael@0: mDownEvent.recycle(); michael@0: mDownEvent = null; michael@0: } michael@0: break; michael@0: } michael@0: return handled; michael@0: } michael@0: michael@0: private void startRefresh() { michael@0: removeCallbacks(mCancel); michael@0: mReturnToStartPosition.run(); michael@0: setRefreshing(true); michael@0: mListener.onRefresh(); michael@0: } michael@0: michael@0: private void updateContentOffsetTop(int targetTop) { michael@0: final int currentTop = mTarget.getTop(); michael@0: if (targetTop > mDistanceToTriggerSync) { michael@0: targetTop = (int) mDistanceToTriggerSync; michael@0: } else if (targetTop < 0) { michael@0: targetTop = 0; michael@0: } michael@0: setTargetOffsetTopAndBottom(targetTop - currentTop); michael@0: } michael@0: michael@0: private void setTargetOffsetTopAndBottom(int offset) { michael@0: mTarget.offsetTopAndBottom(offset); michael@0: mCurrentTargetOffsetTop = mTarget.getTop(); michael@0: } michael@0: michael@0: private void updatePositionTimeout() { michael@0: removeCallbacks(mCancel); michael@0: postDelayed(mCancel, RETURN_TO_ORIGINAL_POSITION_TIMEOUT); michael@0: } michael@0: michael@0: /** michael@0: * Classes that wish to be notified when the swipe gesture correctly michael@0: * triggers a refresh should implement this interface. michael@0: */ michael@0: public interface OnRefreshListener { michael@0: public void onRefresh(); michael@0: } michael@0: michael@0: /** michael@0: * Simple AnimationListener to avoid having to implement unneeded methods in michael@0: * AnimationListeners. michael@0: */ michael@0: private class BaseAnimationListener implements AnimationListener { michael@0: @Override michael@0: public void onAnimationStart(Animation animation) { michael@0: } michael@0: michael@0: @Override michael@0: public void onAnimationEnd(Animation animation) { michael@0: } michael@0: michael@0: @Override michael@0: public void onAnimationRepeat(Animation animation) { michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * The only modification to this class is the shape of the refresh indicator to be a rectangle michael@0: * rather than a circle. michael@0: */ michael@0: private static final class SwipeProgressBar { michael@0: // Default progress animation colors are grays. michael@0: private final static int COLOR1 = 0xB3000000; michael@0: private final static int COLOR2 = 0x80000000; michael@0: private final static int COLOR3 = 0x4d000000; michael@0: private final static int COLOR4 = 0x1a000000; michael@0: michael@0: // The duration of the animation cycle. michael@0: private static final int ANIMATION_DURATION_MS = 2000; michael@0: michael@0: // The duration of the animation to clear the bar. michael@0: private static final int FINISH_ANIMATION_DURATION_MS = 1000; michael@0: michael@0: // Interpolator for varying the speed of the animation. michael@0: private static final Interpolator INTERPOLATOR = BakedBezierInterpolator.getInstance(); michael@0: michael@0: private final Paint mPaint = new Paint(); michael@0: private final RectF mClipRect = new RectF(); michael@0: private float mTriggerPercentage; michael@0: private long mStartTime; michael@0: private long mFinishTime; michael@0: private boolean mRunning; michael@0: michael@0: // Colors used when rendering the animation, michael@0: private int mColor1; michael@0: private int mColor2; michael@0: private int mColor3; michael@0: private int mColor4; michael@0: private View mParent; michael@0: michael@0: private Rect mBounds = new Rect(); michael@0: michael@0: public SwipeProgressBar(View parent) { michael@0: mParent = parent; michael@0: mColor1 = COLOR1; michael@0: mColor2 = COLOR2; michael@0: mColor3 = COLOR3; michael@0: mColor4 = COLOR4; michael@0: } michael@0: michael@0: /** michael@0: * Set the four colors used in the progress animation. The first color will michael@0: * also be the color of the bar that grows in response to a user swipe michael@0: * gesture. michael@0: * michael@0: * @param color1 Integer representation of a color. michael@0: * @param color2 Integer representation of a color. michael@0: * @param color3 Integer representation of a color. michael@0: * @param color4 Integer representation of a color. michael@0: */ michael@0: void setColorScheme(int color1, int color2, int color3, int color4) { michael@0: mColor1 = color1; michael@0: mColor2 = color2; michael@0: mColor3 = color3; michael@0: mColor4 = color4; michael@0: } michael@0: michael@0: /** michael@0: * Update the progress the user has made toward triggering the swipe michael@0: * gesture. and use this value to update the percentage of the trigger that michael@0: * is shown. michael@0: */ michael@0: void setTriggerPercentage(float triggerPercentage) { michael@0: mTriggerPercentage = triggerPercentage; michael@0: mStartTime = 0; michael@0: ViewCompat.postInvalidateOnAnimation(mParent); michael@0: } michael@0: michael@0: /** michael@0: * Start showing the progress animation. michael@0: */ michael@0: void start() { michael@0: if (!mRunning) { michael@0: mTriggerPercentage = 0; michael@0: mStartTime = AnimationUtils.currentAnimationTimeMillis(); michael@0: mRunning = true; michael@0: mParent.postInvalidate(); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Stop showing the progress animation. michael@0: */ michael@0: void stop() { michael@0: if (mRunning) { michael@0: mTriggerPercentage = 0; michael@0: mFinishTime = AnimationUtils.currentAnimationTimeMillis(); michael@0: mRunning = false; michael@0: mParent.postInvalidate(); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * @return Return whether the progress animation is currently running. michael@0: */ michael@0: boolean isRunning() { michael@0: return mRunning || mFinishTime > 0; michael@0: } michael@0: michael@0: void draw(Canvas canvas) { michael@0: final int width = mBounds.width(); michael@0: final int height = mBounds.height(); michael@0: final int cx = width / 2; michael@0: final int cy = height / 2; michael@0: boolean drawTriggerWhileFinishing = false; michael@0: int restoreCount = canvas.save(); michael@0: canvas.clipRect(mBounds); michael@0: michael@0: if (mRunning || (mFinishTime > 0)) { michael@0: long now = AnimationUtils.currentAnimationTimeMillis(); michael@0: long elapsed = (now - mStartTime) % ANIMATION_DURATION_MS; michael@0: long iterations = (now - mStartTime) / ANIMATION_DURATION_MS; michael@0: float rawProgress = (elapsed / (ANIMATION_DURATION_MS / 100f)); michael@0: michael@0: // If we're not running anymore, that means we're running through michael@0: // the finish animation. michael@0: if (!mRunning) { michael@0: // If the finish animation is done, don't draw anything, and michael@0: // don't repost. michael@0: if ((now - mFinishTime) >= FINISH_ANIMATION_DURATION_MS) { michael@0: mFinishTime = 0; michael@0: return; michael@0: } michael@0: michael@0: // Otherwise, use a 0 opacity alpha layer to clear the animation michael@0: // from the inside out. This layer will prevent the circles from michael@0: // drawing within its bounds. michael@0: long finishElapsed = (now - mFinishTime) % FINISH_ANIMATION_DURATION_MS; michael@0: float finishProgress = (finishElapsed / (FINISH_ANIMATION_DURATION_MS / 100f)); michael@0: float pct = (finishProgress / 100f); michael@0: // Radius of the circle is half of the screen. michael@0: float clearRadius = width / 2 * INTERPOLATOR.getInterpolation(pct); michael@0: mClipRect.set(cx - clearRadius, 0, cx + clearRadius, height); michael@0: canvas.saveLayerAlpha(mClipRect, 0, 0); michael@0: // Only draw the trigger if there is a space in the center of michael@0: // this refreshing view that needs to be filled in by the michael@0: // trigger. If the progress view is just still animating, let it michael@0: // continue animating. michael@0: drawTriggerWhileFinishing = true; michael@0: } michael@0: michael@0: // First fill in with the last color that would have finished drawing. michael@0: if (iterations == 0) { michael@0: canvas.drawColor(mColor1); michael@0: } else { michael@0: if (rawProgress >= 0 && rawProgress < 25) { michael@0: canvas.drawColor(mColor4); michael@0: } else if (rawProgress >= 25 && rawProgress < 50) { michael@0: canvas.drawColor(mColor1); michael@0: } else if (rawProgress >= 50 && rawProgress < 75) { michael@0: canvas.drawColor(mColor2); michael@0: } else { michael@0: canvas.drawColor(mColor3); michael@0: } michael@0: } michael@0: michael@0: // Then draw up to 4 overlapping concentric circles of varying radii, based on how far michael@0: // along we are in the cycle. michael@0: // progress 0-50 draw mColor2 michael@0: // progress 25-75 draw mColor3 michael@0: // progress 50-100 draw mColor4 michael@0: // progress 75 (wrap to 25) draw mColor1 michael@0: if ((rawProgress >= 0 && rawProgress <= 25)) { michael@0: float pct = (((rawProgress + 25) * 2) / 100f); michael@0: drawCircle(canvas, cx, cy, mColor1, pct); michael@0: } michael@0: if (rawProgress >= 0 && rawProgress <= 50) { michael@0: float pct = ((rawProgress * 2) / 100f); michael@0: drawCircle(canvas, cx, cy, mColor2, pct); michael@0: } michael@0: if (rawProgress >= 25 && rawProgress <= 75) { michael@0: float pct = (((rawProgress - 25) * 2) / 100f); michael@0: drawCircle(canvas, cx, cy, mColor3, pct); michael@0: } michael@0: if (rawProgress >= 50 && rawProgress <= 100) { michael@0: float pct = (((rawProgress - 50) * 2) / 100f); michael@0: drawCircle(canvas, cx, cy, mColor4, pct); michael@0: } michael@0: if ((rawProgress >= 75 && rawProgress <= 100)) { michael@0: float pct = (((rawProgress - 75) * 2) / 100f); michael@0: drawCircle(canvas, cx, cy, mColor1, pct); michael@0: } michael@0: if (mTriggerPercentage > 0 && drawTriggerWhileFinishing) { michael@0: // There is some portion of trigger to draw. Restore the canvas, michael@0: // then draw the trigger. Otherwise, the trigger does not appear michael@0: // until after the bar has finished animating and appears to michael@0: // just jump in at a larger width than expected. michael@0: canvas.restoreToCount(restoreCount); michael@0: restoreCount = canvas.save(); michael@0: canvas.clipRect(mBounds); michael@0: drawTrigger(canvas, cx, cy); michael@0: } michael@0: // Keep running until we finish out the last cycle. michael@0: ViewCompat.postInvalidateOnAnimation(mParent); michael@0: } else { michael@0: // Otherwise if we're in the middle of a trigger, draw that. michael@0: if (mTriggerPercentage > 0 && mTriggerPercentage <= 1.0) { michael@0: drawTrigger(canvas, cx, cy); michael@0: } michael@0: } michael@0: canvas.restoreToCount(restoreCount); michael@0: } michael@0: michael@0: private void drawTrigger(Canvas canvas, int cx, int cy) { michael@0: mPaint.setColor(mColor1); michael@0: // Use a rectangle to instead of a circle as per UX. michael@0: // canvas.drawCircle(cx, cy, cx * mTriggerPercentage, mPaint); michael@0: canvas.drawRect(cx - cx * mTriggerPercentage, 0, cx + cx * mTriggerPercentage, michael@0: mBounds.bottom, mPaint); michael@0: } michael@0: michael@0: /** michael@0: * Draws a circle centered in the view. michael@0: * michael@0: * @param canvas the canvas to draw on michael@0: * @param cx the center x coordinate michael@0: * @param cy the center y coordinate michael@0: * @param color the color to draw michael@0: * @param pct the percentage of the view that the circle should cover michael@0: */ michael@0: private void drawCircle(Canvas canvas, float cx, float cy, int color, float pct) { michael@0: mPaint.setColor(color); michael@0: canvas.save(); michael@0: canvas.translate(cx, cy); michael@0: float radiusScale = INTERPOLATOR.getInterpolation(pct); michael@0: canvas.scale(radiusScale, radiusScale); michael@0: canvas.drawCircle(0, 0, cx, mPaint); michael@0: canvas.restore(); michael@0: } michael@0: michael@0: /** michael@0: * Set the drawing bounds of this SwipeProgressBar. michael@0: */ michael@0: void setBounds(int left, int top, int right, int bottom) { michael@0: mBounds.left = left; michael@0: mBounds.top = top; michael@0: mBounds.right = right; michael@0: mBounds.bottom = bottom; michael@0: } michael@0: } michael@0: michael@0: private static final class BakedBezierInterpolator implements Interpolator { michael@0: private static final BakedBezierInterpolator INSTANCE = new BakedBezierInterpolator(); michael@0: michael@0: public final static BakedBezierInterpolator getInstance() { michael@0: return INSTANCE; michael@0: } michael@0: michael@0: /** michael@0: * Use getInstance instead of instantiating. michael@0: */ michael@0: private BakedBezierInterpolator() { michael@0: super(); michael@0: } michael@0: michael@0: /** michael@0: * Lookup table values. michael@0: * Generated using a Bezier curve from (0,0) to (1,1) with control points: michael@0: * P0 (0,0) michael@0: * P1 (0.4, 0) michael@0: * P2 (0.2, 1.0) michael@0: * P3 (1.0, 1.0) michael@0: * michael@0: * Values sampled with x at regular intervals between 0 and 1. michael@0: */ michael@0: private static final float[] VALUES = new float[] { michael@0: 0.0f, 0.0002f, 0.0009f, 0.0019f, 0.0036f, 0.0059f, 0.0086f, 0.0119f, 0.0157f, 0.0209f, michael@0: 0.0257f, 0.0321f, 0.0392f, 0.0469f, 0.0566f, 0.0656f, 0.0768f, 0.0887f, 0.1033f, 0.1186f, michael@0: 0.1349f, 0.1519f, 0.1696f, 0.1928f, 0.2121f, 0.237f, 0.2627f, 0.2892f, 0.3109f, 0.3386f, michael@0: 0.3667f, 0.3952f, 0.4241f, 0.4474f, 0.4766f, 0.5f, 0.5234f, 0.5468f, 0.5701f, 0.5933f, michael@0: 0.6134f, 0.6333f, 0.6531f, 0.6698f, 0.6891f, 0.7054f, 0.7214f, 0.7346f, 0.7502f, 0.763f, michael@0: 0.7756f, 0.7879f, 0.8f, 0.8107f, 0.8212f, 0.8326f, 0.8415f, 0.8503f, 0.8588f, 0.8672f, michael@0: 0.8754f, 0.8833f, 0.8911f, 0.8977f, 0.9041f, 0.9113f, 0.9165f, 0.9232f, 0.9281f, 0.9328f, michael@0: 0.9382f, 0.9434f, 0.9476f, 0.9518f, 0.9557f, 0.9596f, 0.9632f, 0.9662f, 0.9695f, 0.9722f, michael@0: 0.9753f, 0.9777f, 0.9805f, 0.9826f, 0.9847f, 0.9866f, 0.9884f, 0.9901f, 0.9917f, 0.9931f, michael@0: 0.9944f, 0.9955f, 0.9964f, 0.9973f, 0.9981f, 0.9986f, 0.9992f, 0.9995f, 0.9998f, 1.0f, 1.0f michael@0: }; michael@0: michael@0: private static final float STEP_SIZE = 1.0f / (VALUES.length - 1); michael@0: michael@0: @Override michael@0: public float getInterpolation(float input) { michael@0: if (input >= 1.0f) { michael@0: return 1.0f; michael@0: } michael@0: michael@0: if (input <= 0f) { michael@0: return 0f; michael@0: } michael@0: michael@0: int position = Math.min( michael@0: (int)(input * (VALUES.length - 1)), michael@0: VALUES.length - 2); michael@0: michael@0: float quantized = position * STEP_SIZE; michael@0: float difference = input - quantized; michael@0: float weight = difference / STEP_SIZE; michael@0: michael@0: return VALUES[position] + weight * (VALUES[position + 1] - VALUES[position]); michael@0: } michael@0: michael@0: } michael@0: }