总述

NestedScrolling机制详解

上图是一个非常常见的嵌套滑动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

NestedScrolling机制详解

一般状况下咱们处理滑动冲突,重写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陈述状况。

NestedScrolling机制详解

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