源码解析:SwipeRefreshLayout 是如何工作的(上)

1,114 阅读6分钟
原文链接: www.jianshu.com

上一篇博客:《连按两次Back键退出应用程序》,我们从源码的角度分析了一下,虽然这个功能实现起来很简单,但是我们还是能通过系统源码学到很多知识。今天我们从源码的角度来分析一下SwipeRefreshLayout这个神奇的控件吧~

SwipeRefreshLayout是当之无愧的下拉刷新全能王,它继承了ViewGroup,因此,我们的分析角度就可以这么来:构造函数->onMeasure->onLayout->onInterceptTouchEvent->onTouchEvent

首先,我们来分析一下构造函数

    /**
     * 在代码中创建SwipeRefreshLayout的时候,会调用这个方法
     * Simple constructor to use when creating a SwipeRefreshLayout from code.
     *
     * @param context
     */
    public SwipeRefreshLayout(Context context) {
        this(context, null);
    }

    /**
     * 在布局文件中创建SwipeRefreshLayout的时候,会调用这个方法
     * Constructor that is called when inflating SwipeRefreshLayout from XML.
     *
     * @param context
     * @param attrs
     */
    public SwipeRefreshLayout(Context context, AttributeSet attrs) {
        super(context, attrs);

        //ViewConfigurationi:包含了方法和标准的常量用来设置UI的超时、大小和距离
        //mTouchSlop:是一个距离,表示滑动的时候,手的移动要大于这个距离才开始移动控件。
        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();

        mMediumAnimationDuration = getResources().getInteger(
                android.R.integer.config_mediumAnimTime);

        setWillNotDraw(false);
        mDecelerateInterpolator = new DecelerateInterpolator(DECELERATE_INTERPOLATION_FACTOR);

        final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS);
        setEnabled(a.getBoolean(0, true));
        a.recycle();

        final DisplayMetrics metrics = getResources().getDisplayMetrics();
        mCircleDiameter = (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);

        mOriginalOffsetTop = mCurrentTargetOffsetTop = -mCircleDiameter;
        //把那个旋转的小圈圈放到初始位置
        moveToStart(1.0f);
    }

它只实现了两个构造方法,并且在第二个构造方法中对UI进行了一定的初始化
然后我们来看看onMeasure方法

    @Override
    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (mTarget == null) {
            ensureTarget();
        }
        if (mTarget == null) {
            return;
        }
        //mTarget的尺寸为match_parent,除去内边距
        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));
        //如果mOriginalOffsetTop未被初始化并且mUsingCustomStart ?,则将下拉小圆的初始位置设置成默认值
        if (!mUsingCustomStart && !mOriginalOffsetCalculated) {
            mOriginalOffsetCalculated = true;
            mCurrentTargetOffsetTop = mOriginalOffsetTop = -mCircleView.getMeasuredHeight();
        }
        mCircleViewIndex = -1;
        // Get the index of the circleview.获取circleview的索引值,主要是为了后面重载getChildDrawingOrder时要用
        for (int index = 0; index < getChildCount(); index++) {
            if (getChildAt(index) == mCircleView) {
                mCircleViewIndex = index;
                break;
            }
        }
    }

大概就是计算一下各个控件需要的宽高,计算好之后就要考虑如何摆放这些控件了,我们来看看onLayout方法

    @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();
        //将mTarget放在覆盖parent的位置(除去内边距)
        child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
        //将mCircleView放在mTarget的平面位置上面居中,初始化时是完全隐藏在屏幕外的
        int circleWidth = mCircleView.getMeasuredWidth();
        int circleHeight = mCircleView.getMeasuredHeight();
        mCircleView.layout((width / 2 - circleWidth / 2), mCurrentTargetOffsetTop,
                (width / 2 + circleWidth / 2), mCurrentTargetOffsetTop + circleHeight);
    }

控件都摆放好了,接下来就要处理用户的下拉动作了

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

        final int action = MotionEventCompat.getActionMasked(ev);

        //如果当mCircleView正在返回初始位置的同时手指按下了,将标志mReturningToStart复位
        if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
            mReturningToStart = false;
        }

        //如果下拉被禁用、mCircleView正在返回初始位置、mTarget没有到达顶部、
        //正在刷新、mNestedScrollInProgress
        // 不拦截,不处理点击事件,处理权交还mTarget
        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(), 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;
                //如果是滑动动作,将mIsBeingDragged置为true
                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;
        }

        //如果正在被拖拽,拦截该系列的点击事件,并调用自己的onTouchEvent()来处理
        return mIsBeingDragged;
    }

这里是对用户的操作进行拦截,首先调用了第3步中的ensureTarget来确保滑动控件被初始化了,完后可以看到获取了ACTION,这里跟平时直接用getAction方法来获取还是有些区别的,这里用到的getActionMasked方法可以捕获到多个手指的事件,这个后面会说,下面的mReturningToStart这个是布尔变量,初始为false,用来判断滑动控件是否在执行滑动回顶点的动画,在动画开始的时候,会将这个值赋值为true,完后这里判断如果值为true,并且为按下事件,则赋值为false,表示是一次新的滑动了。
if (!isEnabled() || mReturningToStart || canChildScrollUp())

这3个分别是判断是否可用,是否正在执行动画,以及是否能滚动,如果不满足条件,直接返回false,就不往下执行了。
ACTION_DOWN,mLastMotionY是最后滑动的y坐标,mInitialMotionY是按下时初始的y坐标,mActivePointerId是第一个按下手指的id,mIsBeingDragged是一个布尔变量,表示是否正在拖动,mCurrPercentage是默认动画相关的百分比,改造后用不到ACTION_MOVE,首先判断了id是否为-1,是的话直接报错返回(UP或CANCEL时会赋值为-1),完后获取现在的y坐标,并计算与初始y坐标的差是否大于mTouchSlop,这个值是ViewConfiguration类中定义的一个常量,它定义了最小滑动距离,小于这个值表示不是正常的滑动,一般你随便滑动一下肯定超过这个值,ViewConfiguration中定义了很多这种标准值,有兴趣的可以自己去看看,如果大于mTouchSlop,则将mLastMotionY赋值为新值,并且将mIsBeingDragged赋值为true,表示控件开始随手指滑动了ACTION_UP/ACTION_CANCEL,这里很简单,不说了最后还有一个ACTION_POINTER_UP,这里调用了onSecondaryPointerUp方法,这个具体到onTouchEvent中再说

@Override
    public boolean onTouchEvent(MotionEvent ev) {
        final int action = MotionEventCompat.getActionMasked(ev);
        int pointerIndex = -1;

        if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
            mReturningToStart = false;
        }

        //如果被禁用、CircleView正在复位、没到达顶部、mNestedScrollInProgress,直接返回,不处理该事件
        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(LOG_TAG, "Got ACTION_MOVE event but have an invalid active pointer id.");
                    return false;
                }

                final float y = MotionEventCompat.getY(ev, pointerIndex);
                //下拉的总高度
                final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
                if (mIsBeingDragged) {
                    if (overscrollTop > 0) {
                        //spinner可理解为下拉组件,将spinner移到指定的高度
                        //很关键的方法,进入看看
                        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;
                }

                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;
    }

这个控件还没有分析完,等待我的更新吧。我的微信公众号:代码也是人。每天都会分享一篇技术干货,赶紧关注吧!觉得我写得好的话,记得收藏一下哈


代码也是人