Index: third_party/android_swipe_refresh/java/src/chromium/third_party/android/swiperefresh/SwipeRefreshLayout.java |
diff --git a/third_party/android_swipe_refresh/java/src/chromium/third_party/android/swiperefresh/SwipeRefreshLayout.java b/third_party/android_swipe_refresh/java/src/chromium/third_party/android/swiperefresh/SwipeRefreshLayout.java |
new file mode 100644 |
index 0000000000000000000000000000000000000000..dfd4cc2ec7e66c110cd886b5734253af8a8438f2 |
--- /dev/null |
+++ b/third_party/android_swipe_refresh/java/src/chromium/third_party/android/swiperefresh/SwipeRefreshLayout.java |
@@ -0,0 +1,923 @@ |
+/* |
+ * 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.support.v4.view.MotionEventCompat; |
+import android.support.v4.view.ViewCompat; |
+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 int INVALID_POINTER = -1; |
+ 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 View mTarget; // the target of the gesture |
+ private OnRefreshListener mListener; |
+ private boolean mRefreshing = false; |
+ private int mTouchSlop; |
+ 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; |
+ private int mActivePointerId = INVALID_POINTER; |
+ // 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 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 { |
+ 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(); |
+ } |
+ }; |
+ |
+ 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); |
+ |
+ mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); |
+ |
+ 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(); |
+ ViewCompat.setChildrenDrawingOrderEnabled(this, 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; |
+ } |
+ |
+ /** |
+ * 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); |
+ } |
+ 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 { |
+ ViewCompat.setScaleX(mCircleView, progress); |
+ ViewCompat.setScaleY(mCircleView, progress); |
+ } |
+ } |
+ |
+ private void setRefreshing(boolean refreshing, final boolean notify) { |
+ if (mRefreshing != refreshing) { |
+ mNotify = notify; |
+ ensureTarget(); |
+ mRefreshing = refreshing; |
+ if (mRefreshing) { |
+ animateOffsetToCorrectPosition(mCurrentTargetOffsetTop, mRefreshListener); |
+ } else { |
+ startScaleDownAnimation(mRefreshListener); |
+ } |
+ } |
+ } |
+ |
+ private void startScaleDownAnimation(Animation.AnimationListener listener) { |
+ 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; |
+ } |
+ |
+ /** |
+ * Set the background color of the progress spinner disc. |
+ * |
+ * @param colorRes Resource id of the color. |
+ */ |
+ public void setProgressBackgroundColor(int colorRes) { |
+ mCircleView.setBackgroundColor(colorRes); |
+ mProgress.setBackgroundColor(getResources().getColor(colorRes)); |
+ } |
+ |
+ /** |
+ * @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) { |
+ ensureTarget(); |
+ mProgress.setColorSchemeColors(colors); |
+ } |
+ |
+ /** |
+ * @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) { |
+ for (int i = 0; i < getChildCount(); i++) { |
+ View child = getChildAt(i); |
+ if (!child.equals(mCircleView)) { |
+ mTarget = child; |
+ break; |
+ } |
+ } |
+ } |
+ } |
+ |
+ /** |
+ * 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(); |
+ final int height = getMeasuredHeight(); |
+ if (getChildCount() == 0) { |
+ return; |
+ } |
+ if (mTarget == null) { |
+ ensureTarget(); |
+ } |
+ if (mTarget == null) { |
+ return; |
+ } |
+ final View child = mTarget; |
+ final int childLeft = getPaddingLeft(); |
+ final int childTop = getPaddingTop(); |
+ final int childWidth = width - getPaddingLeft() - getPaddingRight(); |
+ final int childHeight = height - getPaddingTop() - getPaddingBottom(); |
+ child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight); |
+ 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); |
+ if (mTarget == null) { |
+ ensureTarget(); |
+ } |
+ if (mTarget == null) { |
+ return; |
+ } |
+ mTarget.measure(MeasureSpec.makeMeasureSpec( |
+ getMeasuredWidth() - getPaddingLeft() - getPaddingRight(), |
+ MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec( |
+ getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY)); |
+ mCircleView.measure(MeasureSpec.makeMeasureSpec(mCircleWidth, MeasureSpec.EXACTLY), |
+ MeasureSpec.makeMeasureSpec(mCircleHeight, MeasureSpec.EXACTLY)); |
+ if (!mUsingCustomStart && !mOriginalOffsetCalculated) { |
+ mOriginalOffsetCalculated = true; |
+ mCurrentTargetOffsetTop = mOriginalOffsetTop = -mCircleView.getMeasuredHeight(); |
+ } |
+ mCircleViewIndex = -1; |
+ // Get the index of the circleview. |
+ for (int index = 0; index < getChildCount(); index++) { |
+ if (getChildAt(index) == mCircleView) { |
+ mCircleViewIndex = index; |
+ break; |
+ } |
+ } |
+ } |
+ |
+ /** |
+ * @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(); |
+ |
+ final int action = MotionEventCompat.getActionMasked(ev); |
+ |
+ if (mReturningToStart && action == MotionEvent.ACTION_DOWN) { |
+ mReturningToStart = false; |
+ } |
+ |
+ if (!isEnabled() || mReturningToStart || canChildScrollUp() || mRefreshing) { |
+ // Fail fast if we're not in a state where a swipe is possible |
+ return false; |
+ } |
+ |
+ switch (action) { |
+ case MotionEvent.ACTION_DOWN: |
+ setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCircleView.getTop(), true); |
+ mActivePointerId = MotionEventCompat.getPointerId(ev, 0); |
+ mIsBeingDragged = false; |
+ final float initialMotionY = getMotionEventY(ev, mActivePointerId); |
+ if (initialMotionY == -1) { |
+ return false; |
+ } |
+ mInitialMotionY = initialMotionY; |
+ |
+ case MotionEvent.ACTION_MOVE: |
+ if (mActivePointerId == INVALID_POINTER) { |
+ Log.e(LOG_TAG, "Got ACTION_MOVE event but don't have an active pointer id."); |
+ return false; |
+ } |
+ |
+ final float y = getMotionEventY(ev, mActivePointerId); |
+ if (y == -1) { |
+ return false; |
+ } |
+ final float yDiff = y - mInitialMotionY; |
+ if (yDiff > mTouchSlop && !mIsBeingDragged) { |
+ mIsBeingDragged = true; |
+ mProgress.setAlpha(STARTING_PROGRESS_ALPHA); |
+ } |
+ break; |
+ |
+ case MotionEventCompat.ACTION_POINTER_UP: |
+ onSecondaryPointerUp(ev); |
+ break; |
+ |
+ case MotionEvent.ACTION_UP: |
+ case MotionEvent.ACTION_CANCEL: |
+ mIsBeingDragged = false; |
+ mActivePointerId = INVALID_POINTER; |
+ break; |
+ } |
+ |
+ return mIsBeingDragged; |
+ } |
+ |
+ private float getMotionEventY(MotionEvent ev, int activePointerId) { |
+ final int index = MotionEventCompat.findPointerIndex(ev, activePointerId); |
+ if (index < 0) { |
+ return -1; |
+ } |
+ return MotionEventCompat.getY(ev, index); |
+ } |
+ |
+ @Override |
+ public void requestDisallowInterceptTouchEvent(boolean b) { |
+ // Nope. |
+ } |
+ |
+ private boolean isAnimationRunning(Animation animation) { |
+ return animation != null && animation.hasStarted() && !animation.hasEnded(); |
+ } |
+ |
+ @Override |
+ public boolean onTouchEvent(MotionEvent ev) { |
+ final int action = MotionEventCompat.getActionMasked(ev); |
+ |
+ if (mReturningToStart && action == MotionEvent.ACTION_DOWN) { |
+ mReturningToStart = false; |
+ } |
+ |
+ if (!isEnabled() || mReturningToStart || canChildScrollUp()) { |
+ // Fail fast if we're not in a state where a swipe is possible |
+ return false; |
+ } |
+ |
+ switch (action) { |
+ case MotionEvent.ACTION_DOWN: |
+ mActivePointerId = MotionEventCompat.getPointerId(ev, 0); |
+ mIsBeingDragged = false; |
+ break; |
+ |
+ case MotionEvent.ACTION_MOVE: { |
+ final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); |
+ if (pointerIndex < 0) { |
+ Log.e(LOG_TAG, "Got ACTION_MOVE event but have an invalid active pointer id."); |
+ return false; |
+ } |
+ |
+ final float y = MotionEventCompat.getY(ev, pointerIndex); |
+ final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE; |
+ if (mIsBeingDragged) { |
+ mProgress.showArrow(true); |
+ float originalDragPercent = overscrollTop / mTotalDragDistance; |
+ if (originalDragPercent < 0) { |
+ return false; |
+ } |
+ 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) { |
+ ViewCompat.setScaleX(mCircleView, 1f); |
+ ViewCompat.setScaleY(mCircleView, 1f); |
+ } |
+ if (overscrollTop < mTotalDragDistance) { |
+ if (mScale) { |
+ setAnimationProgress(overscrollTop / mTotalDragDistance); |
+ } |
+ if (mProgress.getAlpha() > STARTING_PROGRESS_ALPHA |
+ && !isAnimationRunning(mAlphaStartAnimation)) { |
+ // Animate the alpha |
+ startProgressAlphaStartAnimation(); |
+ } |
+ float strokeStart = (float) (adjustedPercent * .8f); |
+ mProgress.setStartEndTrim(0f, Math.min(MAX_PROGRESS_ANGLE, strokeStart)); |
+ mProgress.setArrowScale(Math.min(1f, adjustedPercent)); |
+ } else { |
+ if (mProgress.getAlpha() < MAX_ALPHA |
+ && !isAnimationRunning(mAlphaMaxAnimation)) { |
+ // Animate the alpha |
+ startProgressAlphaMaxAnimation(); |
+ } |
+ } |
+ float rotation = (-0.25f + .4f * adjustedPercent + tensionPercent * 2) * .5f; |
+ mProgress.setProgressRotation(rotation); |
+ setTargetOffsetTopAndBottom(targetY - mCurrentTargetOffsetTop, |
+ true /* requires update */); |
+ } |
+ break; |
+ } |
+ case MotionEventCompat.ACTION_POINTER_DOWN: { |
+ final int index = MotionEventCompat.getActionIndex(ev); |
+ mActivePointerId = MotionEventCompat.getPointerId(ev, index); |
+ break; |
+ } |
+ |
+ case MotionEventCompat.ACTION_POINTER_UP: |
+ onSecondaryPointerUp(ev); |
+ break; |
+ |
+ case MotionEvent.ACTION_UP: |
+ case MotionEvent.ACTION_CANCEL: { |
+ if (mActivePointerId == INVALID_POINTER) { |
+ if (action == MotionEvent.ACTION_UP) { |
+ Log.e(LOG_TAG, "Got ACTION_UP event but don't have an active pointer id."); |
+ } |
+ return false; |
+ } |
+ final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); |
+ final float y = MotionEventCompat.getY(ev, pointerIndex); |
+ final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE; |
+ mIsBeingDragged = false; |
+ if (overscrollTop > mTotalDragDistance) { |
+ setRefreshing(true, true /* notify */); |
+ } else { |
+ // cancel refresh |
+ mRefreshing = false; |
+ mProgress.setStartEndTrim(0f, 0f); |
+ Animation.AnimationListener listener = null; |
+ if (!mScale) { |
+ listener = new Animation.AnimationListener() { |
+ |
+ @Override |
+ public void onAnimationStart(Animation animation) { |
+ } |
+ |
+ @Override |
+ public void onAnimationEnd(Animation animation) { |
+ if (!mScale) { |
+ startScaleDownAnimation(null); |
+ } |
+ } |
+ |
+ @Override |
+ public void onAnimationRepeat(Animation animation) { |
+ } |
+ |
+ }; |
+ } |
+ animateOffsetToStartPosition(mCurrentTargetOffsetTop, listener); |
+ mProgress.showArrow(false); |
+ } |
+ mActivePointerId = INVALID_POINTER; |
+ return false; |
+ } |
+ } |
+ |
+ return true; |
+ } |
+ |
+ 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 */); |
+ } |
+ }; |
+ |
+ 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 = ViewCompat.getScaleX(mCircleView); |
+ } |
+ 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(); |
+ } |
+ } |
+ |
+ private void onSecondaryPointerUp(MotionEvent ev) { |
+ final int pointerIndex = MotionEventCompat.getActionIndex(ev); |
+ final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex); |
+ if (pointerId == mActivePointerId) { |
+ // This was our active pointer going up. Choose a new |
+ // active pointer and adjust accordingly. |
+ final int newPointerIndex = pointerIndex == 0 ? 1 : 0; |
+ mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex); |
+ } |
+ } |
+ |
+ /** |
+ * Classes that wish to be notified when the swipe gesture correctly |
+ * triggers a refresh should implement this interface. |
+ */ |
+ public interface OnRefreshListener { |
+ public void onRefresh(); |
+ } |
+} |