問題分析
嵌套滑動(dòng)一直是Android中比較棘手的問題, 根本原因是Android的事件分發(fā)機(jī)制導(dǎo)致的.不了解事件分發(fā)機(jī)制的同學(xué)可以先看看一點(diǎn)見解: Android事件分發(fā)機(jī)制, 導(dǎo)致嵌套滑動(dòng)難處理的關(guān)鍵原因在于當(dāng)子控件消費(fèi)了事件, 那么父控件就不會(huì)再有機(jī)會(huì)處理這個(gè)事件了, 所以一旦內(nèi)部的滑動(dòng)控件消費(fèi)了滑動(dòng)操作, 外部的滑動(dòng)控件就再也沒機(jī)會(huì)響應(yīng)這個(gè)滑動(dòng)操作了.
嵌套滑動(dòng)
不過這個(gè)問題終于在LOLLIPOP(SDK21)之后終于有了官方的解決方法, 就是嵌套滑動(dòng)機(jī)制. 在分析具體的代碼邏輯之前, 下面先簡單介紹下嵌套滑動(dòng)的一些基本知識(shí).
嵌套滑動(dòng)機(jī)制可以理解為一個(gè)約定, 原生的支持嵌套滑動(dòng)的控件都是依據(jù)這個(gè)約定來實(shí)現(xiàn)嵌套滑動(dòng)的, 例如CoordinatorLayout, 所以如果你自定義的控件也遵守這個(gè)約定, 那么就可以跟原生的控件進(jìn)行嵌套滑動(dòng)了.
基本原理
嵌套滑動(dòng)的基本原理是在子控件接收到滑動(dòng)一段距離的請求時(shí), 先詢問父控件是否要滑動(dòng), 如果滑動(dòng)了父控件就通知子控件它消耗了一部分滑動(dòng)距離, 子控件就只處理剩下的滑動(dòng)距離, 然后子控件滑動(dòng)完畢后再把剩余的滑動(dòng)距離傳給父控件.
通過這樣的嵌套滑動(dòng)機(jī)制, 在一次滑動(dòng)操作過程中
父控件和子控件都有機(jī)會(huì)對滑動(dòng)操作作出響應(yīng), 尤其父控件能夠分別在子控件處理滑動(dòng)距離之前和之后對滑動(dòng)距離進(jìn)行響應(yīng).
這解決了事件分發(fā)機(jī)制缺點(diǎn)引起的問題.
版本之別
在看具體的代碼之前先說下嵌套滑動(dòng)相關(guān)方法的一些我認(rèn)為值得注意的地方.
LOLLIPOP(SDK21)之后
為什么說這個(gè)是官方的解決方法? 因?yàn)?/p>
嵌套滑動(dòng)的相關(guān)邏輯作為普通方法直接寫進(jìn)了最新的(SDK21之后)
View
和ViewGroup
類.
普通方法是指這個(gè)方法不是繼承自接口或者其他類, 例如[View#dispatchNestedScroll](https://developer.android.com/reference/android/view/View.html#dispatchNestedScroll(int, int, int, int, int[])), 可以看到官方標(biāo)注了Added in API level 21
標(biāo)示, 也就是說這是在SDK21版本之后添加進(jìn)去的一個(gè)普通方法.
向前兼容
而SDK21之前的版本
官方在
android.support.v4
兼容包中提供了兩個(gè)接口NestedScrollingChild
和NestedScrollingParent
, 還有兩個(gè)輔助類NestedScrollingChildHelper
和NestedScrollingParentHelper
來幫助控件實(shí)現(xiàn)嵌套滑動(dòng).
這個(gè)兼容的原理很簡單
兩個(gè)接口
NestedScrollingChild
和NestedScrollingParent
分別定義上面提到的View
和ViewParent
新增的普通方法
在嵌套滑動(dòng)中會(huì)要求控件要么是繼承于SDK21之后的View
或ViewGroup
, 要么實(shí)現(xiàn)了這兩個(gè)接口, 這是控件能夠進(jìn)行嵌套滑動(dòng)的前提條件.
那么怎么知道調(diào)用的方法是控件自有的方法, 還是接口的方法? 在代碼中是通過ViewCompat
和ViewParentCompat
類來實(shí)現(xiàn).
ViewCompat
和ViewParentCompat
通過當(dāng)前的Build.VERSION.SDK_INT
來判斷當(dāng)前版本, 然后選擇不同的實(shí)現(xiàn)類, 這樣就可以根據(jù)版本選擇調(diào)用的方法.
例如如果版本是SDK21之前, 那么就會(huì)判斷控件是否實(shí)現(xiàn)了接口, 然后調(diào)用接口的方法, 如果是SDK21之后, 那么就可以直接調(diào)用對應(yīng)的方法.
輔助類
除了接口兼容包還提供了NestedScrollingChildHelper
和NestedScrollingParentHelper
兩個(gè)輔助類, 這兩個(gè)輔助類實(shí)際上就是對應(yīng)View
和ViewParent
中新增的普通方法, 代碼就不貼了, 簡單對比下就可以發(fā)現(xiàn), 對應(yīng)方法實(shí)現(xiàn)的邏輯基本一樣, 所以
只要在接口方法內(nèi)對應(yīng)調(diào)用輔助類的方法就可以兼容嵌套滑動(dòng)了.
例如在NestedScrollingChild#startNestedScroll
方法中調(diào)用NestedScrollingChildHelper#startNestedScroll
.
題外話: 這里實(shí)際用了代理模式來讓SDK21之前的控件具有了新增的方法.
默認(rèn)處理邏輯
雖然View
和ViewGroup
(SDK21之后)本身就具有嵌套滑動(dòng)的相關(guān)方法, 但是默認(rèn)情況是是不會(huì)被調(diào)用, 因?yàn)?code>View和ViewGroup
本身不支持滑動(dòng), 所以
本身不支持滑動(dòng)的控件即使有嵌套滑動(dòng)的相關(guān)方法也不能進(jìn)行嵌套滑動(dòng).
上面已經(jīng)說到要讓控件支持嵌套滑動(dòng)
- 首先要控件類具有嵌套滑動(dòng)的相關(guān)方法, 要么僅支持SDK21之后版本, 要么實(shí)現(xiàn)對應(yīng)的接口, 為了兼容低版本, 更常用到的是后者.
- 因?yàn)槟J(rèn)的情況是不會(huì)支持滑動(dòng)的, 所以控件要在合適的位置主動(dòng)調(diào)起嵌套滑動(dòng)的方法.
接下來通過分析相對簡單的支持嵌套滑動(dòng)的容器NestedScrollView
來了解下怎樣主動(dòng)調(diào)起嵌套滑動(dòng)的方法, 以及嵌套滑動(dòng)的具體邏輯.
相關(guān)方法
先簡單看看相關(guān)方法的作用, 更具體的說明建議看源碼注釋中的方法說明.
注意 : 下文分析用內(nèi)控件表示兩層嵌套中的子控件, 外控件表示嵌套中的父控件.**
NestedScrollingChild
startNestedScroll
: 起始方法, 主要作用是找到接收滑動(dòng)距離信息的外控件.
dispatchNestedPreScroll
: 在內(nèi)控件處理滑動(dòng)前把滑動(dòng)信息分發(fā)給外控件.
dispatchNestedScroll
: 在內(nèi)控件處理完滑動(dòng)后把剩下的滑動(dòng)距離信息分發(fā)給外控件.
stopNestedScroll
: 結(jié)束方法, 主要作用就是清空嵌套滑動(dòng)的相關(guān)狀態(tài)
setNestedScrollingEnabled
和isNestedScrollingEnabled
: 一對get&set方法, 用來判斷控件是否支持嵌套滑動(dòng).
dispatchNestedPreFling
和dispatchNestedFling
: 跟Scroll的對應(yīng)方法作用類似, 不過分發(fā)的不是滑動(dòng)信息而是Fling信息.(這個(gè)Fling好難翻譯.. =挡爵。=)本文主要關(guān)注滑動(dòng)的處理, 所以后續(xù)不分析這兩個(gè)方法.
從方法名就可以看出
內(nèi)控件是嵌套滑動(dòng)的發(fā)起者.
NestedScrollingParent
因?yàn)閮?nèi)控件是發(fā)起者, 所以外控件的大部分方法都是被內(nèi)控件的對應(yīng)方法回調(diào)的.
onStartNestedScroll
: 對應(yīng)startNestedScroll
, 內(nèi)控件通過調(diào)用外控件的這個(gè)方法來確定外控件是否接收滑動(dòng)信息.
onNestedScrollAccepted
: 當(dāng)外控件確定接收滑動(dòng)信息后該方法被回調(diào), 可以讓外控件針對嵌套滑動(dòng)做一些前期工作.
onNestedPreScroll
: 關(guān)鍵方法, 接收內(nèi)控件處理滑動(dòng)前的滑動(dòng)距離信息, 在這里外控件可以優(yōu)先響應(yīng)滑動(dòng)操作, 消耗部分或者全部滑動(dòng)距離.
onNestedScroll
: 關(guān)鍵方法, 接收內(nèi)控件處理完滑動(dòng)后的滑動(dòng)距離信息, 在這里外控件可以選擇是否處理剩余的滑動(dòng)距離.
onStopNestedScroll
: 對應(yīng)stopNestedScroll
, 用來做一些收尾工作.
getNestedScrollAxes
: 返回嵌套滑動(dòng)的方向, 區(qū)分橫向滑動(dòng)和豎向滑動(dòng), 作用不大
onNestedPreFling
和onNestedFling
: 同上略
外控件通過
onNestedPreScroll
和onNestedScroll
來接收內(nèi)控件響應(yīng)滑動(dòng)前后的滑動(dòng)距離信息.
再次指出, 這兩個(gè)方法是實(shí)現(xiàn)嵌套滑動(dòng)效果的關(guān)鍵方法.
從NestedScrollView看嵌套機(jī)制
說完上面一大通, 終于可以開始分析源碼來了解嵌套滑動(dòng)機(jī)制起作用的具體邏輯了.
NestedScrollView
簡單地說就是支持嵌套滑動(dòng)的ScrollView
, 內(nèi)部邏輯簡單, 而且它既可以是內(nèi)控件, 也可以是外控件, 所以選擇分析它來了解嵌套滑動(dòng)機(jī)制.
注意 : 因?yàn)?code>NestedScrollingChildHelper和NestedScrollingParent
這兩個(gè)輔助類的實(shí)現(xiàn)跟View
和ViewGroup
中的對應(yīng)方法是一樣的, 而且View
和ViewGroup
的源碼沒有使用兼容類, 所以下面分析相關(guān)方法的時(shí)候源碼都使用View
和ViewGroup
中的代碼.
上面已經(jīng)說了嵌套滑動(dòng)是從startNestedScroll
開始, 所以先看看哪里調(diào)用了這個(gè)方法, 在源碼里一搜就能知道有兩個(gè)地方調(diào)用了這個(gè)方法.
-
onInterceptTouchEvent
中ACTION_DOWN
的情況 -
onTouchEvent
中ACTION_DOWN
的情況
因?yàn)?code>ACTION_DOWN是滑動(dòng)操作的開始事件, 所以當(dāng)接收到這個(gè)事件的時(shí)候嘗試找對應(yīng)的外控件. 只有找到了外控件才有后續(xù)的嵌套滑動(dòng)的邏輯發(fā)生.
關(guān)于NestedScrollView
在這里的實(shí)現(xiàn)其實(shí)有個(gè)奇怪的地方, 提出一個(gè)問題, 不感興趣的可以直接跳過這段.
- 既然內(nèi)控件是發(fā)起者, 為什么要在
onInterceptTouchEvent
也調(diào)用startNestedScroll
呢?
因?yàn)槭录鬟f的時(shí)候會(huì)先執(zhí)行外控件的onInterceptTouchEvent
, 也就是說第一個(gè)執(zhí)行startNestedScroll
的是最外層的NestedScrollView
, 即使它找到了對應(yīng)的外控件后續(xù)如果有子控件消費(fèi)了這個(gè)事件, 也就是說不執(zhí)行onTouchEvent
方法, 那么找到外控件也沒用的, 不清楚設(shè)計(jì)者的意圖.
接著我們看startNestedScroll
是如何找對應(yīng)的外控件的, 因?yàn)?code>NestedScrollView#startNestedScroll調(diào)用了輔助方法的startNestedScroll
, 所以下面直接貼View#startNestedScroll
.
// View.javapublic
boolean startNestedScroll(int axes) {
// ...
if (isNestedScrollingEnabled()) {
ViewParent p = getParent();
View child = this;
while (p != null) {
try {
// 關(guān)鍵代碼
if (p.onStartNestedScroll(child, this, axes)) {
mNestedScrollingParent = p;
p.onNestedScrollAccepted(child, this, axes);
return true;
}
} catch (AbstractMethodError e) {
// ...
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
非常簡單的邏輯遍歷父控件, 調(diào)用父控件的onStartNestedScroll
, 返回true
表示找到了對應(yīng)的外控件, 找到外控件后馬上調(diào)用onNestedScrollAccepted
從這里可以知道
外控件不一定是內(nèi)控件的直接父控件, 但一定是最近的符合條件的外控件.
還可以確定了上面關(guān)于onStartNestedScroll
的方法說明, 返回true
表示接收內(nèi)控件的滑動(dòng)信息.對于NestedScrollView#onStartNestedScroll
內(nèi)部邏輯很簡單, 只要是豎直滑動(dòng)方向就返回true
, 所以可以知道
NestedScrollView
不支持橫向嵌套滑動(dòng).
接著被調(diào)用的是onNestedScrollAccepted
, 看NestedScrollView#onNestedScrollAccepted
// NestedScrollView.java
@Overridepublic void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
}
輔助類的方法很簡單, 就是記錄當(dāng)前的滑動(dòng)方向, 在這里NestedScrollView
又調(diào)用startNestedScroll
來找它自己的外控件, 這是為了連續(xù)嵌套NestedScrollView
, 不過這是NestedScrollView
自己的實(shí)現(xiàn), 不管它.
找到了外控件后ACTION_DOWN
事件就沒嵌套滑動(dòng)的事了, 要滑動(dòng)肯定會(huì)在onTouchEvent
中處理ACTION_MOVE
事件, 接著我們看ACTION_MOVE
事件是怎樣處理的.
// NestedScrollView#onTouchEvent
case MotionEvent.ACTION_MOVE:
// ...
final int y = (int) MotionEventCompat.getY(ev, activePointerIndex);
int deltaY = mLastMotionY - y;
// 讓外控件先處理滑動(dòng)距離
if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {
deltaY -= mScrollConsumed[1];// 消耗滑動(dòng)距離
// ...
}
// ...
if (mIsBeingDragged) {
// ...
// 內(nèi)控件處理滑動(dòng)距離
if (overScrollByCompat(0, deltaY, 0, getScrollY(), 0, range, 0,
0, true) && !hasNestedScrollingParent()) {
// ...
}
final int scrolledDeltaY = getScrollY() - oldY;
final int unconsumedY = deltaY - scrolledDeltaY;
if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) {
// ...
}
// ...
}
break;
這部分是NestedScrollView
能夠處理嵌套滑動(dòng)的關(guān)鍵代碼了, 其他能夠嵌套滑動(dòng)的控件也應(yīng)該在ACTION_MOVE
中類似地處理滑動(dòng)距離.
先計(jì)算出本次滑動(dòng)距離deltaY
, 這里有個(gè)小細(xì)節(jié)
deltaY
等于上一次的Y坐標(biāo)減去這次的Y坐標(biāo), 這意味著在相關(guān)方法中接收到的滑動(dòng)距離參數(shù)中, 滑動(dòng)距離 > 0表示手指向下滑動(dòng), 反之表示手指向上滑動(dòng). 這是因?yàn)樵谄聊恢衁軸正方向是向下的.
得到滑動(dòng)距離deltaY
后, 先把它傳給dispatchNestedPreScroll
, 然后在結(jié)果返回true
的時(shí)候, delta
會(huì)減去mScrollConsumed[1]
.
接著看dispatchNestedPreScroll
干了什么
// View.java
public boolean dispatchNestedPreScroll(int dx, int dy,
@Nullable @Size(2) int[] consumed, @Nullable @Size(2) int[] offsetInWindow) {
// ... 忽略狀態(tài)判斷
consumed[0] = 0;
consumed[1] = 0;
mNestedScrollingParent.onNestedPreScroll(this, dx, dy, consumed);
return consumed[0] != 0 || consumed[1] != 0;
// 其他情況返回false
}
忽略條件判斷和offsetInWindow
的相關(guān)處理, 先指出consumed
就是上一步分析中的mScrollConsumed
, dy
就是deltaY
.
因?yàn)?code>dispatchNestedPreScroll的工作就是把滑動(dòng)距離在內(nèi)控件處理前分發(fā)給外控件, 所以這里的關(guān)鍵代碼也很簡單, 就是直接把相關(guān)的參數(shù)傳給外控件的onNestedPreScroll
, 然后只要外控件消耗了滑動(dòng)距離(不論橫向還是豎向), 就會(huì)返回true
所以
外控件如果想在內(nèi)控件之前消耗滑動(dòng)距離僅需要在
onNestedPreScroll
把消耗的值放到數(shù)組中返回給內(nèi)控件.
onNestedPreScroll
是決定外控件的嵌套滑動(dòng)邏輯的關(guān)鍵方法, 在不同的控件中應(yīng)該是根據(jù)需要有不同的實(shí)現(xiàn)的, 而在NestedScrollView
中就是直接詢問它自己的外控件是否消耗滑動(dòng)距離, 實(shí)現(xiàn)比較簡單就不貼代碼了.
在這里提醒下, 在我們自己修改嵌套滑動(dòng)邏輯的時(shí)候需要注意滑動(dòng)距離的正負(fù)號(hào)和內(nèi)控件處理consumed
數(shù)組的方式. 不過這些都是些數(shù)字游戲, 不細(xì)說了.
好了, 現(xiàn)在外控件已經(jīng)比內(nèi)控件先處理了滑動(dòng)距離了, 如果外控件沒有完全消耗掉所有滑動(dòng)距離, 這時(shí)該內(nèi)控件處理剩下的滑動(dòng)距離了, 不同的控件有不同的滑動(dòng)實(shí)現(xiàn), 在NestedScrollView
中通過NestedScrollView#overScrollByCompat
來進(jìn)行滑動(dòng), 并且滑動(dòng)結(jié)束后通過比對滑動(dòng)前后的scrollY
值得到了內(nèi)控件消耗的滑動(dòng)距離, 然后得到剩下的滑動(dòng)距離, 最后傳給dispatchNestedScroll
.
dispatchNestedScroll
的邏輯跟dispatchNestedPreScroll
幾乎一樣, 區(qū)別是它調(diào)用了外控件的onNestedScroll
, 因?yàn)榈竭@里已經(jīng)是處理滑動(dòng)距離最后的機(jī)會(huì)了, 所以onNestedScroll
不會(huì)再影響內(nèi)控件的處理邏輯了.
到這里ACTION_MOVE
事件就分析完畢了.
最后就是stopNestedScroll
了, 代碼就不貼了, 調(diào)用這個(gè)方法基本是新的滑動(dòng)操作開始前, 或者滑動(dòng)操作結(jié)束/取消, 代碼邏輯就是進(jìn)行一些變量的重置工作和調(diào)用onStopNestedScroll
, 而onStopNestedScroll
也類似.
整個(gè)嵌套滑動(dòng)的基本邏輯就是這樣. 注意這里雖然分析的是NestedScrollView
, 但這代表了嵌套滑動(dòng)的"約定"處理方式, 雖然不同的控件實(shí)際的實(shí)現(xiàn)會(huì)有不同不過應(yīng)該遵循基本方法的調(diào)用順序, 確保參數(shù)的含義和參數(shù)的處理方式.
總結(jié)
- 如果要支持嵌套滑動(dòng), 內(nèi)控件和外控件要支持對應(yīng)的方法, 為了兼容低版本一般通過實(shí)現(xiàn)
NestedScrollingChild
和NestedScrollingParent
接口以及使用NestedScrollingChildHelper
和NestedScrollingParent
輔助類. - 具體嵌套滑動(dòng)邏輯主要是在
onNestedPreScroll
和onNestedScroll
方法中. - 父控件通過給數(shù)組賦值來把消耗的滑動(dòng)距離傳遞給內(nèi)控件.
當(dāng)你希望滑動(dòng)內(nèi)部列表的時(shí)候先把列表頂部的控件隱藏掉, 例如ActionBar, 這時(shí)候嵌套滑動(dòng)就大有用處了, 具體的應(yīng)用場景可以看看Android 嵌套滑動(dòng)機(jī)制(NestedScrolling)的實(shí)現(xiàn)效果.
感覺這篇說得有些零碎, 如果有改進(jìn)的建議歡迎在討論區(qū)指出. :D