NestedScrolling机制详解

综述

嵌套滑动.gif

上图是一个非常常见的嵌套滑动UI交互,实现这样的效果,大致有如下三种思路:

  1. 基于普通的事件分发机制

  2. 基于NestedScrolling机制

  3. 基于CoordinatorLayout与Behavior

以上三种思路从原理上循序渐进,逐层封装。由于本文主要介绍嵌套滑动,会主要介绍第二种方案及其原理,第一种会稍微讲解一下。

Demo布局

<com.threeloe.nestscrolling.nest.ScrollHeaderLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/scrollHeaderLayout"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">
        <Button
            android:id="@+id/header"
            android:layout_width="match_parent"
            android:layout_height="200dp"
            android:background="@color/colorPrimary"
            android:gravity="center"
            android:text="Header"/>

        <android.support.design.widget.TabLayout
            android:id="@+id/tabLayout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>
    </LinearLayout>

    <android.support.v4.view.ViewPager
        android:id="@+id/viewPager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
    />
</com.threeloe.nestscrolling.nest.ScrollHeaderLayout>

无论采用哪种实现方式,布局都分为上部分的Header和下部分的ViewPager两部分。

传统的事件分发机制

优点:

灵活性最高。

缺点:

处理细节多,难度大,需要对事件分发机制, 多点触控,滑动,fling,以及一些周边API等都比较清楚。

基本思路

要完成上述效果,在竖直滑动的情况下,上滑时先让外层的父View滚动,到滚动的最大距离时候,再让子View开始滚动。下滑时如果子View滑动距离不是0的话,先让子View滑动,然后让父View滑动。因此一次滑动中的事件需要再父View和子View中切换传递。

复习一下事件分发机制:

事件序列:从手指按下知道抬起,中间经历一个ACTION_DONW ,多个ACTION_MOVE和一个ACTION_UP

事件分发机制.png

一般情况下我们处理滑动冲突,重写onInterceptTouchEvent方法即可,但是一旦onInterceptTouchEvent方法返回true,那么该事件序列以后的事件都会直接给父View处理,这种情况在处理滑动冲突是是可行的。但是在我们上面的案例因为对于一个事件序列需要交替得在子View和父View中传递,如果重写该方法的话,需要我们自己再合适时机手动派发一些事件。

因此更为简单的做法不如直接重写dispatchTouchEvent方法,以下代码只是处理了单手指滑动的情况,没有考虑多点触控,也没有处理fling。

如上我们需要判断isInnerScrollViewTop(),即内部的View滑动距离是否为0。因此父View需要知道滑动的子View到底是谁,需要外界告诉。

override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
    when (ev.action) {
        MotionEvent.ACTION_DOWN -> {
            mLastX = ev.x
            mLastY = ev.y
        }
        MotionEvent.ACTION_MOVE -> {
            if (!mIsReadyToDragHorizontal) {
                var dy = (mLastY - ev.y).toInt()
                var dx = (mLastX - ev.x).toInt()
                //当连续滑动距离达到TouchSlop时候,认为滑动
                if (!mIsBeingDragged) {
                    if (Math.abs(dy) > mTouchSlop) {
                        if (dy > 0) {
                            dy -= mTouchSlop
                        } else {
                            dy += mTouchSlop
                        }
                        mIsBeingDragged = true
                    }
                    if (Math.abs(dx) > mTouchSlop) {
                        when {
                            dy == 0 -> mIsReadyToDragHorizontal = true
                            Math.abs(dx).toFloat() / Math.abs(dy).toFloat() > 30 -> mIsReadyToDragHorizontal = true
                            else -> {
                                mIsBeingDragged = true
                                if (dy > 0) {
                                    dy -= mTouchSlop
                                } else {
                                    dy += mTouchSlop
                                }
                            }
                        }
                    }
                }
                if (mIsBeingDragged) {
                    mLastY = ev.y
                    var consumedDy = 0
                    if (dy == 0) {
                        //过滤掉
                        return true
                    } else if (dy > 0) {
                        consumedDy = Math.min(dy, mScrollRange - scrollY)
                    } else {
                        if (isInnerScrollViewTop()) {
                            consumedDy = Math.max(dy, -scrollY)
                        }
                    }
                    if (consumedDy != 0) {
                        scrollBy(0, consumedDy)
                  
                    }
                }
            }
        }
        MotionEvent.ACTION_UP -> {
            mIsBeingDragged = false
            mIsReadyToDragHorizontal = false
        }
    }
    //?
    super.dispatchTouchEvent(ev)
    return true
}

NestedScrolling机制

Android 5.0之后加入该机制。

support v4包提供两个接口:

  • NestedScrollingParent,嵌套滑动的父View需要实现。已有实现CoordinatorLayout,NestedScroView

  • NestedScrollingChild, 嵌套滑动的子View需要实现。已有实现RecyclerView,NestedScroView

Google在给我提供这两个接口的时候,同时也给我们提供了实现这两个接口时一些方法的标准实现,

分别是

  • NestedScrollingChildHelper

  • NestedScrollingParentHelper

我们在实现上面两个接口的方法时,只需要调用相应Helper中相同签名的方法即可。

之后由于NestedScrollingParent/NestedScrollingChild功能有些不足,Google又引入NestedScrollingParent2/NestedScrollingChild2,具体引入原因下文会说。

本文示例代码主要是NestedScrollingParent2/NestedScrollingChild2

基本原理

对原始的事件分发机制做了一层封装,子View实现NestedScrollingChild接口,父View实现NestedScrollingParent 接口。

假设产生一个竖直滑动,简单来说滑动事件会由NestedScrollingChild先接收到产生一个dy,然后询问NestedScrollingParent要消耗多少(dyConsumed),自己再拿dy-dyConsumed来进行滑动。当然NestedScrollingChild有可能自己本身也并不会消耗完,此时会再向父View报告情况。

222.png

NestedScrollingParent

NestedScrollingParentHelper 只为我们提供了onNestedScrollAccepted,onStopNestedScroll,getNestedScrollAxes三个方法的实现,其余的方法我们根据自身需要自己实现。NestedScrollingParent的方法基本上都是提供给NestedScrollingChild来调用的,我们自己无需调用。

本例使用的是27.0.0的RecyclerView,实现了NestedScrollingChild2,下面是本例中NestedScrollingParent2的完整实现。

class ScrollHeaderLayout : LinearLayout, NestedScrollingParent2 {

    private lateinit var mNestedScrollingParentHelper: NestedScrollingParentHelper
    private lateinit var mHeaderView: View
    private lateinit var mBottomView: View
    private var mScrollRange = 0

    constructor(context: Context?) : this(context, null)
    constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0)
    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
        init()
    }

    private fun init() {
        orientation = VERTICAL
        mNestedScrollingParentHelper = NestedScrollingParentHelper(this)
    }

    override fun onFinishInflate() {
        super.onFinishInflate()
        if (childCount != 2) {
            throw IllegalStateException("ScrollHeaderLayout must have two children")
        }
        mHeaderView = getChildAt(0)
        mBottomView = getChildAt(1)
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        mScrollRange = scrollEvaluator.getScrollRange(mHeaderView)
        val bottomHeightSpec = MeasureSpec.makeMeasureSpec(measuredHeight - mHeaderView.measuredHeight + mScrollRange, MeasureSpec.EXACTLY)
        measureChild(mBottomView, widthMeasureSpec, bottomHeightSpec)
    }

    /**
     * -----------------------------------------------------------
     *  NestedScrollingParent
     */

    /**
     * NestedScrollingChild 未fling之前告诉准备fling的情况
     *
     * @param target    具体嵌套滑动的那个子类
     * @param velocityX 水平方向速度
     * @param velocityY 垂直方向速度
     * @return true 父View是否消耗了fling
     */
    override fun onNestedPreFling(target: View, velocityX: Float, velocityY: Float): Boolean {
        return false
    }

    /**
     * NestedScrollingChild 在fling之后告诉自己fling情况
     *
     * @param target    具体嵌套滑动的那个子类
     * @param velocityX 水平方向速度
     * @param velocityY 垂直方向速度
     * @param consumed  子view是否fling了
     * @return true 父View是否消耗了fling
     */
    override fun onNestedFling(target: View, velocityX: Float, velocityY: Float, consumed: Boolean): Boolean {
        return false
    }

    /**
     * -----------------------------------------------------------
     *  NestedScrollingParent2
     */

    /**
     * 有子View发起了嵌套滑动,确认该父View是否接受嵌套滑动
     *
     * @param child       target向上一直寻找NestedScrollingParent,child在这个路径上,是NestedScrollingParent的直接子View
     * @param target      NestedScrollingChild,即发起NestedScrolling的类
     * @param axes        嵌套滑动的方向,水平方向,垂直方向,或者不指定
     * @param type
     * @return 是否接受该嵌套滑动
     */
    override fun onStartNestedScroll(child: View, target: View, axes: Int, type: Int): Boolean {
        return axes == ViewCompat.SCROLL_AXIS_VERTICAL
    }

    /**
     * 表示该父View已经接受了嵌套滑动。onStartNestedScroll 方法返回true后该方法会调用。
     * NestedScrollingParentHelper为我们提供了该方法的标准实现。
     *
     */
    override fun onNestedScrollAccepted(child: View, target: View, axes: Int, type: Int) {
        mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes, type)
    }

    /**
     * NestedScrollingChild在准备滑动前先询问NestedScrollingParent需要消耗多少
     *
     * @param dx       NestedScrollingChild水平方向想要滚动的距离
     * @param dy       垂直方向嵌套滑动的子View竖直方向想要滚动的距离
     * @param consumed 这个参数用于告诉NestedScrollingChild 父View消耗掉的距离
     *                 consumed[0] 水平消耗的距离,consumed[1] 垂直消耗的距离
     */
    override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray?, type: Int) {
        val headViewHeight = mScrollRange
        var consumedDy = 0
        if (dy > 0) {
            consumedDy = Math.min(dy, headViewHeight - scrollY)
        } else {
            if (target is RecyclerView) {
                if (ScrollHelper.isRecyclerViewTop(target)) {
                    consumedDy = Math.max(dy, -scrollY)
                }
            }
        }
        consumed?.set(1, consumedDy)
        scrollBy(0, consumedDy)
    }

    /**
     * NestedScrollingChild自身也不一定消耗完全部距离,因此
     * NestedScrollingChild自身滑动完成后,告诉NestedScrollingParent自己的滑动情况
     * @param dxConsumed   NestedScrollingChild水平方向消耗的距离
     * @param dyConsumed   NestedScrollingChild竖直方向消耗的距离
     * @param dxUnconsumed NestedScrollingChild水平方向未消耗的距离
     * @param dyUnconsumed NestedScrollingChild竖直方向未消耗的距离
     */
    override fun onNestedScroll(target: View, dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int, type: Int) {
        Log.i(ScrollHeaderLayout::class.java.simpleName, "dyConsumed:$dyConsumed,dyUnconsumed:$dyUnconsumed")
    }

    /**
     * 停止嵌套滑动时
     */
    override fun onStopNestedScroll(target: View, type: Int) {
        mNestedScrollingParentHelper.onStopNestedScroll(target, type)
    }

    /**
     * ------------------------------------
     */
    private var scrollEvaluator: ScrollRangeEvaluator = object : ScrollRangeEvaluator {
        override fun getScrollRange(header: View): Int {
            return if ((header is ViewGroup) && header.childCount > 0) {
                header.getChildAt(0).measuredHeight
            } else {
                header.measuredHeight
            }
        }
    }

    fun setScrollRangeEvaluator(evaluator: ScrollRangeEvaluator) {
        this.scrollEvaluator = evaluator
    }

    interface ScrollRangeEvaluator {
        fun getScrollRange(header: View): Int
    }

}

NestedScrollingChild

一般情况下我们并不需要自己实现一个NestedScrollingChild, 系统已经为我们提供了RecyclerView和NestedScrollView大多数情况下都够用了,这里只是帮助大家更好理解它。

我们自己要实现一个NestedScrollingChild分为两步

1) 实现NestedScrollingChild里的方法。这一步非常简单,NestedScrollingChildHelper里面已经为我们提供了所有NestedScrollingChild所需要的实现。

2)在合适的实际调用相应的方法,大部分都需要在onTouchEvent方法中调用。调用时机下文会以RecyclerView为例来讲解。

class NestedChildView(context: Context, attrs: AttributeSet?) : View(context, attrs), NestedScrollingChild2 {

    private var mScrollingChildHelper: NestedScrollingChildHelper = NestedScrollingChildHelper(this)

    init {
        isNestedScrollingEnabled = true
    }

    /**
     * 设置是否开启嵌套滑动
     * @param enabled
     */
    override fun setNestedScrollingEnabled(enabled: Boolean) {
        mScrollingChildHelper.isNestedScrollingEnabled = enabled
    }

    override fun isNestedScrollingEnabled(): Boolean {
        return mScrollingChildHelper.isNestedScrollingEnabled
    }

    /**
     * 开始嵌套滑动流程,一般ACTION_DOWN里面调用。
     * 调用这个函数的时候会向上寻找NestedScrollingParent,如果找到了并且NestedScrollingParent 说可以滑动的话就返回true,否则返回false
     * @param axes:支持嵌套滚动轴。水平方向,垂直方向,或者不指定
     * @return true 父控件说可以滑动,false 父控件说不可以滑动
     */
    override fun startNestedScroll(axes: Int, type: Int): Boolean {
        return mScrollingChildHelper.startNestedScroll(axes, type)
    }

    /**
     * 是否有嵌套滑动对应的父控件
     */
    override fun hasNestedScrollingParent(type: Int): Boolean {
        return mScrollingChildHelper.hasNestedScrollingParent(type)
    }

    /**
     * 在嵌套滑动的子View滑动之前,告诉父View滑动的距离,让父View做相应的处理。
     *
     * @param dx             告诉父View水平方向需要滑动的距离
     * @param dy             告诉父View垂直方向需要滑动的距离
     * @param consumed       出参. 父View通过这个参数告诉子View,自己对事件的消耗情况。consumed[0]父View告诉子View水平方向滑动的距离(dx)
     * consumed[1]父View告诉子View垂直方向滑动的距离(dy).
     * @param offsetInWindow 可选 length=2的数组,如果父View滑动导致子View的窗口发生了变化(子View的位置发生了变化)
     * 该参数返回x(offsetInWindow[0]) y(offsetInWindow[1])方向的变化。 这个参数用于对触摸事件位置进行校准。
     * 如果你记录了手指最后的位置,需要根据参数offsetInWindow计算偏移量,才能保证下一次的touch事件的计算是正确的。
     *
     * 一般在ACTION_MOVE中准备滑动之前
     * @return true 父View滑动了,false 父View没有滑动。
     */
    override fun dispatchNestedPreScroll(dx: Int, dy: Int, consumed: IntArray?, offsetInWindow: IntArray?,type: Int): Boolean {
        return mScrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow,type)
    }

    /**
     * 在嵌套滑动的子View滑动之后再调用该函数向父View汇报滑动情况。
     *
     * @param dxConsumed     子View水平方向滑动的距离
     * @param dyConsumed     子View垂直方向滑动的距离
     * @param dxUnconsumed   子View水平方向没有滑动的距离
     * @param dyUnconsumed   子View垂直方向没有滑动的距离
     *
     * 一般在在ACTION_MOVE中调用,在dispatchNestedPreScroll之后
     * @return true 如果父View有滑动做了相应的处理, false 父View没有滑动.
     */
    override fun dispatchNestedScroll(dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int,
                                      offsetInWindow: IntArray?,type: Int): Boolean {
        return mScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow,type)
    }

    /**
     * 停止嵌套滑动流程(一般ACTION_UP里面调用)
     */
    override fun stopNestedScroll(type: Int) {
        mScrollingChildHelper.stopNestedScroll()
    }

    /**
     * 在嵌套滑动的子View fling之前告诉父View fling的情况。
     *
     * @param velocityX 水平方向的速度
     * @param velocityY 垂直方向的速度
     * @return 如果父View fling了
     */
    override fun dispatchNestedPreFling(velocityX: Float, velocityY: Float): Boolean {
        return mScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY)
    }

    /**
     * 在嵌套滑动的子View fling之后再调用该函数向父View汇报fling情况。
     *
     * @param velocityX 水平方向的速度
     * @param velocityY 垂直方向的速度
     * @param consumed  true 如果子View fling了, false 如果子View没有fling
     * @return true 如果父View fling了
     */
    override fun dispatchNestedFling(velocityX: Float, velocityY: Float, consumed: Boolean): Boolean {
        return mScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed)
    }

    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        mScrollingChildHelper.onDetachedFromWindow()
    }
}

Why V2

override fun onNestedPreFling(target: View, velocityX: Float, velocityY: Float): Boolean {
    return false
}

override fun onNestedFling(target: View, velocityX: Float, velocityY: Float, consumed: Boolean): Boolean {
    return false
}

NestedScrollingParent中为我们提供了如上两个方法用于处理fling事件,但是由于传过来一个速度。对于速度而言无法说父View消耗一部分,子View消耗一部分。因此老版本fling事件只能由父View或者子View中的一个处理。这种情况显然不合理,比如示例Demo滑动速度大,父View滑动完,子View应该继续滑动。

针对fling无法在子View和父View之间交替的问题,NestedScrollingParent2直接废弃onNestedPreFling和onNestedFling方法。 并给原来的onStartNestedScroll,onNestedScrollAccepted,onNestedPreScroll,onNestedScroll,onStopNestedScroll方法添加一个type参数,定义如下

@IntDef({TYPE_TOUCH, TYPE_NON_TOUCH})
@Retention(RetentionPolicy.SOURCE)
@RestrictTo(LIBRARY_GROUP)
public @interface NestedScrollType {}

TYPE_TOUCH表示正常的手指触摸的滚动

TYPE_NON_TOUCH表示的是fling引起的滚动

然后再fling时候也会重新走一遍嵌套滑动的流程,只是type传的TYPE_NON_TOUCH。

源码分析

以RecyclerView为例分析,RecylerView实现NestedScrollingParent2接口,方法的实现和NestedChildView几乎一样,我们主要是看一下相应方法的调用时机,以及NestedScrollingChildHelper的标准实现做了些什么。

@Override
public boolean onTouchEvent(MotionEvent e) {

    final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
    final boolean canScrollVertically = mLayout.canScrollVertically();

    if (mVelocityTracker == null) {
        mVelocityTracker = VelocityTracker.obtain();
    }
    boolean eventAddedToVelocityTracker = false;

    final MotionEvent vtev = MotionEvent.obtain(e);
    final int action = e.getActionMasked();
    final int actionIndex = e.getActionIndex();

    if (action == MotionEvent.ACTION_DOWN) {
        mNestedOffsets[0] = mNestedOffsets[1] = 0;
    }
    //如果父View发生了滑动等,触摸事件位置需要偏移
    vtev.offsetLocation(mNestedOffsets[0], mNestedOffsets[1]);

    switch (action) {
        case MotionEvent.ACTION_DOWN: {
            mScrollPointerId = e.getPointerId(0);
            mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
            mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);

            int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
            if (canScrollHorizontally) {
                nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
            }
            if (canScrollVertically) {
                nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
            }

             //1.ACTION_DOWN时候开始嵌套滑动
            startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
        } break;

        case MotionEvent.ACTION_POINTER_DOWN: {
            mScrollPointerId = e.getPointerId(actionIndex);
            mInitialTouchX = mLastTouchX = (int) (e.getX(actionIndex) + 0.5f);
            mInitialTouchY = mLastTouchY = (int) (e.getY(actionIndex) + 0.5f);
        } break;

        case MotionEvent.ACTION_MOVE: {
            final int index = e.findPointerIndex(mScrollPointerId);
            if (index < 0) {
                Log.e(TAG, "Error processing scroll; pointer index for id "
                        + mScrollPointerId + " not found. Did any MotionEvents get skipped?");
                return false;
            }

            final int x = (int) (e.getX(index) + 0.5f);
            final int y = (int) (e.getY(index) + 0.5f);
            int dx = mLastTouchX - x;
            int dy = mLastTouchY - y;
            //2.RecylcerView没开始滑动,先问一下父View是不是需要滑动
            if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset, TYPE_TOUCH)) {
                //减去父View消耗
                dx -= mScrollConsumed[0];
                dy -= mScrollConsumed[1];
                vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
                // 父View滑动的话更新offset
                mNestedOffsets[0] += mScrollOffset[0];
                mNestedOffsets[1] += mScrollOffset[1];
            }

            if (mScrollState != SCROLL_STATE_DRAGGING) {
                boolean startScroll = false;
                if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
                    if (dx > 0) {
                        dx -= mTouchSlop;
                    } else {
                        dx += mTouchSlop;
                    }
                    startScroll = true;
                }
                if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
                    if (dy > 0) {
                        dy -= mTouchSlop;
                    } else {
                        dy += mTouchSlop;
                    }
                    startScroll = true;
                }
                if (startScroll) {
                    setScrollState(SCROLL_STATE_DRAGGING);
                }
            }

            if (mScrollState == SCROLL_STATE_DRAGGING) {
                mLastTouchX = x - mScrollOffset[0];
                mLastTouchY = y - mScrollOffset[1];
                //3.自身滑动,并向父View报告滑动情况
                if (scrollByInternal(
                        canScrollHorizontally ? dx : 0,
                        canScrollVertically ? dy : 0,
                        vtev)) {
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
                if (mGapWorker != null && (dx != 0 || dy != 0)) {
                    mGapWorker.postFromTraversal(this, dx, dy);
                }
            }
        } break;

        case MotionEvent.ACTION_POINTER_UP: {
            onPointerUp(e);
        } break;

        case MotionEvent.ACTION_UP: {
            mVelocityTracker.addMovement(vtev);
            eventAddedToVelocityTracker = true;
            mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
            final float xvel = canScrollHorizontally
                    ? -mVelocityTracker.getXVelocity(mScrollPointerId) : 0;
            final float yvel = canScrollVertically
                    ? -mVelocityTracker.getYVelocity(mScrollPointerId) : 0;
            //fling触发调用
            if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
                setScrollState(SCROLL_STATE_IDLE);
            }
            //4.停止嵌套滑动
            resetTouch();
        } break;

        case MotionEvent.ACTION_CANCEL: {
            cancelTouch();
        } break;
    }

    if (!eventAddedToVelocityTracker) {
        mVelocityTracker.addMovement(vtev);
    }
    vtev.recycle();

    return true;
}

  1. ACTION_DOWN时候开始嵌套滑动

startNestedScroll的目的就是向上找到NestedScrollParent并询问是否接要嵌套滑动

public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
    if (hasNestedScrollingParent(type)) {
        return true;
    }
    if (isNestedScrollingEnabled()) {
        ViewParent p = mView.getParent();
        View child = mView;
        //循环往上寻找NestedScrollingParent
        while (p != null) {
            if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
                setNestedScrollingParentForType(type, p);
                ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
                return true;
            }
            //为什么判断
            if (p instanceof View) {
                child = (View) p;
            }
            p = p.getParent();
        }
    }
    return false;
}

如果是NestedScrollingParent2的话直接onStartNestedScroll,不是的话因为之前老版本的NestedScrollingParent只支持TYPE_TOUCH的滑动,因此需要判断一下。

public static boolean onStartNestedScroll(ViewParent parent, View child, View target,
        int nestedScrollAxes, int type) {
    if (parent instanceof NestedScrollingParent2) {
        // First try the NestedScrollingParent2 API
        return ((NestedScrollingParent2) parent).onStartNestedScroll(child, target,
                nestedScrollAxes, type);
    } else if (type == ViewCompat.TYPE_TOUCH) {
        // Else if the type is the default (touch), try the NestedScrollingParent API
        return IMPL.onStartNestedScroll(parent, child, target, nestedScrollAxes);
    }
    return false;
}

记录找到的NestedScrollingParent。

private void setNestedScrollingParentForType(@NestedScrollType int type, ViewParent p) {
    switch (type) {
        case TYPE_TOUCH:
            mNestedScrollingParentTouch = p;
            break;
        case TYPE_NON_TOUCH:
            mNestedScrollingParentNonTouch = p;
            break;
    }
}

  1. ACTION_MOVE,子View未开始滑动前先询问父View是否消耗

public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
        @Nullable int[] offsetInWindow, @NestedScrollType int type) {
    if (isNestedScrollingEnabled()) {
        //获取找打startNestedScroll时候找到的NestedScrollingParent
        final ViewParent parent = getNestedScrollingParentForType(type);
        if (parent == null) {
            return false;
        }
        if (dx != 0 || dy != 0) {
            int startX = 0;
            int startY = 0;
            if (offsetInWindow != null) {
                mView.getLocationInWindow(offsetInWindow);
                //记录RecyclerView在滑动事件传给父View前 在窗口上位置
                startX = offsetInWindow[0];
                startY = offsetInWindow[1];
            }

            if (consumed == null) {
                if (mTempNestedScrollConsumed == null) {
                    mTempNestedScrollConsumed = new int[2];
                }
                consumed = mTempNestedScrollConsumed;
            }
            //置0
            consumed[0] = 0;
            consumed[1] = 0;
            //调用NestedScrollingParent的onNestedPreScroll
            ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);

            if (offsetInWindow != null) {
                mView.getLocationInWindow(offsetInWindow);
                //父View滑动后位置减去滑动前位置得到一个偏移量
                offsetInWindow[0] -= startX;
                offsetInWindow[1] -= startY;
            }
            //通过consumed!=0确定父View是否消耗
            return consumed[0] != 0 || consumed[1] != 0;
        } else if (offsetInWindow != null) {
            offsetInWindow[0] = 0;
            offsetInWindow[1] = 0;
        }
    }
    return false;
}

3.NestedScrollingChild完成对滚动事件的消耗,并向NestedScrollingParent报告

boolean scrollByInternal(int x, int y, MotionEvent ev) {
    int unconsumedX = 0, unconsumedY = 0;
    int consumedX = 0, consumedY = 0;
    if (mAdapter != null) {
        if (x != 0) {
            consumedX = mLayout.scrollHorizontallyBy(x, mRecycler, mState);
            unconsumedX = x - consumedX;
        }
        if (y != 0) {
            //RecylerView滑动,返回自己滑动消耗的
            consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
            //获取未消耗的
            unconsumedY = y - consumedY;
        }
    }
    //自己滑动消耗完事件后,向NestedScrollingParent报告自己滑动的情况,父View此时还可以进行一些滑动操作等
    if (dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset,
            TYPE_TOUCH)) {
        mLastTouchX -= mScrollOffset[0];
        mLastTouchY -= mScrollOffset[1];
        if (ev != null) {
            ev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
        }
        mNestedOffsets[0] += mScrollOffset[0];
        mNestedOffsets[1] += mScrollOffset[1];
    } 

    return consumedX != 0 || consumedY != 0;
}

dispatchNestedScroll的核心就是调用父View的onNestedScroll,代码很简单

  1. 停止嵌套滑动

ACTION_UP或者ACTION_CANCEL触发后,都会调用resetTouch这个方法。

private void resetTouch() {
    if (mVelocityTracker != null) {
        mVelocityTracker.clear();
    }
    stopNestedScroll(TYPE_TOUCH);
    releaseGlows();
}

调用NestedScrollingParent的onStopNestedScroll方法,把自己的成员变量置空。

public void stopNestedScroll(@NestedScrollType int type) {
    ViewParent parent = getNestedScrollingParentForType(type);
    if (parent != null) {
        ViewParentCompat.onStopNestedScroll(parent, mView, type);
        setNestedScrollingParentForType(type, null);
    }
}

  1. fling
public boolean fling(int velocityX, int velocityY) {
    //这两个方法v2的版本其实不需要了,这里只是兼容一下
    if (!dispatchNestedPreFling(velocityX, velocityY)) {
        final boolean canScroll = canScrollHorizontal || canScrollVertical;
        dispatchNestedFling(velocityX, velocityY, canScroll);

        if (mOnFlingListener != null && mOnFlingListener.onFling(velocityX, velocityY)) {
            return true;
        }
        if (canScroll) {
            int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
            if (canScrollHorizontal) {
                nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
            }
            if (canScrollVertical) {
                nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
            }
            //1.开始嵌套滑动
            startNestedScroll(nestedScrollAxis, TYPE_NON_TOUCH);
            //ViewFlinger真正实现fling
            mViewFlinger.fling(velocityX, velocityY);
            return true;
        }
    }
    return false;
}

class ViewFlinger implements Runnable {

    public void fling(int velocityX, int velocityY) {
        setScrollState(SCROLL_STATE_SETTLING);
        mLastFlingX = mLastFlingY = 0;
        mScroller.fling(0, 0, velocityX, velocityY,
                Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
        postOnAnimation();
    }

 void postOnAnimation() {
        if (mEatRunOnAnimationRequest) {
            mReSchedulePostAnimationCallback = true;
        } else {
            removeCallbacks(this);
            //简单认为View.post
            ViewCompat.postOnAnimation(RecyclerView.this, this);
        }
    }

    @Override
    public void run() {

        final OverScroller scroller = mScroller;
        final SmoothScroller smoothScroller = mLayout.mSmoothScroller;
        if (scroller.computeScrollOffset()) {
            final int[] scrollConsumed = mScrollConsumed;
            final int x = scroller.getCurrX();
            final int y = scroller.getCurrY();
            int dx = x - mLastFlingX;
            int dy = y - mLastFlingY;
            int hresult = 0;
            int vresult = 0;
            mLastFlingX = x;
            mLastFlingY = y;
            int overscrollX = 0, overscrollY = 0;
            //2.调用dispatchNestedPreScroll
            if (dispatchNestedPreScroll(dx, dy, scrollConsumed, null, TYPE_NON_TOUCH)) {
                dx -= scrollConsumed[0];
                dy -= scrollConsumed[1];
            }

            if (mAdapter != null) {
                if (dx != 0) {
                    hresult = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
                    overscrollX = dx - hresult;
                }
                if (dy != 0) {
                    vresult = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
                    overscrollY = dy - vresult;
                }
             }

            if (!dispatchNestedScroll(hresult, vresult, overscrollX, overscrollY, null,
                    TYPE_NON_TOUCH)
                    && (overscrollX != 0 || overscrollY != 0)) {
                final int vel = (int) scroller.getCurrVelocity();

                int velX = 0;
                if (overscrollX != x) {
                    velX = overscrollX < 0 ? -vel : overscrollX > 0 ? vel : 0;
                }

                int velY = 0;
                if (overscrollY != y) {
                    velY = overscrollY < 0 ? -vel : overscrollY > 0 ? vel : 0;
                }

                if (getOverScrollMode() != View.OVER_SCROLL_NEVER) {
                    absorbGlows(velX, velY);
                }
                if ((velX != 0 || overscrollX == x || scroller.getFinalX() == 0)
                        && (velY != 0 || overscrollY == y || scroller.getFinalY() == 0)) {
                    scroller.abortAnimation();
                }
            }
            if (hresult != 0 || vresult != 0) {
                dispatchOnScrolled(hresult, vresult);
            }

            if (!awakenScrollBars()) {
                invalidate();
            }

            final boolean fullyConsumedVertical = dy != 0 && mLayout.canScrollVertically()
                    && vresult == dy;
            final boolean fullyConsumedHorizontal = dx != 0 && mLayout.canScrollHorizontally()
                    && hresult == dx;
            final boolean fullyConsumedAny = (dx == 0 && dy == 0) || fullyConsumedHorizontal
                    || fullyConsumedVertical;

            //如果滑动完成了
            if (scroller.isFinished() || (!fullyConsumedAny
                    && !hasNestedScrollingParent(TYPE_NON_TOUCH))) {
                setScrollState(SCROLL_STATE_IDLE);
                if (ALLOW_THREAD_GAP_WORK) {
                    mPrefetchRegistry.clearPrefetchPositions();
                }
                //停止嵌套滑动
                stopNestedScroll(TYPE_NON_TOUCH);
            } else {
                //滑动没有完成,继续post执行run方法
                postOnAnimation();
            }
        }   
    }
}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容