三級 NestedScroll 巢狀滾動實踐

walkeer發表於2019-03-03

巢狀滾動介紹

我們知道 NestedScrolling(Parent/Child) 這對介面是用來實現巢狀滾動的,一般實現這對介面的 Parent 和 Child 沒有直接巢狀,否則直接用 onInterceptTouchEvent() 和 onTouchEvent() 這對方法實現就可以了。能夠越級巢狀滾動正是它的厲害之處。

NestedScrolling(Parent/Child) vs NestedScrolling(Parent2/Child2)

巢狀滾動的介面有兩對:NestedScrolling(Parent/Child) 和 NestedScrolling(Parent2/Child2) 後者相比前者對 fling 的處理更加細緻。相比第一代 Child 簡單地將 fling 拋給 Parent,第二代 Child 將 fling 轉化為 scroll 後再分發給 Parent,為了和普通的 scroll 區分增加了一個引數 type, 當 type 是 ViewCompat.TYPE_TOUCH 時表示普通的 scroll,當是 ViewCompat.TYPE_NON_TOUCH 時表示由 fling 轉化而來的 scroll。這樣做的好處是當 Child 檢測到一個 fling 時,它可以選擇將這個 fling 引起的 scroll 一部分作用在 Parent 上一部分作用在自己身上,而不是隻作用在 Parent 或者 Child 上。或許你會問 fling 為什麼不能選擇 Parent 和 Child 都作用,事實上你可以,但 fling 的話 Parent 沒法告訴 Child 消費了多少,剩下多少,因為 fling 傳遞的值是速度,不像 scroll 是距離。所以通過 NestedScrolling(Parent2/Child2) 實現巢狀滾動時,當你觸發了一個 fling 時,也可以做很順滑連貫的交替滾動,而 1 就很難達到相同的效果。現在官方 View 的實現也是通過 NestedScrolling(Parent2/Child2),所以我們在實現自定義的巢狀滾動時儘量用 2。

流程解析

上面簡單介紹了 NestedScrolling 2 和 1 的區別以及為什麼要使用2。現在我們來看看 NestedScrolling(Parent2/Child2) 的方法,1 就不看了,和 2 差不多。

public interface NestedScrollingChild2 {

    void setNestedScrollingEnabled(boolean enabled);

    boolean isNestedScrollingEnabled();

    boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type);
    
    void stopNestedScroll(@NestedScrollType int type);

    boolean hasNestedScrollingParent(@NestedScrollType int type);

    boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow,
            @NestedScrollType int type);
            
	boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
            @Nullable int[] offsetInWindow, @NestedScrollType int type);
}
複製程式碼
public interface NestedScrollingParent2 {

	boolean onStartNestedScroll(@NonNull View child, @NonNull View target, @ScrollAxis int axes,
            @NestedScrollType int type);
            
	void onNestedScrollAccepted(@NonNull View child, @NonNull View target, @ScrollAxis int axes,
            @NestedScrollType int type);
            
	void onStopNestedScroll(@NonNull View target, @NestedScrollType int type);

	void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, @NestedScrollType int type);

	void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed,
            @NestedScrollType int type);
}
複製程式碼

從這兩個介面的方法可以看出這些方法都是一一對應的,比如 startNestedScroll 和 onStartNestedScroll,stopNestedScroll 和 onStopNestedScroll 等。從這些方法的命名上也能看出來巢狀滾動的互動順序是 Child 主動觸發,Parent 被動接受,所以決定是否開啟巢狀滾動的方法 setNestedScrollingEnabled 由 Child 實現,決定開始和結束的方法 startNestedScroll 和 stopNestedScroll 也由 Child 實現。

這裡用一個圖來表示巢狀滾動流程

三級 NestedScroll 巢狀滾動實踐

整個過程大概分為兩部分:繫結和滾動分發。繫結部分可以理解為 Child 向上遍歷找 NestedScrollingParent2 的過程,找到後呼叫它的 onStartNestedScroll 方法,如果返回 true 則說明這個 Parent 想接收 nested scroll,Child 會緊接著調 onNestedScrollAccepted 方法表示同意 Parent 處理自己分發的 nested scroll,對應上圖中的 1 2 3。滾動分發部分 Child 將自己的 scroll 分為三個階段 before scroll after,before 和 after 分發給 parent 消費,scroll 階段讓自己消費,這三個階段是按順序進行的,換句話說如果前一步消耗完了 scroll,那後面的階段就沒有 scroll 可以消費。這樣做的好處是讓 Parent 可以在自己消費之前或者之後消費 scroll,如果 Parent 想在 Child 之前消費就在 onNestedPreScroll 方法裡處理,否則就在 onNestedScroll 方法裡,對應上圖中的 4 5 步。上面介紹到的一些通用邏輯被封裝在 NestedScrollingChildHelper 和 NestedScrollingParentHelper 中,在 NestedScrolling(Parent2/Child2) 的方法中可以呼叫 Helper 類中的同名方法,比如 NestedScrollingChild2.startNestedScroll 方法中實現了向上遍歷尋找 NestedScrollingParent 的邏輯。

三級巢狀滾動

為什麼需要

一個常見的巢狀滾動例子是 CoordinatorLayout/AppbarLayout - RecyclerView, 實現的效果是向上滑動列表時,會先將 AppbarLayout 向上滑動直到完全摺疊,向下滑動至列表最頂部後會展開 AppbarLayout, 如下圖:

三級 NestedScroll 巢狀滾動實踐

這裡實現 NestedScrollingParent2 的是 CoordinatorLayout/AppbarLayout, 實現 NestedScrollingChild2 的是 RecyclerView。對於這種兩級巢狀滾動的需求使用 CoordinatorLayout 幾乎都能實現,如果遇到特殊的業務需求基於 CoordinatorLayout 和 RecyclerView 的實現改改也能實現。

三級 NestedScroll 巢狀滾動實踐

我這裡遇到的需求是即刻首頁的樣式(可參考即刻5.4.1版本),除了要有 AppbarLayout 摺疊效果之外還要在 AppbarLayout 頂部展示搜尋框和重新整理動畫。這裡的滑動邏輯是:

  1. 向上滑動時,最先摺疊重新整理動畫,向下滑動時最後展開重新整理動畫。
  2. 向上滑動列表時先摺疊 AppbarLayout,AppbarLayout 完全摺疊後再摺疊搜尋框。
  3. 向下滑動列表時在展開 AppbarLayout 之前先展開搜尋框。
  4. 列表沒滑動到頂部時可以通過觸發一定速度的向下 fling 來展開搜尋框。

可以發現這裡除了 CoordinatorLayout/AppbarLayout - RecyclerView 這對巢狀滾動的 Parent 和 Child 之外還多了搜尋框和重新整理動畫,而這三者之間的滑動邏輯需要通過巢狀滾動實現,只是傳統的兩級巢狀滾動不能滿足,所以需要實現三級巢狀滾動。

如何實現

所謂三級巢狀滾動是在兩級巢狀滾動之上再新增一個 Parent,這裡為了表述方便將三級巢狀滾動的三級由上到下分別稱為 Grand Parent Child。具體是由兩對 NestedScrolling(Parent2/Child2) 介面實現,Grand 實現第一對介面的 Parent,Parent 實現第一對介面的 Child 和第二對介面的 Parent,Child 實現第二對介面的 Child。與兩級巢狀滾動相比三級巢狀的 Grand 和 Child 和兩級的 Parent 和 Child 區別不大,變化比較大的是三級的 Parent 既要實現兩級的 Parent 介面又要實現 Child 介面,示意圖如下:

三級 NestedScroll 巢狀滾動實踐

在即刻首頁這個例子裡,CoordinatorLayout/AppbarLayout 屬於三級巢狀的 Parent 實現了第二對介面的 NestedScrollingParent2,RecyclerView 屬於 Child 實現了第二對介面的 NestedScrollingChild2。這裡我們需要做的是實現第一對巢狀介面,新建一個自定義 Layout 實現 NestedScrollingParent2 介面作為三級巢狀的 Grand,負責搜尋框和重新整理動畫的摺疊和展開。再新建一個自定義 Layout 繼承 CoordinatorLayout 實現 NestedScrollingChild2 介面,負責攔截列表分發上來的滾動事件或者處理 AppbarLayout 消費後剩下的滾動事件。

流程解析

二級巢狀滾動可以理解為給 Parent 提供了攔截 Child 滾動事件和處理 Child 剩餘滾動事件的能力,具體邏輯可參考本文最開始介紹巢狀滾動的部分。相應的三級巢狀滾動給 Grand 提供了攔截 Parent 和處理剩餘滾動事件的能力,只是攔截和處理的時機多了一些,如下圖:

三級 NestedScroll 巢狀滾動實踐

二級巢狀滾動對滾動處理時機只有三個階段:preScroll、scroll 和 afterScroll。而三級巢狀滾動的處理時機就多一些,有七個階段:prePreScroll、preScroll、afterPreScroll、scroll、preAfterScroll、afterScroll 和 afterAfterScroll,可以看出相比二級巢狀多了 prePreScroll、afterPreScroll、preAfterScroll 和 afterAfterScroll 這四個階段,多出的這幾個階段都是給 Grand 用的。到這裡可以發現 NestedScrollingParent2 其實不能完全描述 Grand 的能力,確實最理想的方案應該是新建一對介面 NestedScrollingGrand2 和 NestedScrollingGrandChild2 來描述新增的四個對滾動事件的處理階段,但考慮到我這裡的例子 Grand 對 Parent 的處理沒有那麼精細化,所以還是通過複用 NestedScrolling(Parent2/Child2) 和一些附加方法來實現。以後如果實現了 NestedScrolling(Grand2/GrandChild2) 介面,也會及時更新。根據上圖即刻首頁滑動的實現思路就很簡單了:

  1. onPrePreScroll 中執行摺疊重新整理動畫的邏輯,onAfterAfterScroll 中執行展開重新整理動畫的邏輯。
  2. onPreScroll 中執行摺疊 AppbarLayout 的邏輯,onAfterPreScroll 中執行搜尋框摺疊的邏輯。
  3. onAfterScroll 中執行展開 AppbarLayout 的邏輯,onPreAfterScroll 中執行搜尋框展開的邏輯。
  4. 列表沒滑到頂部根據 fling 展開搜尋框的邏輯單獨在 Parent 的 onNestedPreFling 裡做,這條算是一個特殊處理。

相關文章