了解使用Android NestedScrolling機(jī)制

其實(shí)NestedScrolling對(duì)于現(xiàn)在的Android開發(fā)已經(jīng)是一個(gè)很常見的交互效果资铡,當(dāng)我們需要實(shí)現(xiàn)一些好看卻又比較復(fù)雜的滑動(dòng)變換時(shí)雹顺,基本上就需要借助NestedScrolling機(jī)制文兑。
先看下一個(gè)比較常見的示例效果


1582250634784676.gif

這套效果是利用官方提供的CoordinatorLayout實(shí)現(xiàn)的,當(dāng)然CoordinatorLayout就是NestedScrolling機(jī)制典型的官方應(yīng)用例子。

一 對(duì)比

這里我會(huì)先介紹下基礎(chǔ)的概念和機(jī)制實(shí)現(xiàn),后續(xù)結(jié)合對(duì)整個(gè)機(jī)制的理解實(shí)現(xiàn)一個(gè)簡(jiǎn)單模仿NestedScrollView的Demo嫡锌,以便更好的加深理解。
demo地址 請(qǐng)點(diǎn)擊. 效果如圖:

1585638235258554.gif

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

傳統(tǒng)事件分發(fā)處理主要涉及到Activity琳钉、ViewGroup和View這三個(gè)主要的類势木。

  • 事件分發(fā)機(jī)制 對(duì)應(yīng)分發(fā)方法是dispatchTouchEvent ,只要事件能夠傳遞到當(dāng)前處理的View歌懒,則此方法一定被調(diào)用啦桌。
  • 事件攔截機(jī)制對(duì)應(yīng)的攔截方法是onInterceptTouchEvent,攔截之后則不往下傳遞及皂。(此方法只有ViewGroup擁有甫男,此方法會(huì)在dispatchTouchEvent 中被調(diào)用。
  • 事件的響應(yīng)機(jī)制對(duì)應(yīng)的是onTouchEvent方法验烧。

對(duì)于傳統(tǒng)的事件從分發(fā)板驳,攔截到處理的一般流程:

  1. 由當(dāng)前的Activity接收系統(tǒng)的Touch事件回調(diào),調(diào)用dispatchTouchEvent開始分發(fā)Touch事件
  2. 上層ViewGroup根據(jù)onInterceptTouchEvent判斷是否要中斷Touch事件
  • 中斷噪窘,則攔截Touch事件笋庄,并會(huì)回調(diào)onTouchEvent效扫,進(jìn)行處理
  • 不中斷倔监,則繼續(xù)調(diào)用其子View的dispatchTouchEvent繼續(xù)分發(fā)Touch事件
  1. 直到有子View消費(fèi)掉了Touch事件,則整個(gè)過程就結(jié)束了

傳統(tǒng)的Touch事件分發(fā)是由上向下的整個(gè)過程類似于一個(gè)單向的水流事件菌仁,中間有一環(huán)將水流攔截浩习,則下游便不會(huì)再有水流經(jīng)過。這樣導(dǎo)致的問題济丘,就是在一個(gè)Touch事件流中谱秽,只能有一個(gè)View或ViewGroup對(duì)當(dāng)前Touch事件做出反應(yīng)。原則上當(dāng)Touch事件被攔截后摹迷,是無(wú)法再次交還給下層子View去處理的(除非手動(dòng)干預(yù)事件的分發(fā))疟赊。
因此我們示例中的滑動(dòng)效果,我們滑動(dòng)的是下面的內(nèi)容區(qū)域View峡碉,但是滾動(dòng)的卻是外部的ViewGroup近哟,那么就必須由上面的ViewGroup攔截Touch事件并進(jìn)行處理。而示例效果確實(shí)上層的ViewGroup滑動(dòng)一定程度后鲫寄,又交換給了下面的子View進(jìn)行滑動(dòng)處理吉执,顯然這種無(wú)間斷順滑交互疯淫,按照傳統(tǒng)的Touch事件的分發(fā)機(jī)制,是很難實(shí)現(xiàn)的戳玫。
由此就引出了我們今天的主角NestedScrolling機(jī)制熙掺。

NestedScrolling機(jī)制

基礎(chǔ)流程大致是這樣的:

  1. 首先需要原有的Touch事件處理先交給子View,當(dāng)然父View可以攔截咕宿,攔截后本次處理便無(wú)法交給子View去處理了
  2. 當(dāng)子View接收到Touch事件時(shí)币绩,會(huì)轉(zhuǎn)換為NestedScrolling事件,也就是dx府阀,dy (后續(xù)統(tǒng)稱為NestedScrolling事件)类浪,并開始發(fā)起NestedScrolling事件分發(fā)。
  3. 子View首先將對(duì)應(yīng)的NestedScrolling事件發(fā)送給父View處理肌似,待父View處理完成后則返回對(duì)應(yīng)的處理和未處理的偏移量
  4. 子View根據(jù)剩余偏移量繼續(xù)處理NestedScrolling事件费就,并再次通知父View處理剩余的偏移量
  5. 父View處理完成,子view則最后發(fā)起NestedScrolling事件終結(jié)川队,父view進(jìn)行收尾工作

從上述的流程中可以輕易的得出NestedScrolling事件力细,是一種由下向上發(fā)起,但是在處理過程中固额,會(huì)不斷詢問上層View的處理眠蚂,整個(gè)過程是給予了上層和下層多個(gè)參與處理的機(jī)會(huì)。
不難得出NestedScroll的機(jī)制本質(zhì)上是給View與View之間提供了一種關(guān)聯(lián)的機(jī)制斗躏,以實(shí)現(xiàn)View之間協(xié)同處理原來的Touch事件逝慧,來解決傳統(tǒng)Touch事件機(jī)制無(wú)法回溯到父View的一錘子買賣的問題。當(dāng)然其中的前提條件是子View需要作為Touch事件的處理者啄糙。

二 實(shí)現(xiàn)

1 主要接口
- NestedScrollingParent
- NestedScrollingChild
最新的支持庫(kù)中笛臣,還有
- NestedScrollingParent2
- NestedScrollingChild2
由于新增的兩個(gè)類原理上不影響對(duì)NestedScrolling機(jī)制的分析,在此就不對(duì)這兩個(gè)類多做描述隧饼。

首先了解一下NestedScrollingParent的接口方法

 boolean onStartNestedScroll(@NonNull View child, @NonNull View target, @ScrollAxis int axes);
 void onNestedScrollAccepted(@NonNull View child, @NonNull View target, @ScrollAxis int axes);
 void onStopNestedScroll(@NonNull View target);
 void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed);
 void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed);
 boolean onNestedFling(@NonNull View target, float velocityX, float velocityY, boolean consumed);
 boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY);
 int getNestedScrollAxes();

首先Parent中的方法基本都是on開頭的響應(yīng)式的方法沈堡,我們這里需要著重關(guān)注這幾個(gè)方法

  • onStartNestedScroll
    對(duì)發(fā)起嵌套滑動(dòng)的子view做出回應(yīng),這個(gè)子View不一定是直接子View燕雁,例如viewPager嵌套的RecycleView诞丽。如果此父View接受嵌套滾動(dòng)操作,則需要返回true拐格。
  • onStopNestedScroll
    對(duì)終止嵌套滑動(dòng)的子view做出回應(yīng)僧免。
  • onNestedPreScroll
    這個(gè)方法會(huì)接收子view中的NestedScrolling事件的滑動(dòng)距離dx,dy捏浊,并交給父View處理懂衩,其中consumed數(shù)組,即是記錄父View處理的dx,dy的消耗勃痴。因此這個(gè)方法是是父View在子view滾動(dòng)之前谒所,進(jìn)行NestedScrolling事件處理的恰當(dāng)時(shí)機(jī)。
  • onNestedScroll
    這個(gè)方法包含了NestedScrolling事件的已消耗和未消耗的滑動(dòng)距離沛申,此方法中可以利用未消耗部分劣领,繼續(xù)對(duì)子View進(jìn)行處理,以達(dá)到不間斷處理整體滾動(dòng)的邏輯铁材。
  • onNestedPreFling
    對(duì)子View的fling事件做出反應(yīng)尖淘,當(dāng)返回true的時(shí)候,這個(gè)fling事件的父View也會(huì)參與處理著觉。同樣也是父View參與處理fling事件的恰當(dāng)時(shí)機(jī)村生。
  • onNestedFling
    這個(gè)方法會(huì)對(duì)子View響應(yīng)fling事件,也是最合適的處理最后的fling收尾工作饼丘。

已經(jīng)了解過處理NestedScrolling事件的核心方法后趁桃,接著了解一下NestedScrollingChild的中發(fā)起和分發(fā)接口的核心方法

void setNestedScrollingEnabled(boolean enabled);
boolean isNestedScrollingEnabled();
boolean startNestedScroll(@ScrollAxis int axes);
void stopNestedScroll();
boolean hasNestedScrollingParent();
boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow);
boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
            @Nullable int[] offsetInWindow);
boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
boolean dispatchNestedPreFling(float velocityX, float velocityY);

NestedScrollingChild中的方法相對(duì)來說簡(jiǎn)單明了,同樣我們分析其中比較重要的幾個(gè)方法

  • startNestedScroll
    選擇在一個(gè)滑動(dòng)方向上開啟嵌套滾動(dòng)肄鸽,對(duì)應(yīng)于NestedScrollingParent接口中的onStartNestedScroll卫病。
  • stopNestedScroll
    終止嵌套滾動(dòng)操作洒宝,對(duì)應(yīng)于NestedScrollingParent接口中的onStopNestedScroll才写。
  • dispatchNestedPreScroll
    視圖移動(dòng)之前分發(fā)滑動(dòng)距離崔步,對(duì)應(yīng)于ViewParent中的onNestedPreScroll
  • dispatchNestedScroll
    分發(fā)ViewParent處理之后的滑動(dòng)距離菊霜,對(duì)應(yīng)于ViewParent中的onNestedScroll

同樣的dispatchNestedFling,dispatchNestedPreFling金抡,分別對(duì)應(yīng)于ViewParent中的onNestedFling匀钧,onNestedPreFling序仙。
以上是我們對(duì)nestedScrolling機(jī)制實(shí)現(xiàn)的一些核心方法梅鹦,這些方法的對(duì)應(yīng)關(guān)系裆甩,以及對(duì)每個(gè)方法的作用都簡(jiǎn)單梳理了一下。

3 示例分析

通過上述NestedScrolling的機(jī)制描述帘瞭,以及具體的實(shí)現(xiàn)方式淑掌,具體方法的含義蒿讥,這些比較抽象和概念型的理論知識(shí)后蝶念,理論總要付諸于實(shí)踐。由此開始我們對(duì)NestedScrolling機(jī)制的應(yīng)用芋绸,簡(jiǎn)單仿寫nestedScrollView的效果媒殉。
這里只放上核心的實(shí)現(xiàn),具體的可以看下demo

  • 3.1 布局文件
<?xml version="1.0" encoding="utf-8"?>
<com.stayli.nested.view.MyNestedParent
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center_horizontal"
    android:orientation="vertical">

   ....

    <com.stayli.nested.view.MyNestedChild
        android:id="@+id/mnc"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="5dp"
        android:orientation="vertical">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:text="底部可滾動(dòng)滾動(dòng)組件"
            android:textColor="#f0f"
            android:textSize="20sp" />
       ...
    </com.stayli.nested.view.MyNestedChild>
</com.stayli.nested.view.MyNestedParent>

  • 3.2 接口實(shí)現(xiàn)
    首先NestedParent 的實(shí)現(xiàn)
public class MyNestedParent extends LinearLayout implements NestedScrollingParent {

    private MyNestedChild mNestedScrollChild;
     // 這里我們就需要使用輔助類
    private NestedScrollingParentHelper mNestedScrollingParentHelper;
    private int mImgHeight;

    public MyNestedParent(Context context) {
        super(context);
    }

    public MyNestedParent(Context context, AttributeSet attrs) {
        super(context, attrs);
        mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);
    } 
 ...
    /**
     * @param child  直接子view
     * @param target 目標(biāo)View
     * @param axes   滑動(dòng)方向
     * @return 判斷參數(shù)target是哪一個(gè)子view以及滾動(dòng)的方向摔敛,然后決定是否要配合其進(jìn)行嵌套滾動(dòng)
     */
    @Override
    public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes) {
        return target instanceof MyNestedChild && (axes & ViewCompat.SCROLL_AXIS_HORIZONTAL) != 0;
    }


    @Override
    public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int nestedScrollAxes) {
        mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
    }

    @Override
    public void onStopNestedScroll(@NonNull View target) {
        mNestedScrollingParentHelper.onStopNestedScroll(target);
    }

    /**
     * 優(yōu)先與child 滾動(dòng)前調(diào)用
     *
     * @param target   目標(biāo)View
     * @param dx       x軸偏移
     * @param dy       y軸偏移
     * @param consumed 消費(fèi)量 輸出
     */
    @Override
    public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed) {
        // 無(wú)論是顯示 還是 隱藏圖片廷蓉,其實(shí)都是在子child 滾動(dòng)之前就行移動(dòng)
        if (showImg(dy) || hideImg(dy)) {
            scrollBy(0, -dy);//滾動(dòng)
            consumed[1] = dy;//記錄消費(fèi)的偏移量
        }
    }

    //后于child滾動(dòng)
    @Override
    public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {

    }
}

NestedChild 實(shí)現(xiàn)

public class MyNestedChild extends LinearLayout implements NestedScrollingChild {
    private NestedScrollingChildHelper mNestedScrollingChildHelper;
    private final int[] offset = new int[2]; //偏移量
    private final int[] consumed = new int[2]; //偏移 消費(fèi)
    private int lastY;
    private int showHeight;
    public MyNestedChild(Context context) {
        super(context);
    }
    public MyNestedChild(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    //初始化helper對(duì)象
    private NestedScrollingChildHelper getScrollingChildHelper() {
        if (mNestedScrollingChildHelper == null) {
            mNestedScrollingChildHelper = new NestedScrollingChildHelper(this);
            mNestedScrollingChildHelper.setNestedScrollingEnabled(true);
        }
        return mNestedScrollingChildHelper;
    }
      ...
    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            //按下
            case MotionEvent.ACTION_DOWN:
                lastY = (int) event.getRawY();
                break;
            //移動(dòng)
            case MotionEvent.ACTION_MOVE:
                int y = (int) (event.getRawY());
                int dy = y - lastY;
                lastY = y;
                if (startNestedScroll(ViewCompat.SCROLL_AXIS_HORIZONTAL)
                        && dispatchNestedPreScroll(0, dy, consumed, offset)) //如果找到了支持嵌套滑動(dòng)的父類,父類進(jìn)行了一系列的滑動(dòng)
                {
                    //獲取滑動(dòng)距離
                    int remain = dy - consumed[1];
                    if (remain != 0) {
                        scrollBy(0, -remain);
                    }

                } else {
                    scrollBy(0, -dy);
                }
                break;
        }

        return true;
    }
     ...
    //實(shí)現(xiàn)一下接口
    @Override
    public void setNestedScrollingEnabled(boolean enabled) {
        getScrollingChildHelper().setNestedScrollingEnabled(enabled);
    }
 ....
    @Override
    public boolean startNestedScroll(int axes) {
        return getScrollingChildHelper().startNestedScroll(axes);
    }

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

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

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

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

  • 3.3 關(guān)鍵流程
    核心的實(shí)現(xiàn)就是對(duì)兩個(gè)接口類的實(shí)現(xiàn),我們通過demo中的日志信息來梳理一下這些關(guān)鍵方法的執(zhí)行過程。
  • 3.3.1. 首先我們先把向上滑動(dòng)過程中的日志信息截取出來
MyNestedChild: onTouchEvent: ACTION_DOWN ---> 事件
MyNestedChild: startNestedScroll:  ---> 發(fā)起嵌套滾動(dòng)
MyNestedParent: onStartNestedScroll: ----> 接收Start嵌套滾動(dòng)
MyNestedParent: onNestedScrollAccepted: -------> 接收Accepted嵌套滾動(dòng)
MyNestedChild: onTouchEvent: ACTION_MOVE ---> 事件 轉(zhuǎn)換為 dy -2
MyNestedChild: dispatchNestedPreScroll:  ---> 分發(fā)Pre嵌套滾動(dòng)
MyNestedParent: onNestedPreScroll: 接收Pre嵌套滾動(dòng)
MyNestedChild: dispatchNestedScroll:  ---> 分發(fā)嵌套滾動(dòng)
MyNestedParent: onNestedScroll: 接收嵌套滾動(dòng)
MyNestedChild: stopNestedScroll:  ---> 終止嵌套滾動(dòng)
MyNestedParent: onStopNestedScroll: -------> 終止嵌套滾動(dòng)

基礎(chǔ)流程中我們說了需要將Touch事件處理先交給子View處理桃犬,并且轉(zhuǎn)換為NestedScrolling所處理的事件刹悴,也就是dx,dy攒暇。
通過對(duì)demo中的日志分析我們不難得出這些關(guān)鍵方法的執(zhí)行過程土匀,也是NestedScrolling機(jī)制的核心工作流程,這里我們輕易的就能看出形用,方法之間的執(zhí)行關(guān)系就轧。
表格如下:


AF4B53C1-11AB-4128-9778-AB3C58499867.png

由此我們可以得到這些核心方法的執(zhí)行過程:

  1. child的startNestedScroll()來發(fā)起嵌套滑動(dòng)流程。parent的onStartNestedScroll()得到響應(yīng)田度,若返回true妒御,便是真正開啟嵌套滑動(dòng),此時(shí)OnNestScrollAccepted會(huì)被調(diào)用镇饺。
  2. child滾動(dòng)前乎莉,會(huì)先調(diào)用dispatchNestedPreScroll 進(jìn)行滑動(dòng)距離的分發(fā), 此時(shí)parent的OnNestedPreScroll()得到響應(yīng)奸笤,開啟parent對(duì)滑動(dòng)事件的處理梦鉴,便可以在此優(yōu)先處理滾動(dòng)。
  3. child 繼續(xù)調(diào)用 dispatchNestedScroll()揭保,parent的OnNestedScroll()得到響應(yīng)肥橙,開啟對(duì)子View的滾動(dòng)處理。
  4. 最后child 調(diào)用stopNestedScroll 發(fā)起終結(jié)操作秸侣,parent的onStopNestedScroll()做收尾工作存筏。

4 核心方法時(shí)序

這里網(wǎng)上有一張嵌套滑動(dòng)開始到結(jié)束的方法調(diào)用時(shí)序圖:
可以更好理解整個(gè)NestedScrol事件的分發(fā)與處理。


1941624-70a8060bdf591463.png
金色是NestedScrollingChild的方法 , 為View主動(dòng)調(diào)用味榛。
紫色是NestedScrollingParent回調(diào)的方法 , 由View的相關(guān)方法調(diào)用椭坚。
橙色是滾動(dòng)事件被消費(fèi)的時(shí)機(jī)

看完這個(gè)圖之后,包括已經(jīng)了解到理論知識(shí)以及對(duì)整個(gè)NestedScroll事件分發(fā)的過程搏色,大家也都有了一定的理解了善茎。接下來就對(duì)Android源碼中對(duì)此機(jī)制的實(shí)現(xiàn)來進(jìn)行更深入的分析以此繼續(xù)加深我們的理解。

5 源碼實(shí)現(xiàn)分析

上面的時(shí)序圖频轿,是三層嵌套的時(shí)序垂涯,這里為了使大家更快的弄懂嵌套滑動(dòng)的機(jī)制,就以簡(jiǎn)單的兩層嵌套作為實(shí)例進(jìn)行分析航邢。

這里通過對(duì)NestedScrollParent -> RecyclerView這個(gè)常用的嵌套滑動(dòng)實(shí)例進(jìn)行分析 , 以便深入理解NestedScrolling事件傳遞的機(jī)制耕赘。
簡(jiǎn)單看下xml文件

<?xml version="1.0" encoding="utf-8"?>
<com.stayli.nested.view.MyNestedParent
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <ImageView
        android:id="@+id/cat_header"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:src="@drawable/cat" />

    <android.support.v7.widget.RecyclerView
        android:id="@+id/rv_nested"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</com.stayli.nested.view.MyNestedParent>

布局文件只是簡(jiǎn)單的嵌套,NestedScrollParent繼承Linearlayout膳殷,并實(shí)現(xiàn)NestedScrollingParent接口操骡。
我們分析的這些API到底如何工作的呢,首先我們定義了一個(gè)支持嵌套的父View,NestedScrollParent和一個(gè)已經(jīng)在系統(tǒng)的支持嵌套的子View册招,RecyclerView岔激。
這里我們將 NestedScrollParent(NSP)是父級(jí),RecyclerView(RV)是子級(jí)是掰。
當(dāng)我們滾動(dòng)RV中的內(nèi)容時(shí)鹦倚,如果沒有嵌套滾動(dòng),RV將立即消耗掉scroll事件冀惭,這樣頂部的圖片震叙,就無(wú)法正確的移動(dòng)。而我們真正想要的是上下的兩個(gè)View像一個(gè)整體來進(jìn)行滾動(dòng)散休∶铰ィ或者可以更明確的說:

  • 如果RV內(nèi)容向上滾動(dòng),則RV向上滾動(dòng)的行為應(yīng)該是NSP整體向上滾動(dòng)
  • 如果RV內(nèi)容向下滾動(dòng)戚丸,滾動(dòng)RV下降應(yīng)引起NSP整體向下滾動(dòng)

而我們實(shí)現(xiàn)這個(gè)效果的基礎(chǔ)划址,便是嵌套滑動(dòng)機(jī)制所提供的View與View之間的相互貫穿滾動(dòng)的關(guān)聯(lián)。
大家在具體翻閱源碼的時(shí)候會(huì)發(fā)現(xiàn)

  • NestedScrollingParentHelper
  • NestedScrollingChildHelper
    這兩個(gè)輔助類內(nèi)部已經(jīng)將核心的方法都封裝好了限府,只需要使用對(duì)應(yīng)的調(diào)用方法即可夺颤,后面源碼閱讀時(shí),不對(duì)這兩個(gè)類做說明胁勺。

對(duì)于整個(gè)事件的分發(fā)啊啟動(dòng)世澜,我們從發(fā)起者開始分析:

  1. RVonTouchEvent(ACTION_DOWN)方法被調(diào)用。
 @Override
    public boolean onTouchEvent(MotionEvent e) {
               ...
        final int action = MotionEventCompat.getActionMasked(e);
        switch (action) {
            case MotionEvent.ACTION_DOWN: {
                ...
                startNestedScroll(nestedScrollAxis);
            } 
            break;

        return true;
  1. RV調(diào)用其自己的dispatchNestedPreScroll()方法署穗,該方法通知NSP的onNestedPreScroll() 中處理即將消耗的滑動(dòng)距離寥裂。
 @Override
    public boolean onTouchEvent(MotionEvent e) {
       
        final int action = MotionEventCompat.getActionMasked(e);

        switch (action) {

            case MotionEvent.ACTION_MOVE: {
                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;
                if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
                    dx -= mScrollConsumed[0];
                    dy -= mScrollConsumed[1];
                    vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
                    // Updated the nested offsets
                    mNestedOffsets[0] += mScrollOffset[0];
                    mNestedOffsets[1] += mScrollOffset[1];
                }
          break;
        }
        return true;
  1. 對(duì)應(yīng)的NSPonNestedPreScroll()方法被調(diào)用,給NSP一個(gè)機(jī)會(huì)案疲,在RV之前對(duì)滾動(dòng)事件作出反應(yīng)并通知RV已經(jīng)消耗的距離封恰。
    //先于child滾動(dòng)
    //前3個(gè)為輸入?yún)?shù),最后一個(gè)是輸出參數(shù)
    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        if (showImg(dy) || hideImg(dy)) {//如果需要顯示或隱藏圖片褐啡,即需要自己(parent)滾動(dòng)
            scrollBy(0, -dy);//滾動(dòng)
            consumed[1] = dy;//告訴child我消費(fèi)了多少
        }
    }
  1. RV消耗滾動(dòng)的剩余部分诺舔。
@Override
    public boolean onTouchEvent(MotionEvent e) {
       
        final int action = MotionEventCompat.getActionMasked(e);
     
            case MotionEvent.ACTION_MOVE: {

                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;

                if (mScrollState == SCROLL_STATE_DRAGGING) {
                    mLastTouchX = x - mScrollOffset[0];
                    mLastTouchY = y - mScrollOffset[1];

                    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;

        return true;
    }
  1. RV調(diào)用其自己的dispatchNestedScroll()方法,該方法通知NSP已經(jīng)消耗的滑動(dòng)距離的一部分备畦。
 boolean scrollByInternal(int x, int y, MotionEvent ev) {
        int unconsumedX = 0, unconsumedY = 0;
        int consumedX = 0, consumedY = 0;

        if (mAdapter != null) {
          if (y != 0) {
                consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
                unconsumedY = y - consumedY;
            }

        }

        if (dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset)) { // 繼續(xù)回溯通知父View處理未消耗的距離
            mLastTouchX -= mScrollOffset[0];
            mLastTouchY -= mScrollOffset[1];
            if (ev != null) {
                ev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
            }
            mNestedOffsets[0] += mScrollOffset[0];
            mNestedOffsets[1] += mScrollOffset[1];
        } 
        if (consumedX != 0 || consumedY != 0) {
            dispatchOnScrolled(consumedX, consumedY);
        }
        if (!awakenScrollBars()) {
            invalidate();
        }
        return consumedX != 0 || consumedY != 0;
    }
  1. 對(duì)應(yīng)的NSPonNestedScroll()方法被調(diào)用低飒,再次給NSP一個(gè)機(jī)會(huì),消耗的是仍然沒有被消耗掉剩余的滾動(dòng)距離萍恕。
 //后于child滾動(dòng)
    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {

    }
  1. 最后在RVonTouchEvent(ACTION_UP)消耗掉整個(gè)事件逸嘀。
  @Override
    public boolean onTouchEvent(MotionEvent e) {
        final int action = MotionEventCompat.getActionMasked(e);
            case MotionEvent.ACTION_UP: {
                mVelocityTracker.addMovement(vtev);
                eventAddedToVelocityTracker = true;
                mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
                final float xvel = canScrollHorizontally ?
                        -VelocityTrackerCompat.getXVelocity(mVelocityTracker, mScrollPointerId) : 0;
                final float yvel = canScrollVertically ?
                        -VelocityTrackerCompat.getYVelocity(mVelocityTracker, mScrollPointerId) : 0;
                if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
                    setScrollState(SCROLL_STATE_IDLE);
                }
                resetTouch(); // 處理最后
            } break;
        }
        return true;
    }

    private void resetTouch() {
        if (mVelocityTracker != null) {
            mVelocityTracker.clear();
        }
        stopNestedScroll();
        releaseGlows();
    }
  1. 對(duì)應(yīng)的NSPonStopNestedScroll()方法被調(diào)用。
 @Override
    public void onStopNestedScroll(View target) { // 進(jìn)行最后的收尾工作
        mNestedScrollingParentHelper.onStopNestedScroll(target); 
    }

嵌套Fling的處理方式與嵌套Scroll非常相似允粤。子View檢測(cè)到其onTouchEvent(ACTION_UP)方法發(fā)生變化,并通過調(diào)用其自身的dispatchNestedPreFling()和dispatchNestedFling()方法通知父級(jí)。觸發(fā)對(duì)父對(duì)象onNestedPreFling()和onNestedFling()方法的調(diào)用类垫,并使父對(duì)象有機(jī)會(huì)在孩子使用它之前和之后對(duì)Fling事件做出反應(yīng)司光。

NestedScrolling機(jī)制由實(shí)現(xiàn)了NestedScrollingChild接口的子View觸發(fā) , 所以事實(shí)上 , 當(dāng)子View實(shí)現(xiàn)了NestedScrollingChild接口時(shí) , 默認(rèn)會(huì)使用NestedScrolling機(jī)制分發(fā)事件給實(shí)現(xiàn)了NestedScrollingParent父View。要理解NestedScrolling , 實(shí)際上就是要理解NestedScrolling事件分發(fā)過程悉患。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
禁止轉(zhuǎn)載残家,如需轉(zhuǎn)載請(qǐng)通過簡(jiǎn)信或評(píng)論聯(lián)系作者。
  • 序言:七十年代末售躁,一起剝皮案震驚了整個(gè)濱河市坞淮,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌陪捷,老刑警劉巖回窘,帶你破解...
    沈念sama閱讀 222,590評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異市袖,居然都是意外死亡啡直,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,157評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門苍碟,熙熙樓的掌柜王于貴愁眉苦臉地迎上來酒觅,“玉大人,你說我怎么就攤上這事微峰∠系ぃ” “怎么了?”我有些...
    開封第一講書人閱讀 169,301評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵蜓肆,是天一觀的道長(zhǎng)掂榔。 經(jīng)常有香客問我,道長(zhǎng)症杏,這世上最難降的妖魔是什么装获? 我笑而不...
    開封第一講書人閱讀 60,078評(píng)論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮厉颤,結(jié)果婚禮上穴豫,老公的妹妹穿的比我還像新娘。我一直安慰自己逼友,他們只是感情好精肃,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,082評(píng)論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著帜乞,像睡著了一般司抱。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上黎烈,一...
    開封第一講書人閱讀 52,682評(píng)論 1 312
  • 那天习柠,我揣著相機(jī)與錄音匀谣,去河邊找鬼。 笑死资溃,一個(gè)胖子當(dāng)著我的面吹牛武翎,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播溶锭,決...
    沈念sama閱讀 41,155評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼宝恶,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了趴捅?” 一聲冷哼從身側(cè)響起垫毙,我...
    開封第一講書人閱讀 40,098評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎拱绑,沒想到半個(gè)月后综芥,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,638評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡欺栗,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,701評(píng)論 3 342
  • 正文 我和宋清朗相戀三年毫痕,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片迟几。...
    茶點(diǎn)故事閱讀 40,852評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡消请,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出类腮,到底是詐尸還是另有隱情臊泰,我是刑警寧澤,帶...
    沈念sama閱讀 36,520評(píng)論 5 351
  • 正文 年R本政府宣布蚜枢,位于F島的核電站缸逃,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏厂抽。R本人自食惡果不足惜需频,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,181評(píng)論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望筷凤。 院中可真熱鬧昭殉,春花似錦、人聲如沸藐守。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,674評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)卢厂。三九已至乾蓬,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間慎恒,已是汗流浹背任内。 一陣腳步聲響...
    開封第一講書人閱讀 33,788評(píng)論 1 274
  • 我被黑心中介騙來泰國(guó)打工撵渡, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人族奢。 一個(gè)月前我還...
    沈念sama閱讀 49,279評(píng)論 3 379
  • 正文 我出身青樓姥闭,卻偏偏與公主長(zhǎng)得像丹鸿,于是被迫代替她去往敵國(guó)和親越走。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,851評(píng)論 2 361

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