NestedScrolling機(jī)制詳解

綜述

嵌套滑動.gif

上圖是一個非常常見的嵌套滑動UI交互,實(shí)現(xiàn)這樣的效果,大致有如下三種思路:

  1. 基于普通的事件分發(fā)機(jī)制

  2. 基于NestedScrolling機(jī)制

  3. 基于CoordinatorLayout與Behavior

以上三種思路從原理上循序漸進(jìn),逐層封裝。由于本文主要介紹嵌套滑動,會主要介紹第二種方案及其原理,第一種會稍微講解一下。

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>

無論采用哪種實(shí)現(xiàn)方式,布局都分為上部分的Header和下部分的ViewPager兩部分。

傳統(tǒng)的事件分發(fā)機(jī)制

優(yōu)點(diǎn):

靈活性最高。

缺點(diǎn):

處理細(xì)節(jié)多,難度大,需要對事件分發(fā)機(jī)制, 多點(diǎn)觸控,滑動,fling,以及一些周邊API等都比較清楚。

基本思路

要完成上述效果,在豎直滑動的情況下,上滑時(shí)先讓外層的父View滾動,到滾動的最大距離時(shí)候,再讓子View開始滾動。下滑時(shí)如果子View滑動距離不是0的話,先讓子View滑動,然后讓父View滑動。因此一次滑動中的事件需要再父View和子View中切換傳遞。

復(fù)習(xí)一下事件分發(fā)機(jī)制:

事件序列:從手指按下知道抬起,中間經(jīng)歷一個ACTION_DONW ,多個ACTION_MOVE和一個ACTION_UP

事件分發(fā)機(jī)制.png

一般情況下我們處理滑動沖突,重寫onInterceptTouchEvent方法即可,但是一旦onInterceptTouchEvent方法返回true,那么該事件序列以后的事件都會直接給父View處理,這種情況在處理滑動沖突是是可行的。但是在我們上面的案例因?yàn)閷τ谝粋€事件序列需要交替得在子View和父View中傳遞,如果重寫該方法的話,需要我們自己再合適時(shí)機(jī)手動派發(fā)一些事件。

因此更為簡單的做法不如直接重寫dispatchTouchEvent方法,以下代碼只是處理了單手指滑動的情況,沒有考慮多點(diǎn)觸控,也沒有處理fling。

如上我們需要判斷isInnerScrollViewTop(),即內(nèi)部的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()
                //當(dāng)連續(xù)滑動距離達(dá)到TouchSlop時(shí)候,認(rèn)為滑動
                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機(jī)制

Android 5.0之后加入該機(jī)制。

support v4包提供兩個接口:

  • NestedScrollingParent,嵌套滑動的父View需要實(shí)現(xiàn)。已有實(shí)現(xiàn)CoordinatorLayout,NestedScroView

  • NestedScrollingChild, 嵌套滑動的子View需要實(shí)現(xiàn)。已有實(shí)現(xiàn)RecyclerView,NestedScroView

Google在給我提供這兩個接口的時(shí)候,同時(shí)也給我們提供了實(shí)現(xiàn)這兩個接口時(shí)一些方法的標(biāo)準(zhǔn)實(shí)現(xiàn),

分別是

  • NestedScrollingChildHelper

  • NestedScrollingParentHelper

我們在實(shí)現(xiàn)上面兩個接口的方法時(shí),只需要調(diào)用相應(yīng)Helper中相同簽名的方法即可。

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

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

基本原理

對原始的事件分發(fā)機(jī)制做了一層封裝,子View實(shí)現(xiàn)NestedScrollingChild接口,父View實(shí)現(xiàn)NestedScrollingParent 接口。

假設(shè)產(chǎn)生一個豎直滑動,簡單來說滑動事件會由NestedScrollingChild先接收到產(chǎn)生一個dy,然后詢問NestedScrollingParent要消耗多少(dyConsumed),自己再拿dy-dyConsumed來進(jìn)行滑動。當(dāng)然NestedScrollingChild有可能自己本身也并不會消耗完,此時(shí)會再向父View報(bào)告情況。

222.png

NestedScrollingParent

NestedScrollingParentHelper 只為我們提供了onNestedScrollAccepted,onStopNestedScroll,getNestedScrollAxes三個方法的實(shí)現(xiàn),其余的方法我們根據(jù)自身需要自己實(shí)現(xiàn)。NestedScrollingParent的方法基本上都是提供給NestedScrollingChild來調(diào)用的,我們自己無需調(diào)用。

本例使用的是27.0.0的RecyclerView,實(shí)現(xiàn)了NestedScrollingChild2,下面是本例中NestedScrollingParent2的完整實(shí)現(xiàn)。

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之前告訴準(zhǔn)備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發(fā)起了嵌套滑動,確認(rèn)該父View是否接受嵌套滑動
     *
     * @param child       target向上一直尋找NestedScrollingParent,child在這個路徑上,是NestedScrollingParent的直接子View
     * @param target      NestedScrollingChild,即發(fā)起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已經(jīng)接受了嵌套滑動。onStartNestedScroll 方法返回true后該方法會調(diào)用。
     * NestedScrollingParentHelper為我們提供了該方法的標(biāo)準(zhǔn)實(shí)現(xiàn)。
     *
     */
    override fun onNestedScrollAccepted(child: View, target: View, axes: Int, type: Int) {
        mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes, type)
    }

    /**
     * NestedScrollingChild在準(zhǔn)備滑動前先詢問NestedScrollingParent需要消耗多少
     *
     * @param dx       NestedScrollingChild水平方向想要滾動的距離
     * @param dy       垂直方向嵌套滑動的子View豎直方向想要滾動的距離
     * @param consumed 這個參數(shù)用于告訴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")
    }

    /**
     * 停止嵌套滑動時(shí)
     */
    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

一般情況下我們并不需要自己實(shí)現(xiàn)一個NestedScrollingChild, 系統(tǒng)已經(jīng)為我們提供了RecyclerView和NestedScrollView大多數(shù)情況下都夠用了,這里只是幫助大家更好理解它。

我們自己要實(shí)現(xiàn)一個NestedScrollingChild分為兩步

1) 實(shí)現(xiàn)NestedScrollingChild里的方法。這一步非常簡單,NestedScrollingChildHelper里面已經(jīng)為我們提供了所有NestedScrollingChild所需要的實(shí)現(xiàn)。

2)在合適的實(shí)際調(diào)用相應(yīng)的方法,大部分都需要在onTouchEvent方法中調(diào)用。調(diào)用時(shí)機(jī)下文會以RecyclerView為例來講解。

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

    private var mScrollingChildHelper: NestedScrollingChildHelper = NestedScrollingChildHelper(this)

    init {
        isNestedScrollingEnabled = true
    }

    /**
     * 設(shè)置是否開啟嵌套滑動
     * @param enabled
     */
    override fun setNestedScrollingEnabled(enabled: Boolean) {
        mScrollingChildHelper.isNestedScrollingEnabled = enabled
    }

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

    /**
     * 開始嵌套滑動流程,一般ACTION_DOWN里面調(diào)用。
     * 調(diào)用這個函數(shù)的時(shí)候會向上尋找NestedScrollingParent,如果找到了并且NestedScrollingParent 說可以滑動的話就返回true,否則返回false
     * @param axes:支持嵌套滾動軸。水平方向,垂直方向,或者不指定
     * @return true 父控件說可以滑動,false 父控件說不可以滑動
     */
    override fun startNestedScroll(axes: Int, type: Int): Boolean {
        return mScrollingChildHelper.startNestedScroll(axes, type)
    }

    /**
     * 是否有嵌套滑動對應(yīng)的父控件
     */
    override fun hasNestedScrollingParent(type: Int): Boolean {
        return mScrollingChildHelper.hasNestedScrollingParent(type)
    }

    /**
     * 在嵌套滑動的子View滑動之前,告訴父View滑動的距離,讓父View做相應(yīng)的處理。
     *
     * @param dx             告訴父View水平方向需要滑動的距離
     * @param dy             告訴父View垂直方向需要滑動的距離
     * @param consumed       出參. 父View通過這個參數(shù)告訴子View,自己對事件的消耗情況。consumed[0]父View告訴子View水平方向滑動的距離(dx)
     * consumed[1]父View告訴子View垂直方向滑動的距離(dy).
     * @param offsetInWindow 可選 length=2的數(shù)組,如果父View滑動導(dǎo)致子View的窗口發(fā)生了變化(子View的位置發(fā)生了變化)
     * 該參數(shù)返回x(offsetInWindow[0]) y(offsetInWindow[1])方向的變化。 這個參數(shù)用于對觸摸事件位置進(jìn)行校準(zhǔn)。
     * 如果你記錄了手指最后的位置,需要根據(jù)參數(shù)offsetInWindow計(jì)算偏移量,才能保證下一次的touch事件的計(jì)算是正確的。
     *
     * 一般在ACTION_MOVE中準(zhǔn)備滑動之前
     * @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滑動之后再調(diào)用該函數(shù)向父View匯報(bào)滑動情況。
     *
     * @param dxConsumed     子View水平方向滑動的距離
     * @param dyConsumed     子View垂直方向滑動的距離
     * @param dxUnconsumed   子View水平方向沒有滑動的距離
     * @param dyUnconsumed   子View垂直方向沒有滑動的距離
     *
     * 一般在在ACTION_MOVE中調(diào)用,在dispatchNestedPreScroll之后
     * @return true 如果父View有滑動做了相應(yīng)的處理, 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里面調(diào)用)
     */
    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之后再調(diào)用該函數(shù)向父View匯報(bào)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應(yīng)該繼續(xù)滑動。

針對fling無法在子View和父View之間交替的問題,NestedScrollingParent2直接廢棄onNestedPreFling和onNestedFling方法。 并給原來的onStartNestedScroll,onNestedScrollAccepted,onNestedPreScroll,onNestedScroll,onStopNestedScroll方法添加一個type參數(shù),定義如下

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

TYPE_TOUCH表示正常的手指觸摸的滾動

TYPE_NON_TOUCH表示的是fling引起的滾動

然后再fling時(shí)候也會重新走一遍嵌套滑動的流程,只是type傳的TYPE_NON_TOUCH。

源碼分析

以RecyclerView為例分析,RecylerView實(shí)現(xiàn)NestedScrollingParent2接口,方法的實(shí)現(xiàn)和NestedChildView幾乎一樣,我們主要是看一下相應(yīng)方法的調(diào)用時(shí)機(jī),以及NestedScrollingChildHelper的標(biāo)準(zhǔn)實(shí)現(xiàn)做了些什么。

@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發(fā)生了滑動等,觸摸事件位置需要偏移
    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時(shí)候開始嵌套滑動
            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報(bào)告滑動情況
                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觸發(fā)調(diào)用
            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時(shí)候開始嵌套滑動

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;
        //循環(huán)往上尋找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,不是的話因?yàn)橹袄习姹镜腘estedScrollingParent只支持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時(shí)候找到的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;
            //調(diào)用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報(bào)告

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報(bào)告自己滑動的情況,父View此時(shí)還可以進(jìn)行一些滑動操作等
    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的核心就是調(diào)用父View的onNestedScroll,代碼很簡單

  1. 停止嵌套滑動

ACTION_UP或者ACTION_CANCEL觸發(fā)后,都會調(diào)用resetTouch這個方法。

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

調(diào)用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的版本其實(shí)不需要了,這里只是兼容一下
    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真正實(shí)現(xiàn)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);
            //簡單認(rèn)為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.調(diào)用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 {
                //滑動沒有完成,繼續(xù)post執(zhí)行run方法
                postOnAnimation();
            }
        }   
    }
}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容