滑动刷新效果原理

来源:互联网 发布:人工智能百度云资源 编辑:程序博客网 时间:2024/05/29 15:28

主要使用到的类:

  • NestedScrollingChild
  • NestedScrollingParent
    参考这里
    参考这里
    NestedScrolling提供了一套父 View 和子 View 滑动交互机制。要完成这样的交互,父 View 需要实现 NestedScrollingParent 接口,而子 View 需要实现 NestedScrollingChild 接口。
    实现 NestedScrollingChild
    首先来说NestedScrollingChild。如果你有一个可以滑动的 View,需要被用来作为嵌入滑动的子 View,就必须实现本接口。在此 View 中,包含一个 NestedScrollingChildHelper 辅助类。NestedScrollingChild接口的实现,基本上就是调用本 Helper 类的对应的函数即可,因为 Helper 类中已经实现好了 Child 和 Parent 交互的逻辑。原来的 View 的处理 Touch 事件,并实现滑动的逻辑大体上不需要改变。
    需要做的就是,如果要准备开始滑动了,需要告诉 Parent,你要准备进入滑动状态了,调用startNestedScroll()。你在滑动之前,先问一下你的 Parent 是否需要滑动,也就是调用dispatchNestedPreScroll()。如果父类滑动了一定距离,你需要重新计算一下父类滑动后剩下给你的滑动距 离余量。然后,你自己进行余下的滑动。最后,如果滑动距离还有剩余,你就再问一下,Parent 是否需要在继续滑动你剩下的距离,也就是调用dispatchNestedScroll()。
    以上是一些基本原理,有了上面的基本思路,可以参考这篇 文章 ,这里面有原理的详细解析。如果还是不清楚, 这里 有对应的代码可以参考。
    实现 NestedScrollingParent
    作为一个可以嵌入 NestedScrollingChild 的父 View,需要实现NestedScrollingParent,这个接口方法和NestedScrollingChild大致有一一对应的关系。同样, 也有一个 NestedScrollingParentHelper 辅助类来默默的帮助你实现和 Child 交互的逻辑。滑动动作是 Child 主动发起,Parent 就收滑动回调并作出响应。
    从上面的 Child 分析可知,滑动开始的调用startNestedScroll(),Parent 收到onStartNestedScroll()回调,决定是否需要配合 Child 一起进行处理滑动,如果需要配合,还会回调onNestedScrollAccepted()。
    每次滑动前,Child 先询问 Parent 是否需要滑动,即dispatchNestedPreScroll(),这就回调到 Parent 的onNestedPreScroll(),Parent 可以在这个回调中“劫持”掉 Child 的滑动,也就是先于 Child 滑动。
    Child 滑动以后,会调用onNestedScroll(),回调到 Parent 的onNestedScroll(),这里就是 Child 滑动后,剩下的给 Parent 处理,也就是 后于 Child 滑动。
    最后,滑动结束,调用onStopNestedScroll()表示本次处理结束。
    其实,除了上面的 Scroll 相关的调用和回调,还有 Fling 相关的调用和回调,处理逻辑基本一致。

下面这个类是V4包中提供的一个实用类,实现滑动刷新主要就靠他了。
代码分析:
需要关注的方法:

  • SwipeRefreshLayout(Context context, AttributeSet attrs)构造函数
  • getChildDrawingOrder(int childCount, int i)保证progress最后一个被绘制
  • onLayout(boolean changed, int left, int top, int right, int bottom)确定view位置
  • onMeasure(int widthMeasureSpec, int heightMeasureSpec)确定view大小
  • onInterceptTouchEvent(MotionEvent ev)适当的时候终止时间传递
  • onTouchEvent(MotionEvent ev)对手势的处理
package android.support.v4.widget;import android.content.Context;import android.content.res.Resources;import android.content.res.TypedArray;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;//这里给出重要的注释,简单的地方就不写了/** * SwipeRefreshLayout应该被用在想实现垂直滑动跟新内容的地方。使用这个View的Activity要添加OnRefreshListener监听,当发生滑动事件的时候会受到通知。在监听中应该在完成刷新操作后确认刷新操作完成(调用setRefreshing(false))。如果一个Activity只是想要显示一个progress,可以调用setRefreshing(true)。取消View的手势监听要调用setEnabled(false); */public class SwipeRefreshLayout extends ViewGroup implements NestedScrollingParent,        NestedScrollingChild {    // 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;    // 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 final NestedScrollingParentHelper mNestedScrollingParentHelper;    private final NestedScrollingChildHelper mNestedScrollingChildHelper;    private final int[] mParentScrollConsumed = new int[2];    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 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);//设置为false,不会调用draw方法        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);//创建progress        createProgressView();        //设置当前view添加childView的顺序由getChildDrawingOrder方法决定。因为目前继承自Viewgroup        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);    }//指定添加View的方式,吧progress放在最后一个添加//基本逻辑,mCircleViewIndex记录了progress的位置。如果添加的是Progress之前的View,直接返回。如果是progress之后的View那么就i+1.为什么要加1呢?可以这么想,View的位置是在OnLayout中指定的,而View是存放在一个List中。添加View的时候会遍历list,从0开始取。一共childCount个view ,i表示当前要添加第i个。当添加到mCircleViewIndex个时,因为我想讲progress添加到左后,所以要跳过mCircleViewIndex这个,取下一个。我想,够清楚了。    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;    }    /**    设置当前View刷新状态改变(正在刷新  true OR  false) 不能再进行刷新操作的时候调用     * 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;    }    /**     * @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(@ColorRes int colorRes) {        setProgressBackgroundColorSchemeColor(getResources().getColor(colorRes));    }    /**     * Set the background color of the progress spinner disc.     *     * @param color     */    public void setProgressBackgroundColorSchemeColor(@ColorInt int color) {        mCircleView.setBackgroundColor(color);        mProgress.setBackgroundColor(color);    }    /**     * @deprecated Use {@link #setColorSchemeResources(int...)}     */    @Deprecated    public void setColorScheme(@ColorInt 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(@ColorRes 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     */    @ColorInt    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;            }        }    }    /**     * Get the diameter of the progress circle that is displayed as part of the     * swipe to refresh layout. This is not valid until a measure pass has     * completed.     *     * @return Diameter in pixels of the progress circle view.     */    public int getProgressCircleDiameter() {        return mCircleView != null ?mCircleView.getMeasuredHeight() : 0;    }    /**     * @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 ViewCompat.canScrollVertically(mTarget, -1) || 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 initialDownY = getMotionEventY(ev, mActivePointerId);                if (initialDownY == -1) {                    return false;                }                mInitialDownY = initialDownY;                break;            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 - mInitialDownY;                if (yDiff > mTouchSlop && !mIsBeingDragged) {                    mInitialMotionY = mInitialDownY + mTouchSlop;                    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) {        // 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 ((android.os.Build.VERSION.SDK_INT < 21 && mTarget instanceof AbsListView)                || (mTarget != null && !ViewCompat.isNestedScrollingEnabled(mTarget))) {            // Nope.        } else {            super.requestDisallowInterceptTouchEvent(b);        }    }    // NestedScrollingParent    @Override    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {        if (isEnabled() && !mReturningToStart && !canChildScrollUp() && !mRefreshing                && (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0) {            // Dispatch up to the nested parent            startNestedScroll(nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL);            return true;        }        return false;    }    @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);        mTotalUnconsumed = 0;    }    @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);        // 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(View target, int dxConsumed, int dyConsumed, int dxUnconsumed,            int dyUnconsumed) {        if (dyUnconsumed < 0) {            dyUnconsumed = Math.abs(dyUnconsumed);            mTotalUnconsumed += dyUnconsumed;            moveSpinner(mTotalUnconsumed);        }        // Dispatch up to the nested parent        dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dxConsumed, null);    }    // NestedScrollingChild    @Override    public void setNestedScrollingEnabled(boolean enabled) {        mNestedScrollingChildHelper.setNestedScrollingEnabled(enabled);    }    @Override    public boolean isNestedScrollingEnabled() {        return mNestedScrollingChildHelper.isNestedScrollingEnabled();    }    @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) {        mProgress.showArrow(true);        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 (mProgress.getAlpha() > STARTING_PROGRESS_ALPHA                    && !isAnimationRunning(mAlphaStartAnimation)) {                // Animate the alpha                startProgressAlphaStartAnimation();            }            float strokeStart = 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 */);    }//滑动完成,通知监听器    private void finishSpinner(float overscrollTop) {        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);        }    }/**对手势的处理对手势的说明:在android4.0之后添加了对多点触控的支持。public final int getAction() {        return mAction;    }对于返回的手势值 mAction一般都是ACTION_DOWN。ACTION_UP。 ACTION_MOVE。ACTION_CANCEL。ACTION_OUTSIDE(手指超出屏幕),ACTION_POINTER_DOWN(有一个非主要的手指按下了),ACTION_POINTER_UP这些事件类型。但是为了对多点触控有更好的支持,对每一种事件类型又添加了下表索引。即为每一个事件类型指明是哪一个手指。例如:如果mAction的值是0x0000,那么低八位表示事件类型,高八位指明下标索引。getActionMasked()是使用 mAction &0xff,返回的是事件类型。getAction()返回的是mActiongetActionIndex()返回索引值*/    @Override    public boolean onTouchEvent(MotionEvent ev) {、        final int action = MotionEventCompat.getActionMasked(ev);        int pointerIndex = -1;        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:            //记录手势id                mActivePointerId = MotionEventCompat.getPointerId(ev, 0);                mIsBeingDragged = false;                break;            case MotionEvent.ACTION_MOVE: {            //得到当前ID值对应的索引                pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);                if (pointerIndex < 0) {                //无效的ID                    Log.e(LOG_TAG, "Got ACTION_MOVE event but have an invalid active pointer id.");                    return false;                }                final float y = MotionEventCompat.getY(ev, pointerIndex);                //mInitialMotionY在onInterceptTouchEvent()中初始化的                final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;                if (mIsBeingDragged) {                    if (overscrollTop > 0) {                    //滑动距离达到指定长度,把progress往下移动一段                        moveSpinner(overscrollTop);                    } else {                        return false;                    }                }                break;            }            case MotionEventCompat.ACTION_POINTER_DOWN: {            //有其他手指按下                pointerIndex = MotionEventCompat.getActionIndex(ev);                if (pointerIndex < 0) {                    Log.e(LOG_TAG, "Got ACTION_POINTER_DOWN event but have an invalid action index.");                    return false;                }                //更新手势ID                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(LOG_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 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 = 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();    }}

github

0 0
原创粉丝点击