美文网首页
SwipeRefreshLayout源码浅析

SwipeRefreshLayout源码浅析

作者: Turwe | 来源:发表于2017-05-10 15:27 被阅读149次

    代码版本: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,我们发现调用过程如下:

    滑动事件

    下面看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源码浅析

          本文链接:https://www.haomeiwen.com/subject/piavzttx.html