NestedScrolling機(jī)制閱讀筆記

簡(jiǎn)述

在Android5.0之后,官方提供了一種特定的手勢(shì)處理機(jī)制:嵌套滑動(dòng)
其實(shí)主要的用途就是當(dāng)一個(gè)可以滑動(dòng)的頁面中有其它可以滑動(dòng)的控件的時(shí)候窃页,希望這個(gè)控件在滑動(dòng)到一定條件之后頁面開始滑動(dòng)跺株,這個(gè)可能是相對(duì)來說比較常見的場(chǎng)景
官方為了兼容一些5.0以下的情況复濒,在v4包中提供了NestedScrollingChildHelper和NestedScrollingParentHelper來允許自定義處理
當(dāng)然像舊版本類似ListView之類的控件就不支持這套機(jī)制,一般常用的可能是SwipeRefreshLayout乒省、RecyclerView和NestedScrollView等v4的控件

流程分析

為了更好的理解這個(gè)機(jī)制巧颈,通過一個(gè)例子的流程來分析可能會(huì)好一點(diǎn)
首先要找到兩個(gè)官方支持的控件,這里選擇了NestedScrollView和RecyclerView

public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild {

public class NestedScrollView extends FrameLayout implements NestedScrollingParent,
        NestedScrollingChild, ScrollingView {

NestedScrollView默認(rèn)實(shí)現(xiàn)NestedScrollingParent和NestedScrollingChild
RecyclerView默認(rèn)實(shí)現(xiàn)NestedScrollingChild
接下來看一下這兩個(gè)接口:

    /**
     * 一個(gè)應(yīng)該被ViewGroup所實(shí)現(xiàn)的接口袖扛,如果你希望支持在一個(gè)支持nested scrolling的子視圖中進(jìn)行滑動(dòng)的時(shí)候來代理的話
     *
     * 實(shí)現(xiàn)該接口的類應(yīng)該創(chuàng)建一個(gè)final類型的變量NestedScrollingParentHelper砸泛,在一些處理中可以直接進(jìn)行代理
     *
     * 在處理嵌套滑動(dòng)的時(shí)候最好通過ViewCompat、ViewGroupCompat或者ViewParentCompat的靜態(tài)方法
     * 這樣可以確保在Android的不同版本的一致性
     */
    public interface NestedScrollingParent {
        /**
         * 在子視圖調(diào)用startNestedScroll方法之后蛆封,這個(gè)方法會(huì)被調(diào)用
         * startNestedScroll會(huì)從當(dāng)前視圖向上詢問那些想要處理的nested scrolling的父布局
         *
         * 這個(gè)方法應(yīng)該在一個(gè)ViewGroup希望支持嵌套滑動(dòng)的時(shí)候?qū)崿F(xiàn)唇礁。
         * 如果返回true,當(dāng)前ViewGroup就會(huì)成為nested scrolling parent來代理滑動(dòng)過程中的一些行為惨篱。
         * 當(dāng)一套nested scroll完整結(jié)束之后將會(huì)調(diào)用onStopNestedScroll來結(jié)束當(dāng)前嵌套滑動(dòng)
         *
         * @param child 當(dāng)前可以代理嵌套滑動(dòng)的父布局ViewGroup
         * @param target 當(dāng)前發(fā)出嵌套滑動(dòng)開始的子視圖
         * @param nestedScrollAxes 滑動(dòng)方向標(biāo)志
         * @return true表示當(dāng)前父布局可以處理嵌套滑動(dòng)
         */
        public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);

        /**
         * 該方法會(huì)在onStartNestedScroll返回true之后調(diào)用一次
         * 主要是提供一個(gè)機(jī)會(huì)在處理nested scroll之前進(jìn)行一些配置的初始化
         *
         * @param child 當(dāng)前可以代理嵌套滑動(dòng)的父布局ViewGroup
         * @param target 當(dāng)前發(fā)出嵌套滑動(dòng)開始的子視圖
         * @param nestedScrollAxes 滑動(dòng)方向標(biāo)志
         */
        public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);

        /**
         * 該方法一般在一套完整的nested scroll完成之后調(diào)用盏筐,比方說一般在ACTION_UP和ACTION_CANCEL事件中回調(diào)
         *
         * @param target 當(dāng)前發(fā)出嵌套滑動(dòng)開始的子視圖
         */
        public void onStopNestedScroll(View target);

        /**
         * 這個(gè)方法會(huì)在一個(gè)支持nested scrolling的子視圖分發(fā)nested scroll事件的時(shí)候調(diào)用。
         * 一般可能是MOVE事件的時(shí)候分發(fā)
         * 這個(gè)方法要進(jìn)行回調(diào)砸讳,首先在之前的onStartNestedScroll中要返回true
         *
         * @param target 當(dāng)前發(fā)出嵌套滑動(dòng)開始的子視圖
         * @param dxConsumed 當(dāng)前在水平方向上target已經(jīng)消耗了的偏移量
         * @param dyConsumed 當(dāng)前在豎直方向上target已經(jīng)消耗了的偏移量
         * @param dxUnconsumed 當(dāng)前在水平方向上target還沒有消耗的偏移量
         * @param dyUnconsumed 當(dāng)前在豎直方向上target還沒有消耗的偏移量
         */
        public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
                                   int dxUnconsumed, int dyUnconsumed);

        /**
         * 可以通過這個(gè)方法在當(dāng)前發(fā)出嵌套滑動(dòng)開始操作的子視圖滑動(dòng)之前先消費(fèi)事件
         *
         * 在實(shí)現(xiàn)該方法的時(shí)候應(yīng)該指明消費(fèi)了多少滑動(dòng)的距離琢融,通過consumed數(shù)組,該數(shù)組一定非null簿寂,默認(rèn)值都為0
         * consumed[0]表示水平方向上面消費(fèi)了的距離
         * consumed[1]表示豎直方向上面消費(fèi)了的距離
         *
         * @param target 當(dāng)前發(fā)出嵌套滑動(dòng)開始的子視圖
         * @param dx 當(dāng)前發(fā)出嵌套滑動(dòng)開始的子視圖在當(dāng)前滑動(dòng)中的水平偏移量
         * @param dy 當(dāng)前發(fā)出嵌套滑動(dòng)開始的子視圖在當(dāng)前滑動(dòng)中的豎直偏移量
         * @param consumed 當(dāng)前代理了嵌套滑動(dòng)的父布局在此次滑動(dòng)中消耗的偏移量
         */
        public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);

        /**
         * nested scroll中處理fling事件
         *
         * @param target 當(dāng)前發(fā)出嵌套滑動(dòng)開始的子視圖
         * @param velocityX 1s內(nèi)水平方向的偏移速度
         * @param velocityY 1s內(nèi)豎直方向的偏移速度
         * @param consumed true表示當(dāng)前發(fā)出嵌套滑動(dòng)開始的子視圖是否會(huì)接著進(jìn)行fling操作
         * @return true表示當(dāng)前父布局在當(dāng)前發(fā)出嵌套滑動(dòng)開始的子視圖fling之前消費(fèi)了fling漾抬,后續(xù)子視圖還是可以繼續(xù)處理fling
         */
        public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);

        /**
         * 在當(dāng)前發(fā)出嵌套滑動(dòng)開始的子視圖消費(fèi)fling事件之前的處理
         *
         * 一般來說這個(gè)方法在調(diào)用之前已經(jīng)通過VelocityTracker完成了fling相關(guān)的參數(shù)計(jì)算
         * 規(guī)范的說后續(xù)的velocityX/Y都會(huì)在ViewConfiguration.getScaledMinimumFlingVelocity()/ViewConfiguration.getScaledMaximumFlingVelocity()之間
         *
         * 通過返回true將會(huì)告知當(dāng)前發(fā)出嵌套滑動(dòng)開始的子視圖不處理fling操作,相當(dāng)于攔截
         *
         * @param target 當(dāng)前發(fā)出嵌套滑動(dòng)開始的子視圖
         * @param velocityX 1s內(nèi)水平方向的偏移速度
         * @param velocityY 1s內(nèi)豎直方向的偏移速度
         * @return true表示當(dāng)前父布局在當(dāng)前發(fā)出嵌套滑動(dòng)開始的子視圖fling之前就完全消費(fèi)了fling常遂,會(huì)導(dǎo)致子視圖不處理fling
         */
        public boolean onNestedPreFling(View target, float velocityX, float velocityY);

        /**
         * 返回當(dāng)前NestedScrollingParent進(jìn)行nested scrolling的方向
         *
         * @return Flags indicating the current axes of nested scrolling
         * @see ViewCompat#SCROLL_AXIS_HORIZONTAL 水平方向
         * @see ViewCompat#SCROLL_AXIS_VERTICAL 豎直方向
         * @see ViewCompat#SCROLL_AXIS_NONE
         */
        public int getNestedScrollAxes();
    }

    /**
     * 這個(gè)接口一般是由View實(shí)現(xiàn)
     * 然后結(jié)合實(shí)現(xiàn)NestedScrollingParent的ViewGroup來完成一組嵌套滑動(dòng)
     *
     * 實(shí)現(xiàn)該接口的類需要?jiǎng)?chuàng)建一個(gè)final的NestedScrollingChildHelper對(duì)象
     * 然后可以將內(nèi)部大多數(shù)的方法委托給該對(duì)象處理
     *
     * 盡量通過ViewCompat纳令、ViewGroupCompat、ViewParentCompat處理邏輯烈钞,這樣可以保證不同Android版本的兼容性
     */
    public interface NestedScrollingChild {
        /**
         * 允許/禁止當(dāng)前視圖進(jìn)行嵌套滑動(dòng)操作
         *
         * 如果當(dāng)前視圖被禁止進(jìn)行嵌套滑動(dòng)
         * 那么后續(xù)的類似dispatchNestedScroll等操作都會(huì)不處理
         *
         * @param enabled true的話表示允許泊碑,否則不允許,如果之前是允許狀態(tài)變?yōu)椴辉试S毯欣,那么還會(huì)發(fā)出stopNestedScroll事件
         */
        public void setNestedScrollingEnabled(boolean enabled);

        /**
         * 當(dāng)前視圖是否可以進(jìn)行嵌套滑動(dòng)操作
         * @return true表示可以
         */
        public boolean isNestedScrollingEnabled();

        /**
         * 開始指定方向的嵌套滑動(dòng)
         *
         * 一般來說馒过,視圖應(yīng)該在滑動(dòng)行為一開始就調(diào)用startNestedScroll這個(gè)方法
         * 比方說在ACTION_DOWN這個(gè)事件的時(shí)候
         *
         * 當(dāng)前方法返回true,說明找到了可以進(jìn)行嵌套滑動(dòng)的父布局ViewGroup
         * 否則只能等待下一次的事件序列開始再重新查找酗钞,當(dāng)前的事件序列默認(rèn)沒有嵌套滑動(dòng)
         * 假設(shè)之前已經(jīng)找到了一個(gè)嵌套滑動(dòng)的ViewGroup腹忽,后續(xù)會(huì)直接使用之前找到的ViewGroup來進(jìn)行嵌套滑動(dòng)
         *
         * 視圖在滑動(dòng)之前應(yīng)該先調(diào)用一次dispatchNestedPreScroll來進(jìn)行計(jì)算實(shí)際需要的滑動(dòng)偏移量
         * 如果返回true,意味著有父布局消費(fèi)了一些滑動(dòng)偏移量砚作,那么后續(xù)的滑動(dòng)應(yīng)該基于消費(fèi)后的偏移量進(jìn)行
         *
         * 接著窘奏,需要在當(dāng)前視圖滑動(dòng)之前通過調(diào)用dispatchNestedScroll來進(jìn)行滑動(dòng)事件分發(fā)
         * 這個(gè)過程中可能相應(yīng)嵌套滑動(dòng)的ViewGroup會(huì)消耗滑動(dòng)偏移量
         * 最后再剩余的滑動(dòng)偏移量的基礎(chǔ)上,再考慮是否進(jìn)行當(dāng)前視圖的滑動(dòng)
         *
         * @param axes 嵌套滑動(dòng)的方向標(biāo)志{@link ViewCompat#SCROLL_AXIS_HORIZONTAL}
         *             and/or {@link ViewCompat#SCROLL_AXIS_VERTICAL}.
         * @return true表示當(dāng)前找到了可以進(jìn)行嵌套滑動(dòng)的父布局ViewGroup
         */
        public boolean startNestedScroll(int axes);

        /**
         * 停止進(jìn)行中的嵌套滑動(dòng)
         *
         * @see #startNestedScroll(int)
         */
        public void stopNestedScroll();

        /**
         * 當(dāng)前視圖是否已經(jīng)找到了一個(gè)可以進(jìn)行嵌套滑動(dòng)的父布局ViewGroup
         */
        public boolean hasNestedScrollingParent();

        /**
         * 分發(fā)一個(gè)滑動(dòng)事件
         *
         * 在View自己消費(fèi)滑動(dòng)事件之后葫录,將事件告知可以進(jìn)行嵌套滑動(dòng)的父ViewGroup
         *
         * 如果當(dāng)前不允許進(jìn)行嵌套滑動(dòng)或者在這之前沒有找到可以進(jìn)行嵌套滑動(dòng)的父ViewGroup
         * 當(dāng)前分發(fā)無效
         *
         * @param dxConsumed 滑動(dòng)中當(dāng)前視圖已經(jīng)消費(fèi)的水平偏移量
         * @param dyConsumed 滑動(dòng)中當(dāng)前視圖已經(jīng)消費(fèi)的豎直偏移量
         * @param dxUnconsumed 滑動(dòng)中當(dāng)前視圖還沒有消費(fèi)的水平偏移量
         * @param dyUnconsumed 滑動(dòng)中當(dāng)前視圖還沒有消費(fèi)的豎直偏移量
         * @param offsetInWindow 可選項(xiàng)着裹。如果當(dāng)前非null,那么會(huì)在將滑動(dòng)事件分發(fā)給嵌套滑動(dòng)的父ViewGroup之前和之后分別計(jì)算
         *                       一次當(dāng)前視圖在window的位置米同,然后進(jìn)行做差骇扇。
         *                       這個(gè)是反映在嵌套滑動(dòng)之后視圖的位置變化摔竿,可以用于后續(xù)調(diào)整手指坐標(biāo)
         * @return true表示滑動(dòng)分發(fā)完成
         */
        public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
                                            int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);

        /**
         * 在視圖滑動(dòng)之前提供一個(gè)機(jī)會(huì)給嵌套滑動(dòng)的父布局消耗偏移量
         * 相當(dāng)于進(jìn)行滑動(dòng)前偏移量分發(fā)
         *
         * @param dx 當(dāng)前視圖水平滑動(dòng)的偏移量
         * @param dy 當(dāng)前視圖豎直滑動(dòng)的偏移量
         * @param consumed 輸出。如果非null少孝,通過consumed[0]記錄嵌套滑動(dòng)中父布局消費(fèi)的水平滑動(dòng)偏移量继低,
         *                 通過consumed[1]記錄嵌套滑動(dòng)中父布局消費(fèi)的豎直滑動(dòng)偏移量
         * @param offsetInWindow 可選項(xiàng)。如果當(dāng)前非null稍走,那么會(huì)在將滑動(dòng)事件分發(fā)給嵌套滑動(dòng)的父ViewGroup之前和之后分別計(jì)算
         *                       一次當(dāng)前視圖在window的位置袁翁,然后進(jìn)行做差。
         *                       這個(gè)是反映在嵌套滑動(dòng)之后視圖的位置變化婿脸,可以用于后續(xù)調(diào)整手指坐標(biāo)
         * @return true表示當(dāng)前處理嵌套滑動(dòng)的父布局消耗了一些滑動(dòng)偏移量
         */
        public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);

        /**
         * 分發(fā)fling事件給處理嵌套滑動(dòng)的父布局ViewGroup
         *
         * @param velocityX 1s內(nèi)視圖水平滑動(dòng)的速度
         * @param velocityY 1s內(nèi)視圖豎直滑動(dòng)的速度
         * @param consumed true表示當(dāng)前視圖將會(huì)處理fling粱胜,否則不處理
         * @return true表示當(dāng)前處理嵌套滑動(dòng)的父布局ViewGroup消耗了fling
         */
        public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);

        /**
         * 在視圖處理fling之前先分發(fā)fling事件給當(dāng)前處理嵌套滑動(dòng)的父布局ViewGroup
         *
         * 實(shí)際上這個(gè)方法的主要作用是用來攔截fling事件
         * 如果返回true的話視圖將不會(huì)再處理fling
         *
         * 一般來說這個(gè)方法有兩種處理邏輯:
         * 1.如果視圖被分頁,并且要調(diào)到指定的頁的位置盖淡,那么不需要調(diào)用dispatchNestedPreFling
         * 2.如果前處理嵌套滑動(dòng)的父布局ViewGroup消費(fèi)了fling事件年柠,那么當(dāng)前視圖則不應(yīng)該繼續(xù)滑動(dòng)
         *
         * 視圖不應(yīng)該提供不支持的滑動(dòng)方向的fling豎直,比方說ScrollView不應(yīng)該提供水平方向的滑動(dòng)速度
         *
         * @param velocityX 1s內(nèi)視圖水平滑動(dòng)的速度
         * @param velocityY 1s內(nèi)視圖豎直滑動(dòng)的速度
         * @return true表示當(dāng)前處理嵌套滑動(dòng)的父布局ViewGroup在子視圖fling之前就有消耗fling
         */
        public boolean dispatchNestedPreFling(float velocityX, float velocityY);
    }

從基本的注釋中可以看到褪迟,實(shí)際上NestedScrollingChild相當(dāng)于嵌套滑動(dòng)發(fā)起者冗恨,而 NestedScrollingParent則是嵌套滑動(dòng)的處理者。
一般來說NestedScrollingChild通過委托v4包中的NestedScrollingChildHelper處理味赃,內(nèi)部完成了預(yù)設(shè)的嵌套發(fā)起相關(guān)的邏輯掀抹。
比方說看一下RecyclerView基于NestedScrollingChild接口的實(shí)現(xiàn):

    @Override
    public void setNestedScrollingEnabled(boolean enabled) {
        mScrollingChildHelper.setNestedScrollingEnabled(enabled);
    }

    @Override
    public boolean isNestedScrollingEnabled() {
        return mScrollingChildHelper.isNestedScrollingEnabled();
    }

    @Override
    public boolean startNestedScroll(int axes) {
        return mScrollingChildHelper.startNestedScroll(axes);
    }

    @Override
    public void stopNestedScroll() {
        mScrollingChildHelper.stopNestedScroll();
    }

    @Override
    public boolean hasNestedScrollingParent() {
        return mScrollingChildHelper.hasNestedScrollingParent();
    }

    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
            int dyUnconsumed, int[] offsetInWindow) {
        return mScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed,
                dxUnconsumed, dyUnconsumed, offsetInWindow);
    }

    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
        return mScrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
    }

    @Override
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
        return mScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
    }

    @Override
    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
        return mScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY);
    }

接下里繼續(xù)從RecyclerView入手分析,主要看攔截事件和處理事件這兩個(gè)關(guān)鍵邏輯:

    @Override
    public boolean onInterceptTouchEvent(MotionEvent e) {
        //...
        final int action = MotionEventCompat.getActionMasked(e);
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                //...
                int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
                if (canScrollHorizontally) {//水平方向滑動(dòng)
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
                }
                if (canScrollVertically) {//豎直方向滑動(dòng)
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
                }
                startNestedScroll(nestedScrollAxis);
                break;

            case MotionEvent.ACTION_UP: {
                //...
                stopNestedScroll();
            } break;

            case MotionEvent.ACTION_CANCEL: {
                //...
                stopNestedScroll();
            }
        }
        //...
    }

    @Override
    public boolean onTouchEvent(MotionEvent e) {
        //...
        if (action == MotionEvent.ACTION_DOWN) {
            mNestedOffsets[0] = mNestedOffsets[1] = 0;
        }
        vtev.offsetLocation(mNestedOffsets[0], mNestedOffsets[1]);

        switch (action) {
            case MotionEvent.ACTION_DOWN: {
                //...
                int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
                if (canScrollHorizontally) {
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
                }
                if (canScrollVertically) {
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
                }
                startNestedScroll(nestedScrollAxis);
            } break;

            case MotionEventCompat.ACTION_POINTER_DOWN: {
                //....
            } break;

            case MotionEvent.ACTION_MOVE: {
                final int index = MotionEventCompat.findPointerIndex(e, 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) (MotionEventCompat.getX(e, index) + 0.5f);
                final int y = (int) (MotionEventCompat.getY(e, index) + 0.5f);
                int dx = mLastTouchX - x;//當(dāng)前水平滑動(dòng)的偏移量
                int dy = mLastTouchY - y;//當(dāng)前豎直滑動(dòng)的偏移量
                //首先進(jìn)入分發(fā)準(zhǔn)備滑動(dòng)階段
                if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
                    dx -= mScrollConsumed[0];//之后可用的水平滑動(dòng)的偏移量需要減去處理嵌套滑動(dòng)的父布局消費(fèi)的水平偏移量
                    dy -= mScrollConsumed[1];//之后可用的豎直滑動(dòng)的偏移量需要減去處理嵌套滑動(dòng)的父布局消費(fèi)的豎直偏移量
                    vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
                    // Updated the nested offsets
                    mNestedOffsets[0] += mScrollOffset[0];
                    mNestedOffsets[1] += mScrollOffset[1];
                }

                //...

                if (mScrollState == SCROLL_STATE_DRAGGING) {
                    mLastTouchX = x - mScrollOffset[0];
                    mLastTouchY = y - mScrollOffset[1];
                    //這里是實(shí)際滑動(dòng)的處理
                    if (scrollByInternal(
                            canScrollHorizontally ? dx : 0,
                            canScrollVertically ? dy : 0,
                            vtev)) {
                        getParent().requestDisallowInterceptTouchEvent(true);
                    }
                }
            } break;

            case MotionEventCompat.ACTION_POINTER_UP: {
                //...
            } break;

            case MotionEvent.ACTION_UP: {
                //先計(jì)算當(dāng)前fling速度
                mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
                final float xvel = canScrollHorizontally ?
                        -VelocityTrackerCompat.getXVelocity(mVelocityTracker, mScrollPointerId) : 0;
                final float yvel = canScrollVertically ?
                        -VelocityTrackerCompat.getYVelocity(mVelocityTracker, mScrollPointerId) : 0;
                //這里處理fling的實(shí)際邏輯
                if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
                    setScrollState(SCROLL_STATE_IDLE);
                }

                mVelocityTracker.clear();
                releaseGlows();
            } break;

            case MotionEvent.ACTION_CANCEL: {
                //...
                stopNestedScroll();
            } break;
        }
        //...
        return true;
    }

    boolean scrollByInternal(int x, int y, MotionEvent ev) {
        //...
        if (mAdapter != null) {
            //...
            if (x != 0) {
                //這里實(shí)際上就是通過LayoutManager完成RecyclerView的水平滑動(dòng)心俗,并且從中可以計(jì)算出當(dāng)前水平滑動(dòng)消費(fèi)的偏移量
                consumedX = mLayout.scrollHorizontallyBy(x, mRecycler, mState);
                //計(jì)算當(dāng)前手指水平移動(dòng)時(shí)還沒有被消費(fèi)的偏移量
                unconsumedX = x - consumedX;
            }
            if (y != 0) {
                //這里實(shí)際上就是通過LayoutManager完成RecyclerView的豎直滑動(dòng)傲武,并且從中可以計(jì)算出當(dāng)前豎直滑動(dòng)消費(fèi)的偏移量
                consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
                //計(jì)算當(dāng)前手指豎直移動(dòng)時(shí)還沒有被消費(fèi)的偏移量
                unconsumedY = y - consumedY;
            }
            //這里說一個(gè)簡(jiǎn)單例子就理解了,假設(shè)一個(gè)RecyclerView距離最頂部只有2px城榛,但是本次手指move了5px
            //那么consumedX就是2px揪利,unconsumedX就是3px
            //其實(shí)就是相當(dāng)于一個(gè)常用場(chǎng)景,當(dāng)RecyclerView滑動(dòng)2px到頂部之後狠持,其它視圖可以接著拿到剩下的3px做操作
            //這就是嵌套滑動(dòng)
            //...
        }
        //...
        //當(dāng)前視圖已經(jīng)滑動(dòng)完成疟位,進(jìn)行嵌套滑動(dòng)的滑動(dòng)分發(fā)
        //consumedX和consumedY表示本次視圖滑動(dòng)完成后消費(fèi)的偏移量
        //unconsumedX和unconsumedY表示本次視圖滑動(dòng)完成后還沒有消費(fèi)的偏移量
        if (dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset)) {
            // Update the last touch co-ords, taking any scroll offset into account
            mLastTouchX -= mScrollOffset[0];
            mLastTouchY -= mScrollOffset[1];
            if (ev != null) {
                ev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
            }
            mNestedOffsets[0] += mScrollOffset[0];
            mNestedOffsets[1] += mScrollOffset[1];
        } else if (ViewCompat.getOverScrollMode(this) != ViewCompat.OVER_SCROLL_NEVER) {
            //...
        }
        //...
    }

    public boolean fling(int velocityX, int velocityY) {
        //...
        //首先進(jìn)入準(zhǔn)備分發(fā)嵌套滑動(dòng)的fling事件階段
        if (!dispatchNestedPreFling(velocityX, velocityY)) {//如果返回true,則視圖不會(huì)再進(jìn)行fling操作
            //首先分發(fā)嵌套滑動(dòng)的fling事件
            dispatchNestedFling(velocityX, velocityY, canScroll);

            if (canScroll) {
                //...
                //這里才是進(jìn)行視圖本身的fling操作
                mViewFlinger.fling(velocityX, velocityY);
                return true;
            }
        }
        return false;
    }

可以看到喘垂,這就是一個(gè)標(biāo)準(zhǔn)的View對(duì)于嵌套滑動(dòng)發(fā)起流程:
1.攔截事件階段:
DOWN事件查找是否有響應(yīng)嵌套滑動(dòng)的父布局
UP和CANCEL事件停止嵌套滑動(dòng)甜刻。
2.處理事件階段:
DOWN事件查找是否有響應(yīng)嵌套滑動(dòng)的父布局。
MOVE事件首先在視圖滑動(dòng)前先分發(fā)滑動(dòng)偏移量正勒,然后計(jì)算剩余偏移量得院,然后視圖根據(jù)剩余的偏移量進(jìn)行滑動(dòng),接著分發(fā)嵌套滑動(dòng)的滑動(dòng)事件章贞。
CANCEL事件停止嵌套滑動(dòng)祥绞。
UP事件如果有fling,先分發(fā)嵌套滑動(dòng)準(zhǔn)備fling事件,如果返回true則完成蜕径,否則繼續(xù)分發(fā)嵌套滑動(dòng)fling事件怪蔑,再接著視圖本身進(jìn)行fling操作。如果沒有fling丧荐,一般可以直接停止嵌套滑動(dòng)。
再看NestedScrollingParentHelper的實(shí)現(xiàn)之前喧枷,先通過NestedScrollView的實(shí)現(xiàn)把NestedScrollingParent的實(shí)現(xiàn)理解虹统,從而理解完整的嵌套滑動(dòng)機(jī)制:

    @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        //實(shí)際上這里就是如果子視圖發(fā)出的是豎直方向上面的嵌套滑動(dòng)
        //那么NestedScrollView就接受
        //這里就是用于判斷接受嵌套滑動(dòng)的條件
        return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
    }

    @Override
    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
        //當(dāng)前是第一次接受了嵌套滑動(dòng)請(qǐng)求
        mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
        //因?yàn)镹estedScrollView本身也實(shí)現(xiàn)NestedScrollingChild,那么在自身處理嵌套滑動(dòng)之前將該事件繼續(xù)向上詢問
        //越頂層的視圖越先處理
        startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
    }

    @Override
    public void onStopNestedScroll(View target) {
        mParentHelper.onStopNestedScroll(target);
        stopNestedScroll();
    }

    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
                               int dyUnconsumed) {
        final int oldScrollY = getScrollY();
        scrollBy(0, dyUnconsumed);//這里就是根據(jù)當(dāng)前發(fā)起嵌套滑動(dòng)視圖還沒有消費(fèi)的豎直偏移量進(jìn)行滑動(dòng)
        final int myConsumed = getScrollY() - oldScrollY;//計(jì)算當(dāng)前NestedScrollView豎直滑動(dòng)消費(fèi)的偏移量隧甚,因?yàn)榭赡苤幌M(fèi)了一部分
        final int myUnconsumed = dyUnconsumed - myConsumed;
        //將嵌套滑動(dòng)繼續(xù)向上發(fā)送
        dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null);
    }

    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        //可以看到NestedScrollView在滑動(dòng)之前不需要做什么
        //只是單純向上發(fā)出嵌套滑動(dòng)請(qǐng)求
        dispatchNestedPreScroll(dx, dy, consumed, null);
    }

    @Override
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
        if (!consumed) {//當(dāng)前fling沒有被子視圖消費(fèi)
            //那么進(jìn)行NestedScrollView自身的fling操作
            flingWithNestedDispatch((int) velocityY);
            return true;
        }
        return false;
    }

    @Override
    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
        return dispatchNestedPreFling(velocityX, velocityY);
    }

    @Override
    public int getNestedScrollAxes() {
        return mParentHelper.getNestedScrollAxes();
    }

可以看到车荔,NestedScrollView作為一個(gè)即實(shí)現(xiàn)了NestedScrollingChild和NestedScrollingParent的視圖,在處理嵌套滑動(dòng)的時(shí)候戚扳,會(huì)在特定的回調(diào)中繼續(xù)向上發(fā)起嵌套滑動(dòng)請(qǐng)求忧便。
事件分發(fā)是從視圖樹頂層向下分發(fā),而嵌套滑動(dòng)則是剛好相反帽借,從接收到事件的視圖開始珠增,向視圖樹上面進(jìn)行分發(fā)。
NestedScrollView在嵌套滑動(dòng)中的處理砍艾,簡(jiǎn)單說就是你不滑了我接著滑這種表現(xiàn)形式蒂教。
接下來看NestedScrollingParentHelper的實(shí)現(xiàn),可以看到在NestedScrollView中的回調(diào)都委托了它進(jìn)行處理:

    public class NestedScrollingParentHelper {
        private final ViewGroup mViewGroup;//記錄當(dāng)前使用nested scrolling的ViewGroup
        private int mNestedScrollAxes;//當(dāng)前ViewGroup所接受的嵌套滑動(dòng)的方向

        public NestedScrollingParentHelper(ViewGroup viewGroup) {
            mViewGroup = viewGroup;
        }

        /**
         * 在實(shí)現(xiàn)NestedScrollingParent的onNestedScrollAccepted中使用
         */
        public void onNestedScrollAccepted(View child, View target, int axes) {
            mNestedScrollAxes = axes;//其實(shí)就是記錄了當(dāng)前ViewGroup接受的嵌套滑動(dòng)的方向
        }

        /**
         * 返回當(dāng)前ViewGroup所接受的嵌套滑動(dòng)的方向
         * 
         */
        public int getNestedScrollAxes() {
            return mNestedScrollAxes;
        }

        /**
         * 在實(shí)現(xiàn)NestedScrollingParent的onStopNestedScroll中使用
         */
        public void onStopNestedScroll(View target) {
            mNestedScrollAxes = 0;//還原了當(dāng)前可以接受的嵌套滑動(dòng)的方向脆荷,等待下一次接受嵌套滑動(dòng)的時(shí)候再重新賦值
        }
    }

其實(shí)就是預(yù)定義了幾個(gè)方法凝垛,內(nèi)部中記錄了幾個(gè)參數(shù)而已。
接著看比較重要的NestedScrollingChildHelper:

    public class NestedScrollingChildHelper {
        private final View mView;//當(dāng)前進(jìn)行嵌套滑動(dòng)事件分發(fā)的視圖
        private ViewParent mNestedScrollingParent;//當(dāng)前已經(jīng)找到的響應(yīng)mView發(fā)出的嵌套滑動(dòng)請(qǐng)求的父視圖組
        private boolean mIsNestedScrollingEnabled;//當(dāng)前mView是否可以使用嵌套滑動(dòng)機(jī)制
        private int[] mTempNestedScrollConsumed;//一個(gè)數(shù)組蜓谋,為了避免多次構(gòu)建數(shù)組來存放當(dāng)前消費(fèi)滑動(dòng)的偏移量

        public NestedScrollingChildHelper(View view) {
            mView = view;
        }

        /**
         * 是否允許mView進(jìn)行嵌套滑動(dòng)機(jī)制
         */
        public void setNestedScrollingEnabled(boolean enabled) {
            if (mIsNestedScrollingEnabled) {//如果之前是允許進(jìn)行梦皮,然后修改狀態(tài)為不允許,此時(shí)要先分發(fā)嵌套滑動(dòng)停止事件
                ViewCompat.stopNestedScroll(mView);
            }
            mIsNestedScrollingEnabled = enabled;
        }

        /**
         * 當(dāng)前是否允許進(jìn)行嵌套滑動(dòng)機(jī)制
         */
        public boolean isNestedScrollingEnabled() {
            return mIsNestedScrollingEnabled;
        }

        /**
         * 當(dāng)前是否找到了響應(yīng)當(dāng)前視圖的嵌套滑動(dòng)的父視圖組
         */
        public boolean hasNestedScrollingParent() {
            return mNestedScrollingParent != null;
        }

        /**
         * 通過mView發(fā)起一個(gè)新的嵌套滑動(dòng)請(qǐng)求
         */
        public boolean startNestedScroll(int axes) {
            if (hasNestedScrollingParent()) {
                // 當(dāng)前已經(jīng)處于嵌套滑動(dòng)中桃焕,不需要重新查找
                return true;
            }
            if (isNestedScrollingEnabled()) {//當(dāng)前mView允許進(jìn)行嵌套滑動(dòng)機(jī)制
                ViewParent p = mView.getParent();
                View child = mView;
                while (p != null) {
                    //查找一個(gè)可以響應(yīng)mView發(fā)出的嵌套滑動(dòng)的父視圖組
                    if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
                        //成功找到響應(yīng)的父視圖組
                        mNestedScrollingParent = p;//記錄當(dāng)前響應(yīng)的父視圖組
                        //回調(diào)一次onNestedScrollAccepted方法
                        ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
                        return true;
                    }
                    //向視圖樹上方進(jìn)行遍歷
                    if (p instanceof View) {
                        child = (View) p;
                    }
                    p = p.getParent();
                }
            }
            return false;//當(dāng)前無法找到進(jìn)行嵌套滑動(dòng)的父視圖組
        }

        /**
         * 停止嵌套滑動(dòng)
         */
        public void stopNestedScroll() {
            if (mNestedScrollingParent != null) {
                ViewParentCompat.onStopNestedScroll(mNestedScrollingParent, mView);
                mNestedScrollingParent = null;
            }
        }

        /**
         * 分發(fā)嵌套滑動(dòng)的滑動(dòng)事件
         */
        public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
                                            int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
            if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
                if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) {//當(dāng)前有滑動(dòng)發(fā)生
                    int startX = 0;
                    int startY = 0;
                    if (offsetInWindow != null) {
                        mView.getLocationInWindow(offsetInWindow);
                        startX = offsetInWindow[0];
                        startY = offsetInWindow[1];
                    }

                    ViewParentCompat.onNestedScroll(mNestedScrollingParent, mView, dxConsumed,
                            dyConsumed, dxUnconsumed, dyUnconsumed);

                    if (offsetInWindow != null) {
                        mView.getLocationInWindow(offsetInWindow);
                        offsetInWindow[0] -= startX;
                        offsetInWindow[1] -= startY;
                        //可以看到剑肯,offsetInWindow這個(gè)參數(shù)實(shí)際上就是分發(fā)前后mView的位置變化
                    }
                    return true;
                } else if (offsetInWindow != null) {
                    // No motion, no dispatch. Keep offsetInWindow up to date.
                    offsetInWindow[0] = 0;
                    offsetInWindow[1] = 0;
                }
            }
            return false;
        }

        /**
         * 分發(fā)嵌套滑動(dòng)的準(zhǔn)備滑動(dòng)事件
         */
        public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
            if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
                if (dx != 0 || dy != 0) {
                    int startX = 0;
                    int startY = 0;
                    if (offsetInWindow != null) {
                        mView.getLocationInWindow(offsetInWindow);
                        startX = offsetInWindow[0];
                        startY = offsetInWindow[1];
                    }

                    if (consumed == null) {
                        if (mTempNestedScrollConsumed == null) {
                            mTempNestedScrollConsumed = new int[2];
                        }
                        consumed = mTempNestedScrollConsumed;
                    }
                    consumed[0] = 0;
                    consumed[1] = 0;
                    ViewParentCompat.onNestedPreScroll(mNestedScrollingParent, mView, dx, dy, consumed);

                    if (offsetInWindow != null) {
                        mView.getLocationInWindow(offsetInWindow);
                        offsetInWindow[0] -= startX;
                        offsetInWindow[1] -= startY;
                    }
                    return consumed[0] != 0 || consumed[1] != 0;
                } else if (offsetInWindow != null) {
                    offsetInWindow[0] = 0;
                    offsetInWindow[1] = 0;
                }
            }
            return false;
        }

        /**
         * 分發(fā)嵌套滑動(dòng)的fling事件
         */
        public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
            if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
                return ViewParentCompat.onNestedFling(mNestedScrollingParent, mView, velocityX,
                        velocityY, consumed);
            }
            return false;
        }

        /**
         * 分發(fā)嵌套滑動(dòng)的準(zhǔn)備進(jìn)行fling事件
         */
        public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
            if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
                return ViewParentCompat.onNestedPreFling(mNestedScrollingParent, mView, velocityX,
                        velocityY);
            }
            return false;
        }

        /**
         * mView在onDetachedFromWindow時(shí)候應(yīng)該調(diào)用該方法
         * 用于停止嵌套滑動(dòng)
         */
        public void onDetachedFromWindow() {
            ViewCompat.stopNestedScroll(mView);
        }

        /**
         * 當(dāng)嵌套滑動(dòng)停止的時(shí)候應(yīng)該調(diào)用
         */
        public void onStopNestedScroll(View child) {
            ViewCompat.stopNestedScroll(mView);
        }
    }

主要的工作系統(tǒng)已經(jīng)完成,剩下要做的主要就是在對(duì)應(yīng)的方法中進(jìn)行委托即可覆旭。

結(jié)語

嵌套滑動(dòng)是事件處理的一種機(jī)制退子,這個(gè)機(jī)制是完全基于事件分發(fā)來進(jìn)行的。
當(dāng)一個(gè)事件成功分發(fā)之后型将,一個(gè)視圖獲得了事件序列寂祥,那么這個(gè)視圖在后續(xù)的處理中,可以嘗試在這個(gè)過程中和父視圖組進(jìn)行聯(lián)動(dòng)七兜,在一些特定的事件中讓父視圖組進(jìn)行一些操作丸凭,這個(gè)才是嵌套滑動(dòng)的意義。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市惜犀,隨后出現(xiàn)的幾起案子铛碑,更是在濱河造成了極大的恐慌,老刑警劉巖虽界,帶你破解...
    沈念sama閱讀 218,525評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件汽烦,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡莉御,警方通過查閱死者的電腦和手機(jī)撇吞,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,203評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來礁叔,“玉大人牍颈,你說我怎么就攤上這事±殴兀” “怎么了煮岁?”我有些...
    開封第一講書人閱讀 164,862評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)涣易。 經(jīng)常有香客問我画机,道長(zhǎng),這世上最難降的妖魔是什么新症? 我笑而不...
    開封第一講書人閱讀 58,728評(píng)論 1 294
  • 正文 為了忘掉前任色罚,我火速辦了婚禮,結(jié)果婚禮上账劲,老公的妹妹穿的比我還像新娘戳护。我一直安慰自己,他們只是感情好瀑焦,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,743評(píng)論 6 392
  • 文/花漫 我一把揭開白布腌且。 她就那樣靜靜地躺著,像睡著了一般榛瓮。 火紅的嫁衣襯著肌膚如雪铺董。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,590評(píng)論 1 305
  • 那天禀晓,我揣著相機(jī)與錄音精续,去河邊找鬼。 笑死粹懒,一個(gè)胖子當(dāng)著我的面吹牛重付,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播凫乖,決...
    沈念sama閱讀 40,330評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼确垫,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼弓颈!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起删掀,我...
    開封第一講書人閱讀 39,244評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤翔冀,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后披泪,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體纤子,經(jīng)...
    沈念sama閱讀 45,693評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,885評(píng)論 3 336
  • 正文 我和宋清朗相戀三年款票,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了计福。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,001評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡徽职,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出佩厚,到底是詐尸還是另有隱情姆钉,我是刑警寧澤,帶...
    沈念sama閱讀 35,723評(píng)論 5 346
  • 正文 年R本政府宣布抄瓦,位于F島的核電站潮瓶,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏钙姊。R本人自食惡果不足惜毯辅,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,343評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望煞额。 院中可真熱鬧思恐,春花似錦、人聲如沸膊毁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,919評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽婚温。三九已至描焰,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間栅螟,已是汗流浹背荆秦。 一陣腳步聲響...
    開封第一講書人閱讀 33,042評(píng)論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留力图,地道東北人步绸。 一個(gè)月前我還...
    沈念sama閱讀 48,191評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像吃媒,于是被迫代替她去往敵國和親靡努。 傳聞我的和親對(duì)象是個(gè)殘疾皇子坪圾,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,955評(píng)論 2 355

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