Java tutorial
/* * Apache License * * 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 bhav.swipeaction; import android.content.Context; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; import android.graphics.drawable.shapes.Shape; import android.support.annotation.ColorInt; import android.support.annotation.ColorRes; import android.support.v4.view.MotionEventCompat; import android.support.v4.view.NestedScrollingChild; import android.support.v4.view.NestedScrollingChildHelper; import android.support.v4.view.NestedScrollingParent; import android.support.v4.view.NestedScrollingParentHelper; 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; /** * Swipe Down for action. */ public class SwipeAction extends ViewGroup implements NestedScrollingParent, NestedScrollingChild { private static final String TAG = SwipeAction.class.getSimpleName(); private final static int DEF_PADDING_BOTTOM = 24; private final static int DEF_PADDING_TOP = 24; private final static int DEF_PADDING_START = 24; private final static int DEF_PADDING_END = 24; 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 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; //todo : dis v. // private static final int[] LAYOUT_ATTRS = new int[]{ // android.R.attr.enabled // }; private final NestedScrollingParentHelper mNestedScrollingParentHelper; private final NestedScrollingChildHelper mNestedScrollingChildHelper; private final int[] mParentScrollConsumed = new int[2]; private final int[] mParentOffsetInWindow = new int[2]; private final DecelerateInterpolator mDecelerateInterpolator; protected int mFrom; protected int mOriginalOffsetTop; private int iconPaddingRight = 0; private int iconPaddingLeft = 0; private int iconPaddingTop = 0; private int iconPaddingBottom = 0; private Drawable icon; private View mTarget; // the target of the gesture private OnPullListener mListener; private boolean mRefreshing = false; private int mTouchSlop; private float mTotalDragDistance = -1; // If nested scrolling is enabled, the total amount that needed to be // consumed by this as the nested scrolling parent is used in place of the // overscroll determined by MOVE events in the onTouch handler private float mTotalUnconsumed; private boolean mNestedScrollInProgress; private int mMediumAnimationDuration; private int mCurrentTargetOffsetTop; // Whether or not the starting offset has been determined. private boolean mOriginalOffsetCalculated = false; private float mInitialMotionY; private float mInitialDownY; 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 CircleImageView mCircleView; private final Animation mAnimateToStartPosition = new Animation() { @Override public void applyTransformation(float interpolatedTime, Transformation t) { moveToStart(interpolatedTime); } }; private int mCircleViewIndex = -1; private float mStartingScale; 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 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); // mProgress.setArrowScale(1 - interpolatedTime); } }; 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 // icon.setAlpha(MAX_ALPHA); // mProgress.setAlpha(MAX_ALPHA); // mProgress.start(); if (mNotify) { if (mListener != null) { mListener.onPull(); } } mCurrentTargetOffsetTop = mCircleView.getTop(); } else { reset(); } } }; /** * Simple constructor to use when creating a SwipeRefreshLayout from code. * * @param context */ public SwipeAction(Context context) { this(context, null); } /** * Constructor that is called when inflating SwipeRefreshLayout from XML. * * @param context * @param attrs */ public SwipeAction(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); initStyle(context, attrs); // final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SwipeAction); //// setEnabled(a.getBoolean(0, true)); // icon = a.getDrawable(R.styleable.SwipeAction_src); //// if(icon == null) { //// icon = getResources().getDrawable(android.R.drawable.ic_delete); //// } // a.recycle(); final DisplayMetrics metrics = getResources().getDisplayMetrics(); //todo : changeable size v. 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; mNestedScrollingParentHelper = new NestedScrollingParentHelper(this); mNestedScrollingChildHelper = new NestedScrollingChildHelper(this); setNestedScrollingEnabled(true); } private void reset() { mCircleView.clearAnimation(); // 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); } mCurrentTargetOffsetTop = mCircleView.getTop(); } /** * One of DEFAULT, or LARGE. */ // public void setSize(int size) { // if (size != MaterialSearchDrawable.LARGE && size != MaterialSearchDrawable.DEFAULT) { // return; // } // final DisplayMetrics metrics = getResources().getDisplayMetrics(); // if (size == MaterialSearchDrawable.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); // } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); reset(); } private void setColorViewAlpha(int targetAlpha) { mCircleView.getBackground().setAlpha(targetAlpha); // mProgress.setAlpha(targetAlpha); // icon.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(); } 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); mCircleView.setImageDrawable(icon); mCircleView.setPadding(iconPaddingLeft, iconPaddingTop, iconPaddingRight, iconPaddingBottom); mCircleView.setVisibility(View.GONE); addView(mCircleView); } /** * Set the icon inside {@link #mCircleView} * * @param icon Drawable to set as icon */ public void setIcon(Drawable icon) { mCircleView.setImageDrawable(icon); } public void setBackgroundShape(Shape shape) { mCircleView.setBackgroundShape(shape); } public void setBackgroundColor(@ColorRes int colorRes) { mCircleView.setBackgroundColor(colorRes); } // todo : add custom view // public void setCustomView(View customView) { // // } public void setPadding(int iconPaddingLeft, int iconPaddingTop, int iconPaddingRight, int iconPaddingBottom) { mCircleView.setPadding(iconPaddingLeft, iconPaddingTop, iconPaddingRight, iconPaddingBottom); } public boolean isActing() { return mRefreshing; } /** * Set the listener to be notified when a refresh is triggered via the swipe * gesture. */ public void addOnPullListener(OnPullListener listener) { mListener = listener; } /** * Notify the widget that refresh state has changed. Do not call this when * refresh is triggered by a swipe gesture. * * @param show Whether or not the view should show refresh progress. */ public void setWorking(boolean show) { if (show && mRefreshing != show) { // scale and show mRefreshing = show; int endTarget; if (!mUsingCustomStart) { endTarget = (int) (mSpinnerFinalOffset + mOriginalOffsetTop); } else { endTarget = (int) mSpinnerFinalOffset; } setTargetOffsetTopAndBottom(endTarget - mCurrentTargetOffsetTop); mNotify = false; startScaleUpAnimation(mRefreshListener); } else { setWorking(show, 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); // icon.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); } private void setAnimationProgress(float progress) { ViewCompat.setScaleX(mCircleView, progress); ViewCompat.setScaleY(mCircleView, progress); } private void setWorking(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(icon.getAlpha(), STARTING_PROGRESS_ALPHA); } private void startProgressAlphaMaxAnimation() { mAlphaMaxAnimation = startAlphaAnimation(icon.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) { return null; } Animation alpha = new Animation() { @Override public void applyTransformation(float interpolatedTime, Transformation t) { // icon // .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 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 iconColor Color of the icon * @param backgroundColor Color of the background disc */ @ColorInt public void setColors(int iconColor, int backgroundColor) { ensureTarget(); //todo : set color of icon and background // mProgress.setColorSchemeColors(colors); } 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 Distance in dips to trigger sync */ 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() { 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 || mNestedScrollInProgress) { // 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()); mActivePointerId = MotionEventCompat.getPointerId(ev, 0); mIsBeingDragged = false; final float initialDownY = getMotionEventY(ev, mActivePointerId); if (initialDownY == -1) { return false; } mInitialDownY = initialDownY; break; case MotionEvent.ACTION_MOVE: if (mActivePointerId == INVALID_POINTER) { Log.e(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 - mInitialDownY; if (yDiff > mTouchSlop && !mIsBeingDragged) { mInitialMotionY = mInitialDownY + mTouchSlop; mIsBeingDragged = true; // mProgress.setAlpha(STARTING_PROGRESS_ALPHA); // icon.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) { // if this is a List < L or another view that doesn't support nested // scrolling, ignore this request so that the vertical scroll event // isn't stolen if (!((mTarget instanceof AbsListView) || (mTarget != null && !ViewCompat.isNestedScrollingEnabled(mTarget)))) { super.requestDisallowInterceptTouchEvent(b); } } // NestedScrollingParent @Override public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { return isEnabled() && canChildScrollUp() && !mReturningToStart && !mRefreshing && (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0; } @Override public void onNestedScrollAccepted(View child, View target, int axes) { // Reset the counter of how much leftover scroll needs to be consumed. mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes); // Dispatch up to the nested parent startNestedScroll(axes & ViewCompat.SCROLL_AXIS_VERTICAL); mTotalUnconsumed = 0; mNestedScrollInProgress = true; } @Override public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { // If we are in the middle of consuming, a scroll, then we want to move the spinner back up // before allowing the list to scroll if (dy > 0 && mTotalUnconsumed > 0) { if (dy > mTotalUnconsumed) { consumed[1] = dy - (int) mTotalUnconsumed; mTotalUnconsumed = 0; } else { mTotalUnconsumed -= dy; consumed[1] = dy; } moveSpinner(mTotalUnconsumed); } // If a client layout is using a custom start position for the circle // view, they mean to hide it again before scrolling the child view // If we get back to mTotalUnconsumed == 0 and there is more to go, hide // the circle so it isn't exposed if its blocking content is moved if (mUsingCustomStart && dy > 0 && mTotalUnconsumed == 0 && Math.abs(dy - consumed[1]) > 0) { mCircleView.setVisibility(View.GONE); } // Now let our nested parent consume the leftovers final int[] parentConsumed = mParentScrollConsumed; if (dispatchNestedPreScroll(dx - consumed[0], dy - consumed[1], parentConsumed, null)) { consumed[0] += parentConsumed[0]; consumed[1] += parentConsumed[1]; } } @Override public int getNestedScrollAxes() { return mNestedScrollingParentHelper.getNestedScrollAxes(); } @Override public void onStopNestedScroll(View target) { mNestedScrollingParentHelper.onStopNestedScroll(target); mNestedScrollInProgress = false; // Finish the spinner for nested scrolling if we ever consumed any // unconsumed nested scroll if (mTotalUnconsumed > 0) { finishSpinner(mTotalUnconsumed); mTotalUnconsumed = 0; } // Dispatch up our nested parent stopNestedScroll(); } @Override public void onNestedScroll(final View target, final int dxConsumed, final int dyConsumed, final int dxUnconsumed, final int dyUnconsumed) { // Dispatch up to the nested parent first dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, mParentOffsetInWindow); // This is a bit of a hack. Nested scrolling works from the bottom up, and as we are // sometimes between two nested scrolling views, we need a way to be able to know when any // nested scrolling parent has stopped handling events. We do that by using the // 'offset in window 'functionality to see if we have been moved from the event. // This is a decent indication of whether we should take over the event stream or not. final int dy = dyUnconsumed + mParentOffsetInWindow[1]; if (dy < 0) { mTotalUnconsumed += Math.abs(dy); moveSpinner(mTotalUnconsumed); } } @Override public boolean isNestedScrollingEnabled() { return mNestedScrollingChildHelper.isNestedScrollingEnabled(); } // NestedScrollingChild @Override public void setNestedScrollingEnabled(boolean enabled) { mNestedScrollingChildHelper.setNestedScrollingEnabled(enabled); } @Override public boolean startNestedScroll(int axes) { return mNestedScrollingChildHelper.startNestedScroll(axes); } @Override public void stopNestedScroll() { mNestedScrollingChildHelper.stopNestedScroll(); } @Override public boolean hasNestedScrollingParent() { return mNestedScrollingChildHelper.hasNestedScrollingParent(); } @Override public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) { return mNestedScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow); } @Override public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { return mNestedScrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow); } @Override public boolean onNestedPreFling(View target, float velocityX, float velocityY) { return dispatchNestedPreFling(velocityX, velocityY); } @Override public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { return dispatchNestedFling(velocityX, velocityY, consumed); } @Override public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { return mNestedScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed); } @Override public boolean dispatchNestedPreFling(float velocityX, float velocityY) { return mNestedScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY); } private boolean isAnimationRunning(Animation animation) { return animation != null && animation.hasStarted() && !animation.hasEnded(); } private void moveSpinner(float overscrollTop) { float originalDragPercent = overscrollTop / mTotalDragDistance; 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 (icon.getAlpha() > STARTING_PROGRESS_ALPHA && !isAnimationRunning(mAlphaStartAnimation)) { // Animate the alpha startProgressAlphaStartAnimation(); } } else { if (icon.getAlpha() < MAX_ALPHA && !isAnimationRunning(mAlphaMaxAnimation)) { // Animate the alpha startProgressAlphaMaxAnimation(); } } float rotation = (-0.25f + .4f * adjustedPercent + tensionPercent * 2) * .5f; // icon.setAlpha(255); mCircleView.animate().rotation(rotation); setTargetOffsetTopAndBottom(targetY - mCurrentTargetOffsetTop); } private void finishSpinner(float overscrollTop) { if (overscrollTop > mTotalDragDistance) { setWorking(true, true /* notify */); } else { // cancel refresh mRefreshing = false; 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); } } @Override public boolean onTouchEvent(MotionEvent ev) { final int action = MotionEventCompat.getActionMasked(ev); int pointerIndex; if (mReturningToStart && action == MotionEvent.ACTION_DOWN) { mReturningToStart = false; } if (!isEnabled() || mReturningToStart || canChildScrollUp() || mNestedScrollInProgress) { // 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: { pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); if (pointerIndex < 0) { Log.e(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) { if (overscrollTop > 0) { moveSpinner(overscrollTop); } else { return false; } } break; } case MotionEventCompat.ACTION_POINTER_DOWN: { pointerIndex = MotionEventCompat.getActionIndex(ev); if (pointerIndex < 0) { Log.e(TAG, "Got ACTION_POINTER_DOWN event but have an invalid action index."); return false; } mActivePointerId = MotionEventCompat.getPointerId(ev, pointerIndex); break; } case MotionEventCompat.ACTION_POINTER_UP: onSecondaryPointerUp(ev); break; case MotionEvent.ACTION_UP: { pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); if (pointerIndex < 0) { Log.e(TAG, "Got ACTION_UP event but don't have an active pointer id."); return false; } final float y = MotionEventCompat.getY(ev, pointerIndex); final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE; mIsBeingDragged = false; finishSpinner(overscrollTop); mActivePointerId = INVALID_POINTER; return false; } case MotionEvent.ACTION_CANCEL: 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 void moveToStart(float interpolatedTime) { int targetTop; targetTop = (mFrom + (int) ((mOriginalOffsetTop - mFrom) * interpolatedTime)); int offset = targetTop - mCircleView.getTop(); setTargetOffsetTopAndBottom(offset); } private void startScaleDownReturnToStartAnimation(int from, Animation.AnimationListener listener) { mFrom = from; 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) { mCircleView.bringToFront(); mCircleView.offsetTopAndBottom(offset); mCurrentTargetOffsetTop = mCircleView.getTop(); } 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); } } private void initStyle(Context context, AttributeSet attrs) { final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SwipeAction); if (a != null) { if (a.hasValue(R.styleable.SwipeAction_src)) { icon = a.getDrawable(R.styleable.SwipeAction_src); } else { //default back to available icon icon = getResources().getDrawable(android.R.drawable.ic_delete); } iconPaddingBottom = Math .round(a.getDimension(R.styleable.SwipeAction_iconPaddingBottom, DEF_PADDING_BOTTOM)); iconPaddingTop = Math.round(a.getDimension(R.styleable.SwipeAction_iconPaddingTop, DEF_PADDING_TOP)); iconPaddingLeft = Math .round(a.getDimension(R.styleable.SwipeAction_iconPaddingLeft, DEF_PADDING_START)); iconPaddingRight = Math .round(a.getDimension(R.styleable.SwipeAction_iconPaddingRight, DEF_PADDING_END)); a.recycle(); } } /** * Classes that wish to be notified when the swipe gesture correctly * triggers a refresh should implement this interface. */ public interface OnPullListener { void onPull(); } }