Skip to content

Instantly share code, notes, and snippets.

@felipecsl
Last active January 7, 2020 05:54
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save felipecsl/16405f9d3a9ee533c8cd to your computer and use it in GitHub Desktop.
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)
/*
* 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) {
}
}
}
/*
* 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]);
}
}
/*
* 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;
}
}
@deyanm
Copy link

deyanm commented Jul 21, 2018

Very useful! Thank you very much!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment