SwipeRefreshLayout源码浅析

代码版本:support-v4 24.1.1 共1165行
将SwipeRefreshLayout在本文中简称为SRL

涉及到的知识点

从控件的使用效果来看,我们可以了解到关键的知识点如下:

  • 自定义ViewGroup:可以学习一下ViewGroup以及View的一些关键方法的使用
  • 滑动事件:触摸事件传递规则,简单的多点触控相关
  • 动画

本篇也从这三个方面来解读。

涉及到的关键方法

 SwipeRefreshLayout(Context context, AttributeSet attrs) 
 onMeasure (int widthMeasureSpec, int heightMeasureSpec)
 onLayout(boolean changed, int left, int top, int right, int bottom)
 onInterceptTouchEvent(MotionEvent ev) 
 onTouchEvent(MotionEvent ev)
 moveSpinner(float overscrollTop)

我们可以把整个下拉刷新过程分为几个关键的过程部分:

  • 下拉过程
  • 回弹过程
  • 转圈刷新过程

常量和方法
通过一些常量的定义我们可以认识到SRL的View组成部分,以及一些关键的时间点。

CircleImageView mCircleView(继承自:android.support.v7.widget.AppCompatImageView): 即我们下拉刷新的过程中可以见到的圈圈控件,在构造方法中为其设置了一系列属性.

MaterialProgressDrawable mProgress (继承自:Drawable):这是圆圈控件CircleImageView 的内容,在构造方法中可以看到:mCircleView.setImageDrawable(mProgress);对于圆圈内容的控制基本上都是通过MaterialProgressDrawable 来实现的。

View mTarget:一般即SRL的直接子View比如你嵌入的RecyclerView;在ensureTarget()方法中完成对其赋值。

float mTotalDragDistance:超过这个距离值后即认定为下拉刷新;也就是触发下拉刷新的距离;也就是最终停下来转圈圈的位置。

int mCurrentTargetOffsetTo:CircleView实时距离初始位置滑过的距离。

private void setTargetOffsetTopAndBottom(int offset, boolean requiresUpdate):通过调用mCircleView的offsetTopAndBottom来移动CircleView本身。

public boolean canChildScrollUp():判断SRL的内容是否滑动到顶部,return false 说明滑动到顶部可以触发下拉刷新;return true说明未滑动到顶部不触发下拉刷新。该方法在onTouchEvent和onInterceptTouchEvent方法中均被调用来判断。

初始及化绘制过程:

构造方法
初始化各种常量字段,诸如mCircleView的大小,动画插值器,mTotalDragDistance,创建并添加mCircleView,设置一些属性,比如setWillNotDraw(false)可以提高效率。

绘制布局

@Override
    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //确保mTarget不为空
        if (mTarget == null) {
            ensureTarget();
        }
        if (mTarget == null) {
            return;
        }
        //测量mTarget的宽高,去掉padding
        mTarget.measure(MeasureSpec.makeMeasureSpec(
                getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
                MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(
                getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY));
        //测量mCircleView的宽高
        mCircleView.measure(MeasureSpec.makeMeasureSpec(mCircleWidth, MeasureSpec.EXACTLY),
                MeasureSpec.makeMeasureSpec(mCircleHeight, MeasureSpec.EXACTLY));
        //如果没有使用自定义的起始位置,并且起始位置没有被计算过(一般第一次onMeaure的时候会被调用)
        if (!mUsingCustomStart && !mOriginalOffsetCalculated) {
            mOriginalOffsetCalculated = true;
          //计算出当前CircleView移动的位置,即CircleView的自然高度的负值,
          //也就是说CircleView正好在屏幕上边,我们看不到它
            mCurrentTargetOffsetTop = mOriginalOffsetTop = -mCircleView.getMeasuredHeight();
        }
        mCircleViewIndex = -1;
        // Get the index of the circleview.
        // 获取到圆圈view在当前view中的位置;一般情况下mCircleViewIndex都为0;
        // 这个值会在getChildDrawingOrder被调用
        for (int index = 0; index < getChildCount(); index++) {
            if (getChildAt(index) == mCircleView) {
                mCircleViewIndex = index;
                break;
            }
        }
    }

onMeasure的代码和解释如上,做了两件事: 完成子View的大小测量,对变量进行正确赋值。
接下来在onLayout方法中来确定在mTarget和mCircleView在SRL中的位置,onLayout的代码清晰明了,就做了两件事:对mTarget调用layout方法确定其位置,对CircleView调用layout方法确定其位置,在这里就不贴代码啦。

滑动事件处理:

首先我们知道对于ViewGroup的滑动处理流程的伪代码如下:

public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean result = false;             // 默认状态为没有消费过

    if (!onInterceptTouchEvent(ev)) {   // 如果没有拦截交给子View
        result = child.dispatchTouchEvent(ev);
    }

    if (!result) {                      // 如果事件没有被消费,询问自身onTouchEvent
        result = onTouchEvent(ev);
    }

    return result;
}

在SRL中重写了onInterceptTouchEvent和onTouchEvent方法,通过完成一次完整的下拉刷新,打印Log,我们发现调用过程如下:

SwipeRefreshLayout源码浅析_第1张图片
滑动事件

下面看onInterceptTouchEvent的处理,这里它会返回一个mIsBeingDragged (是否被拖拽的布尔值),返回true则正在被拖拽,这时就会把事件分发到事件本身的onTouchEvent中,反之就会交给子View去处理。

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        ensureTarget();

        final int action = MotionEventCompat.getActionMasked(ev);
        // 设置准备开始的状态
        if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
            mReturningToStart = false;
        }
        // 判断一系列状态,决定是否可以下拉刷新,注意canChildScrollUp()方法,
        // 该方法决定了只有滑动到顶部继续下来才能触发下拉刷新
        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:
                // 默认情况下offset为0,不移动;
                // 若初始指定了mOriginalOffsetTop 的大小则意味着,按下的一刻,被移动到了指定位置。
                setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCircleView.getTop(), true);
                mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
                mIsBeingDragged = false;
                final float initialDownY = getMotionEventY(ev, mActivePointerId);
                if (initialDownY == -1) {
                // 不在屏幕范围内不处理
                    return false;
                }
                // mInitialDownY 在ActionMove中用于与移动距离比较,判断是否被拖拽。
                mInitialDownY = initialDownY;
                //可以看到在当前动作下,设置了CircleView的初始位置;获取到了多点触控相关的手指id;拖拽状态置为false;赋值初始按下的位置值:mInitialDownY
                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;
                // 如果滑动的距离大于被视为滑动的最小距离,并且之前的状态为没有被拖动;
                // 这时把拖拽状态mIsBeingDragged置为true;同时记下按下的位置:mInitialMotionY
                //如果mIsBeingDragged为true就会return true;根据事件处理伪代码那么接下来的操作就交给onTouchEvent处理了
                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;
    }

如上就是onInterceptTouchEvent所做的工作,设置初始位置,进行相关赋值操作,根据滑动的实际情况来决定是否把进一步操作转交给onTouchEvent。

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() || mNestedScrollInProgress) {
            // 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: {
                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;
                }
                //获取滑动位置: y,y减去初始按下的位置在撑拖拽比率才是CircleView真正要划过的距离,
                //DRAG_RATE为0.5,所以CircleView滑过的距离,要比你手指移动的距离短。
                final float y = MotionEventCompat.getY(ev, pointerIndex);
                final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
                if (mIsBeingDragged) {
                    if (overscrollTop > 0) {
                      //滑动距离大于0就去移动CircleView,下面这个方法很重要,就是依靠它来完成CircleView的状态变化和移动的
                        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;
                }
                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;
                }
                // 当结束下拉之后这时把拖拽状态置为false,通过finishSpinner来进行刷新操作或者是不到刷新距离回弹到初始位置的操作
                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;
    }

onTouchEvent分析到这里,我们知道了在这个方法中根据用户真实的滑动状况来调用相关方法:moveSpinner方法完成CircleView及其内容的变化,finishSpinner完成手指抬起后的刷新操作,接下来我们看这两个方法

private void moveSpinner(float overscrollTop) {
        mProgress.showArrow(true);// ture,设置下拉过程中展示小箭头
        //移动的距离除以刷新位置的距离得出一个拖拽比率:originalDragPercent 
        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 (mScale) {
            setAnimationProgress(Math.min(1f, overscrollTop / mTotalDragDistance));
        }
        if (overscrollTop < mTotalDragDistance) {
            if (mProgress.getAlpha() > STARTING_PROGRESS_ALPHA
                    && !isAnimationRunning(mAlphaStartAnimation)) {
                // Animate the alpha
                startProgressAlphaStartAnimation();
            }
        } else {
            if (mProgress.getAlpha() < MAX_ALPHA && !isAnimationRunning(mAlphaMaxAnimation)) {
                // Animate the alpha
                startProgressAlphaMaxAnimation();
            }
        }
       //设置内部圈圈开始出现时的大小
        float strokeStart = adjustedPercent * .8f;
        //设置小箭头的样式        
        mProgress.setStartEndTrim(0f, Math.min(MAX_PROGRESS_ANGLE, strokeStart));
        mProgress.setArrowScale(Math.min(1f, adjustedPercent));

        float rotation = (-0.25f + .4f * adjustedPercent + tensionPercent * 2) * .5f;
        mProgress.setProgressRotation(rotation);
        setTargetOffsetTopAndBottom(targetY - mCurrentTargetOffsetTop, true /* requires update */);
    }

在moveSpinner方法中通过对overscrollTop(CircileView移动的距离)和mTotalDragDistance(刷新位置的距离)进行一系列的计算得出在下拉过程中其他元素的变化程度。最后通过setTargetOffsetTopAndBottom方法来真是移动CircleView本身。

接下来我们看finishSpinner(float overscrollTop)方法,在onTouchEvent中拦截到ACTION_UP手势后调用到了 finishSpinner(overscrollTop);传入的overscrollTop为CircileView移动的距离。

private void finishSpinner(float overscrollTop) {
        if (overscrollTop > mTotalDragDistance) {
        //如果超过了CircleView最终下来刷新位置的距离后则认定为触发下拉刷新调用setRefreshing方法
            setRefreshing(true, true /* notify */);
        } else {
            //否则的话取消本次刷新动作
            // cancel refresh
            mRefreshing = false;
            mProgress.setStartEndTrim(0f, 0f);
            Animation.AnimationListener listener = null;
            //mScale默认为false 在这里监听动画结束来进一步操作
            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);
        }
    }

在流程上的分析基本上到这里就结束了,其中一些自定义view和绘制的方法还需要进一步理解。

动画

在SRL中的动画实现方式都是如下套路:
首先直接继承View动画Animation来实现在动画过程中的相应操作。

private final Animation mAnimateToStartPosition = new Animation() {
        @Override
        public void applyTransformation(float interpolatedTime, Transformation t) {
                int targetTop = 0;
        targetTop = (mFrom + (int) ((mOriginalOffsetTop - mFrom) * interpolatedTime));
        int offset = targetTop - mCircleView.getTop();
       //基于Z轴,放到最上层
        mCircleView.bringToFront();
        mCircleView.offsetTopAndBottom(offset);
        mCurrentTargetOffsetTop = mCircleView.getTop();
        if (requiresUpdate && android.os.Build.VERSION.SDK_INT < 11) {
            //将触发onDraw方法
            invalidate();
        }
        }
    };

然后设置动画的相关属性:

  mAnimateToStartPosition.reset();
            mAnimateToStartPosition.setDuration(ANIMATE_TO_START_DsURATION);
            mAnimateToStartPosition.setInterpolator(mDecelerateInterpolator); //减速返回

最后给View设置动画

 mCircleView.clearAnimation();
            mCircleView.startAnimation(mAnimateToStartPosition);

源码中的动画实现都是这一个套路就不多说了。

其他

我们看到SwipeRefreshLayout还实现了两个接口:NestedScrollingParent 和NestedScrollingChild来处理嵌套滑动,关于这个知识点打算再写一篇博客。

总结

到这里我们已经把SRL的基本实现方式和调用细节大致分析了一遍,主要的知识点还是自定义ViewGroup、 触摸事件的处理和动画的使用。另外关于CircleImageView和MaterialProgressDrawable的实现感兴趣的话还值得深入挖掘。

你可能感兴趣的:(SwipeRefreshLayout源码浅析)