| /* |
| * 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 org.chromium.third_party.android.swiperefresh; |
| |
| import android.content.Context; |
| import android.content.res.Resources; |
| import android.content.res.TypedArray; |
| import android.util.AttributeSet; |
| import android.util.DisplayMetrics; |
| import android.util.Log; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.view.ViewConfiguration; |
| import android.view.ViewGroup; |
| 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> |
| */ |
| public class SwipeRefreshLayout extends ViewGroup { |
| // Maps to ProgressBar.Large style |
| public static final int LARGE = MaterialProgressDrawable.LARGE; |
| // Maps to ProgressBar default style |
| public static final int DEFAULT = MaterialProgressDrawable.DEFAULT; |
| |
| private static final String LOG_TAG = SwipeRefreshLayout.class.getSimpleName(); |
| |
| private static final int MAX_ALPHA = 255; |
| private static final int STARTING_PROGRESS_ALPHA = (int) (.3f * MAX_ALPHA); |
| |
| private static final int CIRCLE_DIAMETER = 40; |
| private static final int CIRCLE_DIAMETER_LARGE = 56; |
| |
| private static final float DECELERATE_INTERPOLATION_FACTOR = 2f; |
| private static final float DRAG_RATE = .5f; |
| |
| // Max amount of circle that can be filled by progress during swipe gesture, |
| // where 1.0 is a full circle |
| private static final float MAX_PROGRESS_ANGLE = .8f; |
| |
| private static final int SCALE_DOWN_DURATION = 150; |
| |
| private static final int ALPHA_ANIMATION_DURATION = 300; |
| |
| private static final int ANIMATE_TO_TRIGGER_DURATION = 200; |
| |
| private static final int ANIMATE_TO_START_DURATION = 200; |
| |
| // Default background for the progress spinner |
| private static final int CIRCLE_BG_LIGHT = 0xFFFAFAFA; |
| // Default offset in dips from the top of the view to where the progress spinner should stop |
| private static final int DEFAULT_CIRCLE_TARGET = 64; |
| |
| private OnRefreshListener mListener; |
| private OnResetListener mResetListener; |
| private boolean mRefreshing = false; |
| private float mTotalDragDistance = -1; |
| private int mMediumAnimationDuration; |
| private int mCurrentTargetOffsetTop; |
| // Whether or not the starting offset has been determined. |
| private boolean mOriginalOffsetCalculated = false; |
| |
| private float mInitialMotionY; |
| private boolean mIsBeingDragged; |
| // Whether this item is scaled up rather than clipped |
| private boolean mScale; |
| |
| // Target is returning to its start offset because it was cancelled or a |
| // refresh was triggered. |
| private boolean mReturningToStart; |
| private final DecelerateInterpolator mDecelerateInterpolator; |
| private static final int[] LAYOUT_ATTRS = new int[] { |
| android.R.attr.enabled |
| }; |
| |
| private CircleImageView mCircleView; |
| private int mCircleViewIndex = -1; |
| |
| protected int mFrom; |
| |
| private float mStartingScale; |
| |
| protected int mOriginalOffsetTop; |
| |
| private MaterialProgressDrawable mProgress; |
| |
| private Animation mScaleAnimation; |
| |
| private Animation mScaleDownAnimation; |
| |
| private Animation mAlphaStartAnimation; |
| |
| private Animation mAlphaMaxAnimation; |
| |
| private Animation mScaleDownToStartAnimation; |
| |
| private Animation.AnimationListener mCancelAnimationListener; |
| |
| private float mSpinnerFinalOffset; |
| |
| private boolean mNotify; |
| |
| private int mCircleWidth; |
| |
| private int mCircleHeight; |
| |
| // Whether the client has set a custom starting position; |
| private boolean mUsingCustomStart; |
| |
| private Animation.AnimationListener mRefreshListener = new Animation.AnimationListener() { |
| @Override |
| public void onAnimationStart(Animation animation) { |
| } |
| |
| @Override |
| public void onAnimationRepeat(Animation animation) { |
| } |
| |
| @Override |
| public void onAnimationEnd(Animation animation) { |
| if (mRefreshing) { |
| // Make sure the progress view is fully visible |
| mProgress.setAlpha(MAX_ALPHA); |
| mProgress.start(); |
| if (mNotify) { |
| if (mListener != null) { |
| mListener.onRefresh(); |
| } |
| } |
| } else { |
| reset(); |
| } |
| mCurrentTargetOffsetTop = mCircleView.getTop(); |
| } |
| }; |
| |
| // Chrome-specific additions. |
| private float mTotalMotionY; |
| // Minimum number of pull updates necessary to trigger a refresh. |
| private static int MIN_PULLS_TO_ACTIVATE = 3; |
| // Multiplier for the default top offset relative to the size of the progress spinner. |
| private static final float DEFAULT_OFFSET_TOP_MULTIPLIER = 1.05f; |
| |
| private void setColorViewAlpha(int targetAlpha) { |
| mCircleView.getBackground().setAlpha(targetAlpha); |
| mProgress.setAlpha(targetAlpha); |
| } |
| |
| /** |
| * The refresh indicator starting and resting position is always positioned |
| * near the top of the refreshing content. This position is a consistent |
| * location, but can be adjusted in either direction based on whether or not |
| * there is a toolbar or actionbar present. |
| * |
| * @param scale Set to true if there is no view at a higher z-order than |
| * where the progress spinner is set to appear. |
| * @param start The offset in pixels from the top of this view at which the |
| * progress spinner should appear. |
| * @param end The offset in pixels from the top of this view at which the |
| * progress spinner should come to rest after a successful swipe |
| * gesture. |
| */ |
| public void setProgressViewOffset(boolean scale, int start, int end) { |
| mScale = scale; |
| mCircleView.setVisibility(View.GONE); |
| mOriginalOffsetTop = mCurrentTargetOffsetTop = start; |
| mSpinnerFinalOffset = end; |
| mUsingCustomStart = true; |
| mCircleView.invalidate(); |
| } |
| |
| /** |
| * The refresh indicator resting position is always positioned near the top |
| * of the refreshing content. This position is a consistent location, but |
| * can be adjusted in either direction based on whether or not there is a |
| * toolbar or actionbar present. |
| * |
| * @param scale Set to true if there is no view at a higher z-order than |
| * where the progress spinner is set to appear. |
| * @param end The offset in pixels from the top of this view at which the |
| * progress spinner should come to rest after a successful swipe |
| * gesture. |
| */ |
| public void setProgressViewEndTarget(boolean scale, int end) { |
| mSpinnerFinalOffset = end; |
| mScale = scale; |
| mCircleView.invalidate(); |
| } |
| |
| /** |
| * One of DEFAULT, or LARGE. |
| */ |
| public void setSize(int size) { |
| if (size != MaterialProgressDrawable.LARGE && size != MaterialProgressDrawable.DEFAULT) { |
| return; |
| } |
| final DisplayMetrics metrics = getResources().getDisplayMetrics(); |
| if (size == MaterialProgressDrawable.LARGE) { |
| mCircleHeight = mCircleWidth = (int) (CIRCLE_DIAMETER_LARGE * metrics.density); |
| } else { |
| mCircleHeight = mCircleWidth = (int) (CIRCLE_DIAMETER * metrics.density); |
| } |
| // force the bounds of the progress circle inside the circle view to |
| // update by setting it to null before updating its size and then |
| // re-setting it |
| mCircleView.setImageDrawable(null); |
| mProgress.updateSizes(size); |
| mCircleView.setImageDrawable(mProgress); |
| } |
| |
| /** |
| * 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); |
| |
| mMediumAnimationDuration = getResources().getInteger( |
| android.R.integer.config_mediumAnimTime); |
| |
| setWillNotDraw(false); |
| mDecelerateInterpolator = new DecelerateInterpolator(DECELERATE_INTERPOLATION_FACTOR); |
| |
| final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS); |
| setEnabled(a.getBoolean(0, true)); |
| a.recycle(); |
| |
| final DisplayMetrics metrics = getResources().getDisplayMetrics(); |
| mCircleWidth = (int) (CIRCLE_DIAMETER * metrics.density); |
| mCircleHeight = (int) (CIRCLE_DIAMETER * metrics.density); |
| |
| createProgressView(); |
| setChildrenDrawingOrderEnabled(true); |
| // the absolute offset has to take into account that the circle starts at an offset |
| mSpinnerFinalOffset = DEFAULT_CIRCLE_TARGET * metrics.density; |
| mTotalDragDistance = mSpinnerFinalOffset; |
| } |
| |
| protected int getChildDrawingOrder(int childCount, int i) { |
| if (mCircleViewIndex < 0) { |
| return i; |
| } else if (i == childCount - 1) { |
| // Draw the selected child last |
| return mCircleViewIndex; |
| } else if (i >= mCircleViewIndex) { |
| // Move the children after the selected child earlier one |
| return i + 1; |
| } else { |
| // Keep the children before the selected child the same |
| return i; |
| } |
| } |
| |
| private void createProgressView() { |
| mCircleView = new CircleImageView(getContext(), CIRCLE_BG_LIGHT, CIRCLE_DIAMETER/2); |
| mProgress = new MaterialProgressDrawable(getContext(), this); |
| mProgress.setBackgroundColor(CIRCLE_BG_LIGHT); |
| mCircleView.setImageDrawable(mProgress); |
| mCircleView.setVisibility(View.GONE); |
| addView(mCircleView); |
| } |
| |
| /** |
| * Set the listener to be notified when a refresh is triggered via the swipe |
| * gesture. |
| */ |
| public void setOnRefreshListener(OnRefreshListener listener) { |
| mListener = listener; |
| } |
| |
| /** |
| * Set the reset listener to be notified when a reset is triggered. |
| */ |
| public void setOnResetListener(OnResetListener listener) { |
| mResetListener = listener; |
| } |
| |
| /** |
| * Pre API 11, alpha is used to make the progress circle appear instead of scale. |
| */ |
| private boolean isAlphaUsedForScale() { |
| return android.os.Build.VERSION.SDK_INT < 11; |
| } |
| |
| /** |
| * 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 (refreshing && mRefreshing != refreshing) { |
| // scale and show |
| mRefreshing = refreshing; |
| int endTarget = 0; |
| if (!mUsingCustomStart) { |
| endTarget = (int) (mSpinnerFinalOffset + mOriginalOffsetTop); |
| } else { |
| endTarget = (int) mSpinnerFinalOffset; |
| } |
| setTargetOffsetTopAndBottom(endTarget - mCurrentTargetOffsetTop, |
| true /* requires update */); |
| mNotify = false; |
| startScaleUpAnimation(mRefreshListener); |
| } else { |
| setRefreshing(refreshing, false /* notify */); |
| } |
| } |
| |
| private void startScaleUpAnimation(AnimationListener listener) { |
| mCircleView.setVisibility(View.VISIBLE); |
| if (android.os.Build.VERSION.SDK_INT >= 11) { |
| // Pre API 11, alpha is used in place of scale up to show the |
| // progress circle appearing. |
| // Don't adjust the alpha during appearance otherwise. |
| mProgress.setAlpha(MAX_ALPHA); |
| } |
| if (mScaleAnimation == null) { |
| mScaleAnimation = new Animation() { |
| @Override |
| public void applyTransformation(float interpolatedTime, Transformation t) { |
| setAnimationProgress(interpolatedTime); |
| } |
| }; |
| mScaleAnimation.setDuration(mMediumAnimationDuration); |
| } |
| if (listener != null) { |
| mCircleView.setAnimationListener(listener); |
| } |
| mCircleView.clearAnimation(); |
| mCircleView.startAnimation(mScaleAnimation); |
| } |
| |
| /** |
| * Pre API 11, this does an alpha animation. |
| * @param progress |
| */ |
| private void setAnimationProgress(float progress) { |
| if (isAlphaUsedForScale()) { |
| setColorViewAlpha((int) (progress * MAX_ALPHA)); |
| } else { |
| mCircleView.setScaleX(progress); |
| mCircleView.setScaleY(progress); |
| } |
| } |
| |
| private void setRefreshing(boolean refreshing, final boolean notify) { |
| if (mRefreshing != refreshing) { |
| mNotify = notify; |
| mRefreshing = refreshing; |
| if (mRefreshing) { |
| animateOffsetToCorrectPosition(mCurrentTargetOffsetTop, mRefreshListener); |
| } else { |
| startScaleDownAnimation(mRefreshListener); |
| } |
| } |
| } |
| |
| private void startScaleDownAnimation(Animation.AnimationListener listener) { |
| if (mScaleDownAnimation == null) { |
| mScaleDownAnimation = new Animation() { |
| @Override |
| public void applyTransformation(float interpolatedTime, Transformation t) { |
| setAnimationProgress(1 - interpolatedTime); |
| } |
| }; |
| mScaleDownAnimation.setDuration(SCALE_DOWN_DURATION); |
| } |
| mCircleView.setAnimationListener(listener); |
| mCircleView.clearAnimation(); |
| mCircleView.startAnimation(mScaleDownAnimation); |
| } |
| |
| private void startProgressAlphaStartAnimation() { |
| mAlphaStartAnimation = startAlphaAnimation(mProgress.getAlpha(), STARTING_PROGRESS_ALPHA); |
| } |
| |
| private void startProgressAlphaMaxAnimation() { |
| mAlphaMaxAnimation = startAlphaAnimation(mProgress.getAlpha(), MAX_ALPHA); |
| } |
| |
| private Animation startAlphaAnimation(final int startingAlpha, final int endingAlpha) { |
| // Pre API 11, alpha is used in place of scale. Don't also use it to |
| // show the trigger point. |
| if (mScale && isAlphaUsedForScale()) { |
| return null; |
| } |
| Animation alpha = new Animation() { |
| @Override |
| public void applyTransformation(float interpolatedTime, Transformation t) { |
| mProgress |
| .setAlpha((int) (startingAlpha+ ((endingAlpha - startingAlpha) |
| * interpolatedTime))); |
| } |
| }; |
| alpha.setDuration(ALPHA_ANIMATION_DURATION); |
| // Clear out the previous animation listeners. |
| mCircleView.setAnimationListener(null); |
| mCircleView.clearAnimation(); |
| mCircleView.startAnimation(alpha); |
| return alpha; |
| } |
| |
| /** |
| * @deprecated Use {@link #setProgressBackgroundColorSchemeResource(int)} |
| */ |
| @Deprecated |
| public void setProgressBackgroundColor(int colorRes) { |
| setProgressBackgroundColorSchemeResource(colorRes); |
| } |
| |
| /** |
| * Set the background color of the progress spinner disc. |
| * |
| * @param colorRes Resource id of the color. |
| */ |
| public void setProgressBackgroundColorSchemeResource(int colorRes) { |
| setProgressBackgroundColorSchemeColor(getResources().getColor(colorRes)); |
| } |
| |
| /** |
| * Set the background color of the progress spinner disc. |
| * |
| * @param color |
| */ |
| public void setProgressBackgroundColorSchemeColor(int color) { |
| mCircleView.setBackgroundColor(color); |
| mProgress.setBackgroundColor(color); |
| } |
| |
| /** |
| * @deprecated Use {@link #setColorSchemeResources(int...)} |
| */ |
| @Deprecated |
| public void setColorScheme(int... colors) { |
| setColorSchemeResources(colors); |
| } |
| |
| /** |
| * Set the color resources used in the progress animation from color resources. |
| * The first color will also be the color of the bar that grows in response |
| * to a user swipe gesture. |
| * |
| * @param colorResIds |
| */ |
| public void setColorSchemeResources(int... colorResIds) { |
| final Resources res = getResources(); |
| int[] colorRes = new int[colorResIds.length]; |
| for (int i = 0; i < colorResIds.length; i++) { |
| colorRes[i] = res.getColor(colorResIds[i]); |
| } |
| setColorSchemeColors(colorRes); |
| } |
| |
| /** |
| * Set the 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 colors |
| */ |
| public void setColorSchemeColors(int... colors) { |
| mProgress.setColorSchemeColors(colors); |
| } |
| |
| /** |
| * @return Whether the SwipeRefreshWidget is actively showing refresh |
| * progress. |
| */ |
| public boolean isRefreshing() { |
| return mRefreshing; |
| } |
| |
| /** |
| * Set the distance to trigger a sync in dips |
| * |
| * @param distance |
| */ |
| public void setDistanceToTriggerSync(int distance) { |
| mTotalDragDistance = distance; |
| } |
| |
| @Override |
| protected void onLayout(boolean changed, int left, int top, int right, int bottom) { |
| final int width = getMeasuredWidth(); |
| if (getChildCount() == 0) { |
| return; |
| } |
| int circleWidth = mCircleView.getMeasuredWidth(); |
| int circleHeight = mCircleView.getMeasuredHeight(); |
| mCircleView.layout((width / 2 - circleWidth / 2), mCurrentTargetOffsetTop, |
| (width / 2 + circleWidth / 2), mCurrentTargetOffsetTop + circleHeight); |
| } |
| |
| @Override |
| public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
| super.onMeasure(widthMeasureSpec, heightMeasureSpec); |
| mCircleView.measure(MeasureSpec.makeMeasureSpec(mCircleWidth, MeasureSpec.EXACTLY), |
| MeasureSpec.makeMeasureSpec(mCircleHeight, MeasureSpec.EXACTLY)); |
| if (!mUsingCustomStart && !mOriginalOffsetCalculated) { |
| mOriginalOffsetCalculated = true; |
| mCurrentTargetOffsetTop = mOriginalOffsetTop = |
| (int) (-mCircleView.getMeasuredHeight() * DEFAULT_OFFSET_TOP_MULTIPLIER); |
| } |
| mCircleViewIndex = -1; |
| // Get the index of the circleview. |
| for (int index = 0; index < getChildCount(); index++) { |
| if (getChildAt(index) == mCircleView) { |
| mCircleViewIndex = index; |
| break; |
| } |
| } |
| } |
| |
| /** |
| * Start the pull effect. If the effect is disabled or a refresh animation |
| * is currently active, the request will be ignored. |
| * @return whether a new pull sequence has started. |
| */ |
| public boolean start() { |
| if (!isEnabled()) return false; |
| if (mRefreshing) return false; |
| mCircleView.clearAnimation(); |
| mProgress.stop(); |
| // See ACTION_DOWN handling in {@link #onTouchEvent(...)}. |
| setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCircleView.getTop(), true); |
| mTotalMotionY = 0; |
| mIsBeingDragged = true; |
| mProgress.setAlpha(STARTING_PROGRESS_ALPHA); |
| return true; |
| } |
| |
| /** |
| * Apply a pull impulse to the effect. If the effect is disabled or has yet |
| * to start, the pull will be ignored. |
| * @param delta the magnitude of the pull. |
| */ |
| public void pull(float delta) { |
| if (!isEnabled()) return; |
| if (!mIsBeingDragged) return; |
| delta *= DRAG_RATE; |
| float max_delta = mTotalDragDistance / MIN_PULLS_TO_ACTIVATE; |
| delta = Math.max(-max_delta, Math.min(max_delta, delta)); |
| mTotalMotionY += delta; |
| |
| // See ACTION_MOVE handling in {@link #onTouchEvent(...)}. |
| final float overscrollTop = mTotalMotionY; |
| mProgress.showArrow(true); |
| float originalDragPercent = overscrollTop / mTotalDragDistance; |
| if (originalDragPercent < 0) return; |
| float dragPercent = Math.min(1f, Math.abs(originalDragPercent)); |
| float adjustedPercent = (float) Math.max(dragPercent - .4, 0) * 5 / 3; |
| float extraOS = Math.abs(overscrollTop) - mTotalDragDistance; |
| float slingshotDist = mUsingCustomStart ? mSpinnerFinalOffset |
| - mOriginalOffsetTop : mSpinnerFinalOffset; |
| float tensionSlingshotPercent = Math.max(0, |
| Math.min(extraOS, slingshotDist * 2) / slingshotDist); |
| float tensionPercent = (float) ((tensionSlingshotPercent / 4) - Math.pow( |
| (tensionSlingshotPercent / 4), 2)) * 2f; |
| float extraMove = (slingshotDist) * tensionPercent * 2; |
| |
| int targetY = mOriginalOffsetTop |
| + (int) ((slingshotDist * dragPercent) + extraMove); |
| // where 1.0f is a full circle |
| if (mCircleView.getVisibility() != View.VISIBLE) { |
| mCircleView.setVisibility(View.VISIBLE); |
| } |
| if (!mScale) { |
| mCircleView.setScaleX(1f); |
| mCircleView.setScaleY(1f); |
| } |
| if (overscrollTop < mTotalDragDistance) { |
| if (mScale) { |
| setAnimationProgress(overscrollTop / mTotalDragDistance); |
| } |
| } |
| float strokeStart = adjustedPercent * .8f; |
| mProgress.setStartEndTrim(0f, Math.min(MAX_PROGRESS_ANGLE, strokeStart)); |
| mProgress.setArrowScale(Math.min(1f, adjustedPercent)); |
| |
| float alphaStrength = Math.max(0f, Math.min(1f, (dragPercent - .9f) / .1f)); |
| int alpha = STARTING_PROGRESS_ALPHA; |
| alpha += (int) (alphaStrength * (MAX_ALPHA - STARTING_PROGRESS_ALPHA)); |
| mProgress.setAlpha(alpha); |
| |
| float rotation = (-0.25f + .4f * adjustedPercent + tensionPercent * 2) * .5f; |
| mProgress.setProgressRotation(rotation); |
| setTargetOffsetTopAndBottom(targetY - mCurrentTargetOffsetTop, |
| true /* requires update */); |
| } |
| |
| /** |
| * Release the active pull. If no pull has started, the release will be |
| * ignored. If the pull was sufficiently large, the refresh sequence will |
| * be initiated. |
| * @param allowRefresh whether to allow a sufficiently large pull to trigger |
| * the refresh action and animation sequence. |
| */ |
| public void release(boolean allowRefresh) { |
| if (!mIsBeingDragged) return; |
| |
| // See ACTION_UP handling in {@link #onTouchEvent(...)}. |
| mIsBeingDragged = false; |
| final float overscrollTop = mTotalMotionY; |
| if (isEnabled() && allowRefresh && overscrollTop > mTotalDragDistance) { |
| setRefreshing(true, true /* notify */); |
| return; |
| } |
| // cancel refresh |
| mRefreshing = false; |
| mProgress.setStartEndTrim(0f, 0f); |
| Animation.AnimationListener listener = null; |
| if (!mScale) { |
| if (mCancelAnimationListener == null) { |
| mCancelAnimationListener = new Animation.AnimationListener() { |
| |
| @Override |
| public void onAnimationStart(Animation animation) { |
| } |
| |
| @Override |
| public void onAnimationEnd(Animation animation) { |
| if (!mScale) { |
| startScaleDownAnimation(mRefreshListener); |
| } |
| } |
| |
| @Override |
| public void onAnimationRepeat(Animation animation) { |
| } |
| }; |
| } |
| listener = mCancelAnimationListener; |
| } |
| animateOffsetToStartPosition(mCurrentTargetOffsetTop, listener); |
| mProgress.showArrow(false); |
| } |
| |
| /** |
| * Reset the effect, clearing any active animations. |
| */ |
| public void reset() { |
| mIsBeingDragged = false; |
| setRefreshing(false, false /* notify */); |
| mProgress.stop(); |
| mCircleView.setVisibility(View.GONE); |
| setColorViewAlpha(MAX_ALPHA); |
| // Return the circle to its start position |
| if (mScale) { |
| setAnimationProgress(0 /* animation complete and view is hidden */); |
| } else { |
| setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCurrentTargetOffsetTop, |
| true /* requires update */); |
| } |
| mCurrentTargetOffsetTop = mCircleView.getTop(); |
| if (mResetListener != null) { |
| mResetListener.onReset(); |
| } |
| } |
| |
| private void animateOffsetToCorrectPosition(int from, AnimationListener listener) { |
| mFrom = from; |
| mAnimateToCorrectPosition.reset(); |
| mAnimateToCorrectPosition.setDuration(ANIMATE_TO_TRIGGER_DURATION); |
| mAnimateToCorrectPosition.setInterpolator(mDecelerateInterpolator); |
| if (listener != null) { |
| mCircleView.setAnimationListener(listener); |
| } |
| mCircleView.clearAnimation(); |
| mCircleView.startAnimation(mAnimateToCorrectPosition); |
| } |
| |
| private void animateOffsetToStartPosition(int from, AnimationListener listener) { |
| if (mScale) { |
| // Scale the item back down |
| startScaleDownReturnToStartAnimation(from, listener); |
| } else { |
| mFrom = from; |
| mAnimateToStartPosition.reset(); |
| mAnimateToStartPosition.setDuration(ANIMATE_TO_START_DURATION); |
| mAnimateToStartPosition.setInterpolator(mDecelerateInterpolator); |
| if (listener != null) { |
| mCircleView.setAnimationListener(listener); |
| } |
| mCircleView.clearAnimation(); |
| mCircleView.startAnimation(mAnimateToStartPosition); |
| } |
| } |
| |
| private final Animation mAnimateToCorrectPosition = new Animation() { |
| @Override |
| public void applyTransformation(float interpolatedTime, Transformation t) { |
| int targetTop = 0; |
| int endTarget = 0; |
| if (!mUsingCustomStart) { |
| endTarget = (int) (mSpinnerFinalOffset - Math.abs(mOriginalOffsetTop)); |
| } else { |
| endTarget = (int) mSpinnerFinalOffset; |
| } |
| targetTop = (mFrom + (int) ((endTarget - mFrom) * interpolatedTime)); |
| int offset = targetTop - mCircleView.getTop(); |
| setTargetOffsetTopAndBottom(offset, false /* requires update */); |
| mProgress.setArrowScale(1 - interpolatedTime); |
| } |
| }; |
| |
| private void moveToStart(float interpolatedTime) { |
| int targetTop = 0; |
| targetTop = (mFrom + (int) ((mOriginalOffsetTop - mFrom) * interpolatedTime)); |
| int offset = targetTop - mCircleView.getTop(); |
| setTargetOffsetTopAndBottom(offset, false /* requires update */); |
| } |
| |
| private final Animation mAnimateToStartPosition = new Animation() { |
| @Override |
| public void applyTransformation(float interpolatedTime, Transformation t) { |
| moveToStart(interpolatedTime); |
| } |
| }; |
| |
| private void startScaleDownReturnToStartAnimation(int from, |
| Animation.AnimationListener listener) { |
| mFrom = from; |
| if (isAlphaUsedForScale()) { |
| mStartingScale = mProgress.getAlpha(); |
| } else { |
| mStartingScale = mCircleView.getScaleX(); |
| } |
| if (mScaleDownToStartAnimation == null) { |
| mScaleDownToStartAnimation = new Animation() { |
| @Override |
| public void applyTransformation(float interpolatedTime, Transformation t) { |
| float targetScale = (mStartingScale + (-mStartingScale * interpolatedTime)); |
| setAnimationProgress(targetScale); |
| moveToStart(interpolatedTime); |
| } |
| }; |
| mScaleDownToStartAnimation.setDuration(SCALE_DOWN_DURATION); |
| } |
| if (listener != null) { |
| mCircleView.setAnimationListener(listener); |
| } |
| mCircleView.clearAnimation(); |
| mCircleView.startAnimation(mScaleDownToStartAnimation); |
| } |
| |
| private void setTargetOffsetTopAndBottom(int offset, boolean requiresUpdate) { |
| mCircleView.bringToFront(); |
| mCircleView.offsetTopAndBottom(offset); |
| mCurrentTargetOffsetTop = mCircleView.getTop(); |
| if (requiresUpdate && android.os.Build.VERSION.SDK_INT < 11) { |
| invalidate(); |
| } |
| } |
| |
| /** |
| * Classes that wish to be notified when the swipe gesture correctly |
| * triggers a refresh should implement this interface. |
| */ |
| public interface OnRefreshListener { |
| public void onRefresh(); |
| } |
| |
| /** |
| * Classes that wish to be notified when a reset is triggered should |
| * implement this interface. |
| */ |
| public interface OnResetListener { |
| public void onReset(); |
| } |
| } |