Last active
July 8, 2024 09:52
-
-
Save felipecsl/16405f9d3a9ee533c8cd to your computer and use it in GitHub Desktop.
Modified Android SwipeRefreshLayout that does not move down the content view on swipe down (no overscroll)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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 com.felipecsl.android.widget; | |
import android.content.Context; | |
import android.content.res.Resources; | |
import android.content.res.TypedArray; | |
import android.graphics.Canvas; | |
import android.support.v4.view.ViewCompat; | |
import android.util.AttributeSet; | |
import android.util.DisplayMetrics; | |
import android.view.MotionEvent; | |
import android.view.View; | |
import android.view.ViewConfiguration; | |
import android.view.ViewGroup; | |
import android.view.animation.AccelerateInterpolator; | |
import android.view.animation.Animation; | |
import android.view.animation.Animation.AnimationListener; | |
import android.view.animation.DecelerateInterpolator; | |
import android.view.animation.Transformation; | |
import android.widget.AbsListView; | |
/** | |
* The SwipeRefreshLayout should be used whenever the user can refresh the | |
* contents of a view via a vertical swipe gesture. The activity that | |
* instantiates this view should add an OnRefreshListener to be notified | |
* whenever the swipe to refresh gesture is completed. The SwipeRefreshLayout | |
* will notify the listener each and every time the gesture is completed again; | |
* the listener is responsible for correctly determining when to actually | |
* initiate a refresh of its content. If the listener determines there should | |
* not be a refresh, it must call setRefreshing(false) to cancel any visual | |
* indication of a refresh. If an activity wishes to show just the progress | |
* animation, it should call setRefreshing(true). To disable the gesture and progress | |
* animation, call setEnabled(false) on the view. | |
* | |
* <p> This layout should be made the parent of the view that will be refreshed as a | |
* result of the gesture and can only support one direct child. This view will | |
* also be made the target of the gesture and will be forced to match both the | |
* width and the height supplied in this layout. The SwipeRefreshLayout does not | |
* provide accessibility events; instead, a menu item must be provided to allow | |
* refresh of the content wherever this gesture is used.</p> | |
* | |
* ********************************************************************************* | |
* ** Changes start here: ** | |
* | |
* This behaves just like the default support lib SwipeRefreshLayout, | |
* with the only different being that it won't move the contents during the swipe down. | |
* It behaves more like ActionBar-PullToRefresh, meaning that the mTarget view offset | |
* top is not changed. | |
* ********************************************************************************* | |
*/ | |
public class SwipeRefreshLayout extends ViewGroup { | |
private static final long RETURN_TO_ORIGINAL_POSITION_TIMEOUT = 300; | |
private static final float ACCELERATE_INTERPOLATION_FACTOR = 1.5f; | |
private static final float DECELERATE_INTERPOLATION_FACTOR = 2f; | |
private static final float PROGRESS_BAR_HEIGHT = 4; | |
private static final float MAX_SWIPE_DISTANCE_FACTOR = .6f; | |
private static final int REFRESH_TRIGGER_DISTANCE = 200; | |
private SwipeProgressBar mProgressBar; //the thing that shows progress is going | |
private View mTarget; //the content that gets pulled down | |
private int mOriginalOffsetTop; | |
private OnRefreshListener mListener; | |
private MotionEvent mDownEvent; | |
private int mFrom; | |
private boolean mRefreshing = false; | |
private int mTouchSlop; | |
private float mDistanceToTriggerSync = -1; | |
private float mPrevY; | |
private int mMediumAnimationDuration; | |
private float mFromPercentage = 0; | |
private float mCurrPercentage = 0; | |
private int mProgressBarHeight; | |
private int mCurrentTargetOffsetTop; | |
// Target is returning to its start offset because it was cancelled or a | |
// refresh was triggered. | |
private boolean mReturningToStart; | |
private final DecelerateInterpolator mDecelerateInterpolator; | |
private final AccelerateInterpolator mAccelerateInterpolator; | |
private static final int[] LAYOUT_ATTRS = new int[] { | |
android.R.attr.enabled | |
}; | |
private final Animation mAnimateToStartPosition = new Animation() { | |
@Override | |
public void applyTransformation(float interpolatedTime, Transformation t) { | |
int targetTop = 0; | |
if (mFrom != mOriginalOffsetTop) { | |
targetTop = (mFrom + (int)((mOriginalOffsetTop - mFrom) * interpolatedTime)); | |
} | |
int offset = targetTop - mTarget.getTop(); | |
final int currentTop = mTarget.getTop(); | |
if (offset + currentTop < 0) { | |
offset = 0 - currentTop; | |
} | |
setTargetOffsetTopAndBottom(offset); | |
} | |
}; | |
private Animation mShrinkTrigger = new Animation() { | |
@Override | |
public void applyTransformation(float interpolatedTime, Transformation t) { | |
float percent = mFromPercentage + ((0 - mFromPercentage) * interpolatedTime); | |
mProgressBar.setTriggerPercentage(percent); | |
} | |
}; | |
private final AnimationListener mReturnToStartPositionListener = new BaseAnimationListener() { | |
@Override | |
public void onAnimationEnd(Animation animation) { | |
// Once the target content has returned to its start position, reset | |
// the target offset to 0 | |
mCurrentTargetOffsetTop = 0; | |
} | |
}; | |
private final AnimationListener mShrinkAnimationListener = new BaseAnimationListener() { | |
@Override | |
public void onAnimationEnd(Animation animation) { | |
mCurrPercentage = 0; | |
} | |
}; | |
private final Runnable mReturnToStartPosition = new Runnable() { | |
@Override | |
public void run() { | |
mReturningToStart = true; | |
animateOffsetToStartPosition(mCurrentTargetOffsetTop + getPaddingTop(), | |
mReturnToStartPositionListener); | |
} | |
}; | |
// Cancel the refresh gesture and animate everything back to its original state. | |
private final Runnable mCancel = new Runnable() { | |
@Override | |
public void run() { | |
mReturningToStart = true; | |
// Timeout fired since the user last moved their finger; animate the | |
// trigger to 0 and put the target back at its original position | |
if (mProgressBar != null) { | |
mFromPercentage = mCurrPercentage; | |
mShrinkTrigger.setDuration(mMediumAnimationDuration); | |
mShrinkTrigger.setAnimationListener(mShrinkAnimationListener); | |
mShrinkTrigger.reset(); | |
mShrinkTrigger.setInterpolator(mDecelerateInterpolator); | |
startAnimation(mShrinkTrigger); | |
} | |
animateOffsetToStartPosition(mCurrentTargetOffsetTop + getPaddingTop(), | |
mReturnToStartPositionListener); | |
} | |
}; | |
/** | |
* Simple constructor to use when creating a SwipeRefreshLayout from code. | |
* @param context | |
*/ | |
public SwipeRefreshLayout(Context context) { | |
this(context, null); | |
} | |
/** | |
* Constructor that is called when inflating SwipeRefreshLayout from XML. | |
* @param context | |
* @param attrs | |
*/ | |
public SwipeRefreshLayout(Context context, AttributeSet attrs) { | |
super(context, attrs); | |
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); | |
mMediumAnimationDuration = getResources().getInteger( | |
android.R.integer.config_mediumAnimTime); | |
setWillNotDraw(false); | |
mProgressBar = new SwipeProgressBar(this); | |
final DisplayMetrics metrics = getResources().getDisplayMetrics(); | |
mProgressBarHeight = (int) (metrics.density * PROGRESS_BAR_HEIGHT); | |
mDecelerateInterpolator = new DecelerateInterpolator(DECELERATE_INTERPOLATION_FACTOR); | |
mAccelerateInterpolator = new AccelerateInterpolator(ACCELERATE_INTERPOLATION_FACTOR); | |
final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS); | |
setEnabled(a.getBoolean(0, true)); | |
a.recycle(); | |
} | |
@Override | |
public void onAttachedToWindow() { | |
super.onAttachedToWindow(); | |
removeCallbacks(mCancel); | |
removeCallbacks(mReturnToStartPosition); | |
} | |
@Override | |
public void onDetachedFromWindow() { | |
super.onDetachedFromWindow(); | |
removeCallbacks(mReturnToStartPosition); | |
removeCallbacks(mCancel); | |
} | |
private void animateOffsetToStartPosition(int from, AnimationListener listener) { | |
mFrom = from; | |
} | |
/** | |
* Set the listener to be notified when a refresh is triggered via the swipe | |
* gesture. | |
*/ | |
public void setOnRefreshListener(OnRefreshListener listener) { | |
mListener = listener; | |
} | |
private void setTriggerPercentage(float percent) { | |
if (percent == 0f) { | |
// No-op. A null trigger means it's uninitialized, and setting it to zero-percent | |
// means we're trying to reset state, so there's nothing to reset in this case. | |
mCurrPercentage = 0; | |
return; | |
} | |
mCurrPercentage = percent; | |
mProgressBar.setTriggerPercentage(percent); | |
} | |
/** | |
* Notify the widget that refresh state has changed. Do not call this when | |
* refresh is triggered by a swipe gesture. | |
* | |
* @param refreshing Whether or not the view should show refresh progress. | |
*/ | |
public void setRefreshing(boolean refreshing) { | |
if (mRefreshing != refreshing) { | |
ensureTarget(); | |
mCurrPercentage = 0; | |
mRefreshing = refreshing; | |
if (mRefreshing) { | |
mProgressBar.start(); | |
} else { | |
mProgressBar.stop(); | |
} | |
} | |
} | |
/** | |
* 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 colorRes1 Color resource. | |
* @param colorRes2 Color resource. | |
* @param colorRes3 Color resource. | |
* @param colorRes4 Color resource. | |
*/ | |
public void setColorScheme(int colorRes1, int colorRes2, int colorRes3, int colorRes4) { | |
ensureTarget(); | |
final Resources res = getResources(); | |
final int color1 = res.getColor(colorRes1); | |
final int color2 = res.getColor(colorRes2); | |
final int color3 = res.getColor(colorRes3); | |
final int color4 = res.getColor(colorRes4); | |
mProgressBar.setColorScheme(color1, color2, color3,color4); | |
} | |
/** | |
* @return Whether the SwipeRefreshWidget is actively showing refresh | |
* progress. | |
*/ | |
public boolean isRefreshing() { | |
return mRefreshing; | |
} | |
private void ensureTarget() { | |
// Don't bother getting the parent height if the parent hasn't been laid out yet. | |
if (mTarget == null) { | |
if (getChildCount() > 1 && !isInEditMode()) { | |
throw new IllegalStateException( | |
"SwipeRefreshLayout can host only one direct child"); | |
} | |
mTarget = getChildAt(0); | |
mOriginalOffsetTop = mTarget.getTop() + getPaddingTop(); | |
} | |
if (mDistanceToTriggerSync == -1) { | |
if (getParent() != null && ((View)getParent()).getHeight() > 0) { | |
final DisplayMetrics metrics = getResources().getDisplayMetrics(); | |
mDistanceToTriggerSync = (int) Math.min( | |
((View) getParent()) .getHeight() * MAX_SWIPE_DISTANCE_FACTOR, | |
REFRESH_TRIGGER_DISTANCE * metrics.density); | |
} | |
} | |
} | |
@Override | |
public void draw(Canvas canvas) { | |
super.draw(canvas); | |
mProgressBar.draw(canvas); | |
} | |
@Override | |
protected void onLayout(boolean changed, int left, int top, int right, int bottom) { | |
final int width = getMeasuredWidth(); | |
final int height = getMeasuredHeight(); | |
mProgressBar.setBounds(0, 0, width, mProgressBarHeight); | |
if (getChildCount() == 0) { | |
return; | |
} | |
final View child = getChildAt(0); | |
final int childLeft = getPaddingLeft(); | |
final int childTop = mCurrentTargetOffsetTop + getPaddingTop(); | |
final int childWidth = width - getPaddingLeft() - getPaddingRight(); | |
final int childHeight = height - getPaddingTop() - getPaddingBottom(); | |
child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight); | |
} | |
@Override | |
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { | |
super.onMeasure(widthMeasureSpec, heightMeasureSpec); | |
if (getChildCount() > 1 && !isInEditMode()) { | |
throw new IllegalStateException("SwipeRefreshLayout can host only one direct child"); | |
} | |
if (getChildCount() > 0) { | |
getChildAt(0).measure( | |
MeasureSpec.makeMeasureSpec( | |
getMeasuredWidth() - getPaddingLeft() - getPaddingRight(), | |
MeasureSpec.EXACTLY), | |
MeasureSpec.makeMeasureSpec( | |
getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), | |
MeasureSpec.EXACTLY)); | |
} | |
} | |
/** | |
* @return Whether it is possible for the child view of this layout to | |
* scroll up. Override this if the child view is a custom view. | |
*/ | |
public boolean canChildScrollUp() { | |
if (android.os.Build.VERSION.SDK_INT < 14) { | |
if (mTarget instanceof AbsListView) { | |
final AbsListView absListView = (AbsListView) mTarget; | |
return absListView.getChildCount() > 0 | |
&& (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0) | |
.getTop() < absListView.getPaddingTop()); | |
} else { | |
return mTarget.getScrollY() > 0; | |
} | |
} else { | |
return ViewCompat.canScrollVertically(mTarget, -1); | |
} | |
} | |
@Override | |
public boolean onInterceptTouchEvent(MotionEvent ev) { | |
ensureTarget(); | |
boolean handled = false; | |
if (mReturningToStart && ev.getAction() == MotionEvent.ACTION_DOWN) { | |
mReturningToStart = false; | |
} | |
if (isEnabled() && !mReturningToStart && !canChildScrollUp()) { | |
handled = onTouchEvent(ev); | |
} | |
return !handled ? super.onInterceptTouchEvent(ev) : handled; | |
} | |
@Override | |
public void requestDisallowInterceptTouchEvent(boolean b) { | |
// Nope. | |
} | |
@Override | |
public boolean onTouchEvent(MotionEvent event) { | |
final int action = event.getAction(); | |
boolean handled = false; | |
switch (action) { | |
case MotionEvent.ACTION_DOWN: | |
mCurrPercentage = 0; | |
mDownEvent = MotionEvent.obtain(event); | |
mPrevY = mDownEvent.getY(); | |
break; | |
case MotionEvent.ACTION_MOVE: | |
if (mDownEvent != null && !mReturningToStart) { | |
final float eventY = event.getY(); | |
float yDiff = eventY - mDownEvent.getY(); | |
if (yDiff > mTouchSlop) { | |
// User velocity passed min velocity; trigger a refresh | |
if (yDiff > mDistanceToTriggerSync) { | |
// User movement passed distance; trigger a refresh | |
startRefresh(); | |
handled = true; | |
break; | |
} else { | |
// Just track the user's movement | |
setTriggerPercentage( | |
mAccelerateInterpolator.getInterpolation( | |
yDiff / mDistanceToTriggerSync)); | |
float offsetTop = yDiff; | |
if (mPrevY > eventY) { | |
offsetTop = yDiff - mTouchSlop; | |
} | |
updateContentOffsetTop((int) (offsetTop)); | |
if (mPrevY > eventY && (mTarget.getTop() < mTouchSlop)) { | |
// If the user puts the view back at the top, we | |
// don't need to. This shouldn't be considered | |
// cancelling the gesture as the user can restart from the top. | |
removeCallbacks(mCancel); | |
} else { | |
updatePositionTimeout(); | |
} | |
mPrevY = event.getY(); | |
handled = true; | |
} | |
} | |
} | |
break; | |
case MotionEvent.ACTION_UP: | |
case MotionEvent.ACTION_CANCEL: | |
if (mDownEvent != null) { | |
mDownEvent.recycle(); | |
mDownEvent = null; | |
} | |
break; | |
} | |
return handled; | |
} | |
private void startRefresh() { | |
removeCallbacks(mCancel); | |
mReturnToStartPosition.run(); | |
setRefreshing(true); | |
mListener.onRefresh(); | |
} | |
private void updateContentOffsetTop(int targetTop) { | |
final int currentTop = mTarget.getTop(); | |
if (targetTop > mDistanceToTriggerSync) { | |
targetTop = (int) mDistanceToTriggerSync; | |
} else if (targetTop < 0) { | |
targetTop = 0; | |
} | |
setTargetOffsetTopAndBottom(targetTop - currentTop); | |
} | |
private void setTargetOffsetTopAndBottom(int offset) { | |
mCurrentTargetOffsetTop = mTarget.getTop(); | |
} | |
private void updatePositionTimeout() { | |
removeCallbacks(mCancel); | |
postDelayed(mCancel, RETURN_TO_ORIGINAL_POSITION_TIMEOUT); | |
} | |
/** | |
* Classes that wish to be notified when the swipe gesture correctly | |
* triggers a refresh should implement this interface. | |
*/ | |
public interface OnRefreshListener { | |
public void onRefresh(); | |
} | |
/** | |
* Simple AnimationListener to avoid having to implement unneeded methods in | |
* AnimationListeners. | |
*/ | |
private class BaseAnimationListener implements AnimationListener { | |
@Override | |
public void onAnimationStart(Animation animation) { | |
} | |
@Override | |
public void onAnimationEnd(Animation animation) { | |
} | |
@Override | |
public void onAnimationRepeat(Animation animation) { | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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 com.felipecsl.android.widget; | |
import android.view.animation.Interpolator; | |
/** | |
* A pre-baked bezier-curved interpolator for indeterminate progress animations. | |
*/ | |
final class BakedBezierInterpolator implements Interpolator { | |
private static final BakedBezierInterpolator INSTANCE = new BakedBezierInterpolator(); | |
public final static BakedBezierInterpolator getInstance() { | |
return INSTANCE; | |
} | |
/** | |
* Use getInstance instead of instantiating. | |
*/ | |
private BakedBezierInterpolator() { | |
super(); | |
} | |
/** | |
* Lookup table values. | |
* Generated using a Bezier curve from (0,0) to (1,1) with control points: | |
* P0 (0,0) | |
* P1 (0.4, 0) | |
* P2 (0.2, 1.0) | |
* P3 (1.0, 1.0) | |
* | |
* Values sampled with x at regular intervals between 0 and 1. | |
*/ | |
private static final float[] VALUES = new float[] { | |
0.0f, 0.0002f, 0.0009f, 0.0019f, 0.0036f, 0.0059f, 0.0086f, 0.0119f, 0.0157f, 0.0209f, | |
0.0257f, 0.0321f, 0.0392f, 0.0469f, 0.0566f, 0.0656f, 0.0768f, 0.0887f, 0.1033f, 0.1186f, | |
0.1349f, 0.1519f, 0.1696f, 0.1928f, 0.2121f, 0.237f, 0.2627f, 0.2892f, 0.3109f, 0.3386f, | |
0.3667f, 0.3952f, 0.4241f, 0.4474f, 0.4766f, 0.5f, 0.5234f, 0.5468f, 0.5701f, 0.5933f, | |
0.6134f, 0.6333f, 0.6531f, 0.6698f, 0.6891f, 0.7054f, 0.7214f, 0.7346f, 0.7502f, 0.763f, | |
0.7756f, 0.7879f, 0.8f, 0.8107f, 0.8212f, 0.8326f, 0.8415f, 0.8503f, 0.8588f, 0.8672f, | |
0.8754f, 0.8833f, 0.8911f, 0.8977f, 0.9041f, 0.9113f, 0.9165f, 0.9232f, 0.9281f, 0.9328f, | |
0.9382f, 0.9434f, 0.9476f, 0.9518f, 0.9557f, 0.9596f, 0.9632f, 0.9662f, 0.9695f, 0.9722f, | |
0.9753f, 0.9777f, 0.9805f, 0.9826f, 0.9847f, 0.9866f, 0.9884f, 0.9901f, 0.9917f, 0.9931f, | |
0.9944f, 0.9955f, 0.9964f, 0.9973f, 0.9981f, 0.9986f, 0.9992f, 0.9995f, 0.9998f, 1.0f, 1.0f | |
}; | |
private static final float STEP_SIZE = 1.0f / (VALUES.length - 1); | |
@Override | |
public float getInterpolation(float input) { | |
if (input >= 1.0f) { | |
return 1.0f; | |
} | |
if (input <= 0f) { | |
return 0f; | |
} | |
int position = Math.min( | |
(int)(input * (VALUES.length - 1)), | |
VALUES.length - 2); | |
float quantized = position * STEP_SIZE; | |
float difference = input - quantized; | |
float weight = difference / STEP_SIZE; | |
return VALUES[position] + weight * (VALUES[position + 1] - VALUES[position]); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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 com.felipecsl.android.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.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 = BakedBezierInterpolator.getInstance(); | |
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); | |
} | |
/** | |
* 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); | |
} 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; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Very useful! Thank you very much!