NestedScrolling機(jī)制詳解

綜述

嵌套滑動(dòng).gif

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

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

  2. 基于NestedScrolling機(jī)制

  3. 基于CoordinatorLayout與Behavior

以上三種思路從原理上循序漸進(jìn)蜀撑,逐層封裝矢炼。由于本文主要介紹嵌套滑動(dòng)析藕,會(huì)主要介紹第二種方案及其原理赫蛇,第一種會(huì)稍微講解一下绵患。

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>

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

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

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

靈活性最高落蝙。

缺點(diǎn):

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

基本思路

要完成上述效果管行,在豎直滑動(dòng)的情況下,上滑時(shí)先讓外層的父View滾動(dòng)邪媳,到滾動(dòng)的最大距離時(shí)候病瞳,再讓子View開(kāi)始滾動(dòng)揽咕。下滑時(shí)如果子View滑動(dòng)距離不是0的話,先讓子View滑動(dòng)套菜,然后讓父View滑動(dòng)亲善。因此一次滑動(dòng)中的事件需要再父View和子View中切換傳遞。

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

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

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

一般情況下我們處理滑動(dòng)沖突,重寫(xiě)onInterceptTouchEvent方法即可戏溺,但是一旦onInterceptTouchEvent方法返回true渣蜗,那么該事件序列以后的事件都會(huì)直接給父View處理,這種情況在處理滑動(dòng)沖突是是可行的旷祸。但是在我們上面的案例因?yàn)閷?duì)于一個(gè)事件序列需要交替得在子View和父View中傳遞耕拷,如果重寫(xiě)該方法的話,需要我們自己再合適時(shí)機(jī)手動(dòng)派發(fā)一些事件托享。

因此更為簡(jiǎn)單的做法不如直接重寫(xiě)dispatchTouchEvent方法骚烧,以下代碼只是處理了單手指滑動(dòng)的情況,沒(méi)有考慮多點(diǎn)觸控闰围,也沒(méi)有處理fling赃绊。

如上我們需要判斷isInnerScrollViewTop(),即內(nèi)部的View滑動(dòng)距離是否為0羡榴。因此父View需要知道滑動(dòng)的子View到底是誰(shuí)碧查,需要外界告訴。

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òng)距離達(dá)到TouchSlop時(shí)候校仑,認(rèn)為滑動(dòng)
                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) {
                        //過(guò)濾掉
                        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包提供兩個(gè)接口:

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

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

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

分別是

  • NestedScrollingChildHelper

  • NestedScrollingParentHelper

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

之后由于NestedScrollingParent/NestedScrollingChild功能有些不足乐纸,Google又引入NestedScrollingParent2/NestedScrollingChild2,具體引入原因下文會(huì)說(shuō)摇予。

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

基本原理

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

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

222.png

NestedScrollingParent

NestedScrollingParentHelper 只為我們提供了onNestedScrollAccepted,onStopNestedScroll寂曹,getNestedScrollAxes三個(gè)方法的實(shí)現(xiàn)哎迄,其余的方法我們根據(jù)自身需要自己實(shí)現(xiàn)。NestedScrollingParent的方法基本上都是提供給NestedScrollingChild來(lái)調(diào)用的隆圆,我們自己無(wú)需調(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    具體嵌套滑動(dòng)的那個(gè)子類
     * @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    具體嵌套滑動(dòng)的那個(gè)子類
     * @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ā)起了嵌套滑動(dòng)旨涝,確認(rèn)該父View是否接受嵌套滑動(dòng)
     *
     * @param child       target向上一直尋找NestedScrollingParent,child在這個(gè)路徑上侣背,是NestedScrollingParent的直接子View
     * @param target      NestedScrollingChild,即發(fā)起NestedScrolling的類
     * @param axes        嵌套滑動(dòng)的方向,水平方向白华,垂直方向,或者不指定
     * @param type
     * @return 是否接受該嵌套滑動(dòng)
     */
    override fun onStartNestedScroll(child: View, target: View, axes: Int, type: Int): Boolean {
        return axes == ViewCompat.SCROLL_AXIS_VERTICAL
    }

    /**
     * 表示該父View已經(jīng)接受了嵌套滑動(dòng)秃踩。onStartNestedScroll 方法返回true后該方法會(huì)調(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)備滑動(dòng)前先詢問(wèn)NestedScrollingParent需要消耗多少
     *
     * @param dx       NestedScrollingChild水平方向想要滾動(dòng)的距離
     * @param dy       垂直方向嵌套滑動(dòng)的子View豎直方向想要滾動(dòng)的距離
     * @param consumed 這個(gè)參數(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自身滑動(dòng)完成后蒜胖,告訴NestedScrollingParent自己的滑動(dòng)情況
     * @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")
    }

    /**
     * 停止嵌套滑動(dòng)時(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)一個(gè)NestedScrollingChild, 系統(tǒng)已經(jīng)為我們提供了RecyclerView和NestedScrollView大多數(shù)情況下都?jí)蛴昧讼穑@里只是幫助大家更好理解它。

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

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

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

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

    private var mScrollingChildHelper: NestedScrollingChildHelper = NestedScrollingChildHelper(this)

    init {
        isNestedScrollingEnabled = true
    }

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

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

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

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

    /**
     * 在嵌套滑動(dòng)的子View滑動(dòng)之前骗随,告訴父View滑動(dòng)的距離蛤织,讓父View做相應(yīng)的處理。
     *
     * @param dx             告訴父View水平方向需要滑動(dòng)的距離
     * @param dy             告訴父View垂直方向需要滑動(dòng)的距離
     * @param consumed       出參. 父View通過(guò)這個(gè)參數(shù)告訴子View鸿染,自己對(duì)事件的消耗情況指蚜。consumed[0]父View告訴子View水平方向滑動(dòng)的距離(dx)
     * consumed[1]父View告訴子View垂直方向滑動(dòng)的距離(dy).
     * @param offsetInWindow 可選 length=2的數(shù)組,如果父View滑動(dòng)導(dǎo)致子View的窗口發(fā)生了變化(子View的位置發(fā)生了變化)
     * 該參數(shù)返回x(offsetInWindow[0]) y(offsetInWindow[1])方向的變化牡昆。 這個(gè)參數(shù)用于對(duì)觸摸事件位置進(jìn)行校準(zhǔn)姚炕。
     * 如果你記錄了手指最后的位置,需要根據(jù)參數(shù)offsetInWindow計(jì)算偏移量丢烘,才能保證下一次的touch事件的計(jì)算是正確的柱宦。
     *
     * 一般在ACTION_MOVE中準(zhǔn)備滑動(dòng)之前
     * @return true 父View滑動(dòng)了,false 父View沒(méi)有滑動(dòng)播瞳。
     */
    override fun dispatchNestedPreScroll(dx: Int, dy: Int, consumed: IntArray?, offsetInWindow: IntArray?,type: Int): Boolean {
        return mScrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow,type)
    }

    /**
     * 在嵌套滑動(dòng)的子View滑動(dòng)之后再調(diào)用該函數(shù)向父View匯報(bào)滑動(dòng)情況掸刊。
     *
     * @param dxConsumed     子View水平方向滑動(dòng)的距離
     * @param dyConsumed     子View垂直方向滑動(dòng)的距離
     * @param dxUnconsumed   子View水平方向沒(méi)有滑動(dòng)的距離
     * @param dyUnconsumed   子View垂直方向沒(méi)有滑動(dòng)的距離
     *
     * 一般在在ACTION_MOVE中調(diào)用,在dispatchNestedPreScroll之后
     * @return true 如果父View有滑動(dòng)做了相應(yīng)的處理, false 父View沒(méi)有滑動(dòng).
     */
    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)
    }

    /**
     * 停止嵌套滑動(dòng)流程(一般ACTION_UP里面調(diào)用)
     */
    override fun stopNestedScroll(type: Int) {
        mScrollingChildHelper.stopNestedScroll()
    }

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

    /**
     * 在嵌套滑動(dòng)的子View fling之后再調(diào)用該函數(shù)向父View匯報(bào)fling情況忧侧。
     *
     * @param velocityX 水平方向的速度
     * @param velocityY 垂直方向的速度
     * @param consumed  true 如果子View fling了, false 如果子View沒(méi)有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中為我們提供了如上兩個(gè)方法用于處理fling事件,但是由于傳過(guò)來(lái)一個(gè)速度牌芋。對(duì)于速度而言無(wú)法說(shuō)父View消耗一部分蚓炬,子View消耗一部分。因此老版本fling事件只能由父View或者子View中的一個(gè)處理躺屁。這種情況顯然不合理肯夏,比如示例Demo滑動(dòng)速度大,父View滑動(dòng)完犀暑,子View應(yīng)該繼續(xù)滑動(dòng)驯击。

針對(duì)fling無(wú)法在子View和父View之間交替的問(wèn)題,NestedScrollingParent2直接廢棄onNestedPreFling和onNestedFling方法耐亏。 并給原來(lái)的onStartNestedScroll徊都,onNestedScrollAccepted,onNestedPreScroll广辰,onNestedScroll暇矫,onStopNestedScroll方法添加一個(gè)type參數(shù),定義如下

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

TYPE_TOUCH表示正常的手指觸摸的滾動(dòng)

TYPE_NON_TOUCH表示的是fling引起的滾動(dòng)

然后再fling時(shí)候也會(huì)重新走一遍嵌套滑動(dòng)的流程择吊,只是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ā)生了滑動(dòng)等冀续,觸摸事件位置需要偏移
    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í)候開(kāi)始嵌套滑動(dòng)
            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沒(méi)開(kāi)始滑動(dòng)琼讽,先問(wèn)一下父View是不是需要滑動(dòng)
            if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset, TYPE_TOUCH)) {
                //減去父View消耗
                dx -= mScrollConsumed[0];
                dy -= mScrollConsumed[1];
                vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
                // 父View滑動(dòng)的話更新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.自身滑動(dòng),并向父View報(bào)告滑動(dòng)情況
                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.停止嵌套滑動(dòng)
            resetTouch();
        } break;

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

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

    return true;
}

  1. ACTION_DOWN時(shí)候開(kāi)始嵌套滑動(dòng)

startNestedScroll的目的就是向上找到NestedScrollParent并詢問(wèn)是否接要嵌套滑動(dòng)

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的滑動(dòng)钻蹬,因此需要判斷一下。

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未開(kāi)始滑動(dòng)前先詢問(wèn)父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在滑動(dòng)事件傳給父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滑動(dòng)后位置減去滑動(dòng)前位置得到一個(gè)偏移量
                offsetInWindow[0] -= startX;
                offsetInWindow[1] -= startY;
            }
            //通過(guò)consumed!=0確定父View是否消耗
            return consumed[0] != 0 || consumed[1] != 0;
        } else if (offsetInWindow != null) {
            offsetInWindow[0] = 0;
            offsetInWindow[1] = 0;
        }
    }
    return false;
}

3.NestedScrollingChild完成對(duì)滾動(dòng)事件的消耗问欠,并向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滑動(dòng),返回自己滑動(dòng)消耗的
            consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
            //獲取未消耗的
            unconsumedY = y - consumedY;
        }
    }
    //自己滑動(dòng)消耗完事件后粒蜈,向NestedScrollingParent報(bào)告自己滑動(dòng)的情況顺献,父View此時(shí)還可以進(jìn)行一些滑動(dòng)操作等
    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,代碼很簡(jiǎn)單

  1. 停止嵌套滑動(dòng)

ACTION_UP或者ACTION_CANCEL觸發(fā)后枯怖,都會(huì)調(diào)用resetTouch這個(gè)方法注整。

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) {
    //這兩個(gè)方法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.開(kāi)始嵌套滑動(dòng)
            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);
            //簡(jiǎn)單認(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;

            //如果滑動(dòng)完成了
            if (scroller.isFinished() || (!fullyConsumedAny
                    && !hasNestedScrollingParent(TYPE_NON_TOUCH))) {
                setScrollState(SCROLL_STATE_IDLE);
                if (ALLOW_THREAD_GAP_WORK) {
                    mPrefetchRegistry.clearPrefetchPositions();
                }
                //停止嵌套滑動(dòng)
                stopNestedScroll(TYPE_NON_TOUCH);
            } else {
                //滑動(dòng)沒(méi)有完成,繼續(xù)post執(zhí)行run方法
                postOnAnimation();
            }
        }   
    }
}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末蕊程,一起剝皮案震驚了整個(gè)濱河市椒袍,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌藻茂,老刑警劉巖驹暑,帶你破解...
    沈念sama閱讀 222,104評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異捌治,居然都是意外死亡岗钩,警方通過(guò)查閱死者的電腦和手機(jī)纽窟,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)肖油,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人臂港,你說(shuō)我怎么就攤上這事森枪。” “怎么了审孽?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,697評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵县袱,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我佑力,道長(zhǎng)式散,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,836評(píng)論 1 298
  • 正文 為了忘掉前任打颤,我火速辦了婚禮暴拄,結(jié)果婚禮上漓滔,老公的妹妹穿的比我還像新娘。我一直安慰自己乖篷,他們只是感情好响驴,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,851評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著撕蔼,像睡著了一般豁鲤。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上鲸沮,一...
    開(kāi)封第一講書(shū)人閱讀 52,441評(píng)論 1 310
  • 那天琳骡,我揣著相機(jī)與錄音,去河邊找鬼诉探。 笑死日熬,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的肾胯。 我是一名探鬼主播竖席,決...
    沈念sama閱讀 40,992評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼敬肚!你這毒婦竟也來(lái)了毕荐?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,899評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤艳馒,失蹤者是張志新(化名)和其女友劉穎憎亚,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體弄慰,經(jīng)...
    沈念sama閱讀 46,457評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡第美,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,529評(píng)論 3 341
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了陆爽。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片什往。...
    茶點(diǎn)故事閱讀 40,664評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖慌闭,靈堂內(nèi)的尸體忽然破棺而出别威,到底是詐尸還是另有隱情,我是刑警寧澤驴剔,帶...
    沈念sama閱讀 36,346評(píng)論 5 350
  • 正文 年R本政府宣布省古,位于F島的核電站,受9級(jí)特大地震影響丧失,放射性物質(zhì)發(fā)生泄漏豺妓。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,025評(píng)論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望琳拭。 院中可真熱鬧载佳,春花似錦、人聲如沸臀栈。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,511評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)权薯。三九已至姑躲,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間盟蚣,已是汗流浹背黍析。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,611評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留屎开,地道東北人阐枣。 一個(gè)月前我還...
    沈念sama閱讀 49,081評(píng)論 3 377
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像奄抽,于是被迫代替她去往敵國(guó)和親蔼两。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,675評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容