咱們知道 NestedScrolling(Parent/Child) 這對接口是用來實現嵌套滾動的,通常實現這對接口的 Parent 和 Child 沒有直接嵌套,不然直接用 onInterceptTouchEvent() 和 onTouchEvent() 這對方法實現就能夠了。可以越級嵌套滾動正是它的厲害之處。java
嵌套滾動的接口有兩對: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 差很少。spa
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 實現。code
這裏用一個圖來表示嵌套滾動流程cdn
整個過程大概分爲兩部分:綁定和滾動分發。綁定部分能夠理解爲 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 的邏輯。blog
一個常見的嵌套滾動例子是 CoordinatorLayout/AppbarLayout - RecyclerView, 實現的效果是向上滑動列表時,會先將 AppbarLayout 向上滑動直到徹底摺疊,向下滑動至列表最頂部後會展開 AppbarLayout, 以下圖:繼承
這裏實現 NestedScrollingParent2 的是 CoordinatorLayout/AppbarLayout, 實現 NestedScrollingChild2 的是 RecyclerView。對於這種兩級嵌套滾動的需求使用 CoordinatorLayout 幾乎都能實現,若是遇到特殊的業務需求基於 CoordinatorLayout 和 RecyclerView 的實現改改也能實現。接口
我這裏遇到的需求是即刻首頁的樣式(可參考即刻5.4.1版本),除了要有 AppbarLayout 摺疊效果以外還要在 AppbarLayout 頂部展現搜索框和刷新動畫。這裏的滑動邏輯是:事件
能夠發現這裏除了 CoordinatorLayout/AppbarLayout - RecyclerView 這對嵌套滾動的 Parent 和 Child 以外還多了搜索框和刷新動畫,而這三者之間的滑動邏輯須要經過嵌套滾動實現,只是傳統的兩級嵌套滾動不能知足,因此須要實現三級嵌套滾動。get
所謂三級嵌套滾動是在兩級嵌套滾動之上再添加一個 Parent,這裏爲了表述方便將三級嵌套滾動的三級由上到下分別稱爲 Grand Parent Child。具體是由兩對 NestedScrolling(Parent2/Child2) 接口實現,Grand 實現第一對接口的 Parent,Parent 實現第一對接口的 Child 和第二對接口的 Parent,Child 實現第二對接口的 Child。與兩級嵌套滾動相比三級嵌套的 Grand 和 Child 和兩級的 Parent 和 Child 區別不大,變化比較大的是三級的 Parent 既要實現兩級的 Parent 接口又要實現 Child 接口,示意圖以下:
在即刻首頁這個例子裏,CoordinatorLayout/AppbarLayout 屬於三級嵌套的 Parent 實現了第二對接口的 NestedScrollingParent2,RecyclerView 屬於 Child 實現了第二對接口的 NestedScrollingChild2。這裏咱們須要作的是實現第一對嵌套接口,新建一個自定義 Layout 實現 NestedScrollingParent2 接口做爲三級嵌套的 Grand,負責搜索框和刷新動畫的摺疊和展開。再新建一個自定義 Layout 繼承 CoordinatorLayout 實現 NestedScrollingChild2 接口,負責攔截列表分發上來的滾動事件或者處理 AppbarLayout 消費後剩下的滾動事件。
二級嵌套滾動能夠理解爲給 Parent 提供了攔截 Child 滾動事件和處理 Child 剩餘滾動事件的能力,具體邏輯可參考本文最開始介紹嵌套滾動的部分。相應的三級嵌套滾動給 Grand 提供了攔截 Parent 和處理剩餘滾動事件的能力,只是攔截和處理的時機多了一些,以下圖:
二級嵌套滾動對滾動處理時機只有三個階段: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) 接口,也會及時更新。根據上圖即刻首頁滑動的實現思路就很簡單了: