嵌套滑動機制,想必大家都不陌生,當我們在使用CoordinatorLayout + AppBarLayout框架設計界面,嵌套滑動就顯得尤爲地重要。CoordinatorLayout成爲協調佈局,目的是協調多個佈局的聯動,聯動就會涉及多個View在滑動時候相互的響應,簡單來說,就是一個View在滑動的時候,另一個View可能需要對應的滑動。那麼這種聯動是怎麼實現的呢?換句話說,View是怎麼知道其他View在滑動呢?有人可能說,是Behavior在進行協調。Behavior畢竟是設計CoordinatorLayout實現出來的東西,不能用於任何View,也就是說,Behavior衆多方法的回調還得依賴View的某些底層機制來實現,那麼這個底層機制是什麼呢?那就是嵌套滑動機制。
回過頭來看一下標題,本文目的是介紹怎麼自定義一個可以產生嵌套滑動的View。那麼既然官方提供了衆多可以支持嵌套滑動的View,爲啥我們還要自己定義呢?自然是官方的不能滿足我們的要求,這也是從我工作中得來教訓。最近,我在負責界面的改版,新界面的交互逼得我不得不使用CoordinatorLayout + AppBarLayout進行開發。當我在開發某一個模塊時,發現需要使用一個支持嵌套滑動的View。最初的想法是使用NestedScrollView套一下,但是 NestedScrollView會把Child給攤平,性能問題自然就會出現了。所以爲了追求極致,就自己定義一個可以支持嵌套滑動的View。
在閱讀本文之前,需要準備知識:
- CoordinatorLayout 的實現原理。
- 嵌套滑動實現的原理。
本文不會深入分析上面兩部分知識,所以我默認大家都瞭解,有興趣的同學可以參考如下文章:
1. 說說嵌套滑動
嵌套滑動機制在API 21 之後就跟View之後綁定之後,Google爸爸在官方庫裏面提供了支持嵌套滑動的View,而這部分View可以分爲兩類:
- 產生嵌套滑動事件的View:這類View前提上是自己本身可以滑動,如果自己都不能滑動,那嵌套滑動什麼的都是白扯。比如說,RecyclerView,NestedScrollView之類,主要是實現NestedScrollingChild、NestedScrollingChild2、NestedScrollingChild3這三個接口的View。(至於這三個接口有啥區別,後文我會分析)
- 處理嵌套滑動事件的View:這類View都會實現NestedScrollingParent、NestedScrollingParent2、NestedScrollingParent3這三個接口中的任意一個。比如說比如CoordinatorLayout、NestedScrollView、SwipeRefreshLayout之類。
通常來說,在嵌套滑動機制中,這兩類的View都是成對出現的,一般是產生嵌套滑動事件的View作爲處理嵌套滑動事件的View的子View,從另一個方面來說,處理嵌套滑動事件的View一般都是ViewGroup,而產生嵌套滑動事件的View可能是任意View的子類。同時,這兩類View如果只出現一個,嵌套滑動也會失效。
從上面舉的例子中,我們可以發現,NestedScrollView同時實現了NestedScrollingChild3、NestedScrollingParent3這兩個接口,那麼就表示這個View同時可以產生嵌套滑動和處理嵌套滑動。這也是爲什麼現在有一個NestedScrollView 套RecyclerView的實現方案。而我本人不推薦此方案,因爲NestedScrollView會攤平內部所有的Child,這就意味着RecyclerView會衆多特性就失效。這也是本文寫作的原因,本文的目的是給大家介紹怎樣自定義一個產生嵌套滑動事件View。
在正式介紹之前,我先給大家分析一下NestedScrollingChildX、NestedScrollingParentX之間的區別。
NestedScrollingChildX之間的區別,直接來看他們的類圖關係:
我來分析這圖中的重點:
- NestedScrollingChild:這個接口主要定義了嵌套滑動需要的幾個關鍵方法,包括preScroll、scroll、preFling、fling等方法。
- NestedScrollingChild2:這個接口是NestedScrollingChild的子接口,在原有的方法基礎上增加type參數,用來判斷TOUCH和非TOUCH的情況,用來區分手指是否還在屏幕上
- NestedScrollingChild3:這個接口是NestedScrollingChild2的子接口,主要是重載了dispatchNestedScroll,在原有的接觸上增加了一個consumed 參數。
我相信大家能區分出來1和2之間的區別,但是3就增加了一個consumed 參數,這是爲何呢?很明顯,這個是用來標記父View消費了多少距離,這個有啥作用呢?主要是在調用了dispatchNestedScroll之後,如果還有未消費的距離,子View就可以停掉滑動。這樣能解決很多奇怪的問題,比如說,我們在Fling RecyclerView到邊界時,觸發了加載更多,理論上應當停掉Fling,但事實上當使用RecyclerView的嵌套滑動時,加載更多完成時會繼續Fling,這就是Fling沒有停掉的原因。不過,這個問題的解決方案需要NestedScrollingChild3 配合NestedScrollingParent3纔會有效。
我們繼續來看NestedScrollingParentX之間的類圖關係:
他們之間的區別跟NestedScrollingChild之間的類似,這裏就不贅述了。不過大家需要注意的是,儘量都實現NestedScrollingChild3和NestedScrollingParent3,因爲這兩個接口方法是最全的,同時最好是將全部的方法都是實現一遍,因爲在某些手機可能會拋出AbstractMethodError
異常,特別是在21以下的手機上。
2. 準備工作
前面對嵌套滑動介紹的差不多了,現在我來介紹怎麼定義一個嵌套滑動的View。步驟主要分爲4步:
- 指定的View實現了NestedScrollingChild3接口,同時實現相關方法,同時使用NestedScrollingChildHelper來分發嵌套滑動。並且調用
setNestedScrollingEnabled
,設置爲true,表示該View能夠產生嵌套滑動的事件,這一點非常的重要。- 在第一步的基礎上,先支持單指的嵌套滑動。
- 在第二步的基礎上,實現多指的嵌套滑動。
- 在第三步的基礎上,實現Fling的嵌套滑動。
注意,本文使用的是CoordinatorLayout 來處理嵌套滑動。
我們先來看看具體的效果:
圖中的CustomNestedViewGroup就是本文要實現的View。同時介於第一步比較簡單,本文就不介紹具體的操作。
本文源碼地址:NestedScrollActivity,有興趣的同學可以參考一下。本文的實現代碼主要參考於NestedScrollView。
3. 支持單指滑動
單指滑動非常的簡單,無非就是在ACTION_MOVE的時機上來觸發滑動而已。但是這種事情看上去簡單,實際上在開發過程中有很多的細節得需要我們注意,正所謂書上得來終覺淺,絕知此事要躬行。不親身去嘗試着寫,理論永遠是理論。
好了,廢話扯得有點多,我們正式開始介紹吧。需要一個View支持單指滑動,基本框架就是要重寫onInterceptTouchEvent
和onTouchEvent
這兩個方法(當然你是繼承View類,便不用重寫onInterceptTouchEvent
方法)。所以,我們分別來看一下這個兩個方法的實現。
(1). onInterceptTouchEvent方法
重寫onInterceptTouchEvent
方法的目的是在合適的時機攔截事件,表示我們的View需要消費後續的事件。我們直接來看onInterceptTouchEvent
方法的實現:
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
val action = ev.actionMasked
if (action == MotionEvent.ACTION_MOVE && mIsBeingDragged) {
return true
}
when (action) {
MotionEvent.ACTION_DOWN -> {
mLastMotionY = ev.y.toInt()
// 開始建立嵌套滑動傳遞鏈
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH)
}
MotionEvent.ACTION_MOVE -> {
val y = ev.y.toInt()
val deltaY = abs(y - mLastMotionY)
if (deltaY > mTouchSlop) {
mIsBeingDragged = true
mNestedYOffset = 0
mLastMotionY = y
parent?.let {
parent.requestDisallowInterceptTouchEvent(true)
}
}
}
MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> {
mIsBeingDragged = false
// 切斷嵌套滑動的傳遞鏈
stopNestedScroll(ViewCompat.TYPE_TOUCH)
}
}
return mIsBeingDragged
}
onInterceptTouchEvent
的實現非常簡單,我在這裏重點的分析幾點:
- 我們在
ACTION_DOWN
調用了startNestedScroll
方法,表示建立起嵌套滑動的傳遞鏈,需要特別注意的是,這裏的Type傳遞的是ViewCompat.TYPE_TOUCH
,主要是爲了區分後續的Fling滑動;其次,我們在ACTION_CANCEL
和ACTION_UP
調用了stopNestedScroll
,表示切斷嵌套滑動的傳遞鏈。- 在
ACTION_MOVE
裏面嘗試設置mIsBeingDragged
,從而攔截事件進行消費。
從onInterceptTouchEvent
方法,我們可以看出一個特點,這個方法不會消費move事件,而只是在這個時機設置某些狀態值,比如說:
mIsBeingDragged :用來表示當前是否需要消費時機,我們可以看到,只要滑動距離超過
mTouchSlop
,就要進行消費。
mLastMotionY:用來記錄上一次event的Y座標,主要用於計算當前event相比於上一次event,產生多少的滑動距離。
mNestedYOffset:用以記錄產生的滑動距離,被父View消費了多少。這個變量怎麼來理解呢?在我們這個案例中,假設View產生了100px的滑動距離,如果View和AppBarLayout整體上移了50px的距離,那麼mNestedYOffset
就爲50。這個變量非常的重要,後續計算mLastMotionY
,以及UP的時候Fling的初速度,都需要它。
既然onInterceptTouchEvent
方法不消費事件,那麼在哪裏消費事件呢?自然是onTouchEvent
方法。
(2). onTouchEvent方法
當View內部沒有child消費的事件,,或者被onInterceptTouchEvent
攔截的事件,都會傳遞到onTouchEvent
方法裏面。而onTouchEvent
方法的作用自然是消費事件,要觸發View內部內容進行滑動,就是在該方法裏面實現。
我們直接來看onTouchEvent
方法的實現:
override fun onTouchEvent(event: MotionEvent): Boolean {
val action = event.actionMasked
if (action == MotionEvent.ACTION_DOWN) {
mNestedYOffset = 0
}
when (action) {
MotionEvent.ACTION_DOWN -> {
mLastMotionY = event.y.toInt()
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH)
}
MotionEvent.ACTION_MOVE -> {
val y = event.y.toInt()
var deltaY = mLastMotionY - y
if (!mIsBeingDragged && abs(deltaY) > mTouchSlop) {
parent?.let {
requestDisallowInterceptTouchEvent(true)
}
mIsBeingDragged = true
if (deltaY > 0) {
deltaY -= mTouchSlop
} else {
deltaY += mTouchSlop
}
}
if (mIsBeingDragged) {
// 1. 在內部內容滑動之前,先調用dispatchNestedPreScroll,讓父View進行滑動。
if (dispatchNestedPreScroll(
0,
deltaY,
mScrollConsumed,
mScrollOffset,
ViewCompat.TYPE_TOUCH
)
) {
// 更新剩下的滑動距離
deltaY -= mScrollConsumed[1]
// 更新父View滑動的距離
mNestedYOffset += mScrollOffset[1]
}
// 更新上一次event的Y座標
mLastMotionY = y - mScrollOffset[1]
val oldScrollY = scrollY
val range = getScrollRange()
// 2. 觸發內容的滑動
overScrollBy(0, deltaY, 0, oldScrollY, 0, range, 0, 0, true)
val scrollDeltaY = scrollY - oldScrollY
val unconsumedY = deltaY - scrollDeltaY
mScrollConsumed[1] = 0
// 3. 當內容滑動完成,如果滑動距離還未消費,那麼就調用dispatchNestedScroll方法,詢問父View是否還消費
dispatchNestedScroll(
0,
scrollDeltaY,
0,
unconsumedY,
mScrollOffset,
ViewCompat.TYPE_TOUCH,
mScrollConsumed
)
// 再次更新相關信息
mLastMotionY -= mScrollOffset[1]
mNestedYOffset += mScrollOffset[1]
}
}
MotionEvent.ACTION_UP -> {
endDrag()
}
MotionEvent.ACTION_CANCEL -> {
endDrag()
}
}
return true
}
onTouchEvent
方法代碼比較長,但是重點都是在move裏面,我們分開來看:
- down事件都是一些基本實現,比如說更新
mLastMotionY
,還有就是調用startNestedScroll
方法。- up和cancel都是調用了
endDrag
,這個方法裏面只做兩件事,重置mIsBeingDragged
,同時還調用了stopNestedScroll
。
而move事件的實現就比較複雜了,我將其分爲三步:
- 根據
mLastMotionY
計算出來本次產生了多少滑動距離,然後就是調用dispatchNestedPreScroll
方法。目的就是,在內部滑動之前,先詢問父View是否要消費距離。其中mScrollConsumed
裏面記錄的父View消費的距離,同時mScrollOffset
表示我們的View在屏幕滑動的距離,主要是根據getLocationInWindow
來計算的。父View滑動完成,自然就是就是更新某些狀態值,比如說:deltaY 、mNestedYOffset 、mLastMotionY 。可能有人會有疑問,爲啥還有更新mLastMotionY呢?因爲我們的View在屏幕中更新了位置,所記錄的上一次event的Y座標自然也要更新,不然下一次event計算的滑動距離會有誤差。- 調用
overScrollBy
方法,用來滑動View內部的內容。這裏,大家可能又有疑問了,爲啥要調用overScrollBy
,而不是調用scrollBy
或者scrollYo
呢?舉一個例子,如果滑動距離還剩下100px,但是View其實只能滑動50px,此時不能直接滑動100px,所以這裏需要裁剪滑動距離,如果直接調用scrollBy
或者scrollYo
,我們需要自己計算裁剪距離,但是overScrollBy
方法內部會根據scrollRange來進行裁剪,所以第調用overScrollBy
是爲了我們自己不需要寫裁剪的代碼。- 當View自己滑動完成,調用
dispatchNestedScroll
,詢問父View是否需要消費剩下的距離。如果消費了,自然要更新mLastMotionY
和mNestedYOffset
。
在嵌套滑動流程中,特別是move事件中需要觸發嵌套滑動時,這個流程固定不變的,即:
看上去還是比較簡單的,但是有些前提大家必須知道,在這裏,我再次強調一遍:
- 調用
setNestedScrollingEnabled
方法,設置爲true。- 在調用
dispatchNestedPreScroll
和dispatchNestedScroll
之前,必須先調用startNestedScroll
,並且傳遞的Type必須是一致的。- 滑動完成之後,需要調用
stopNestedScroll
方法來切斷傳遞鏈。
Type一共有兩個,分別是:
- TYPE_TOUCH:表示手指在屏幕上產生的嵌套滑動事件。
- TYPE_NON_TOUCH:表示手指未在屏幕上產生的嵌套滑動事件,比如說Fling滑動。
單指滑動的實現就介紹在這裏,整體上來說還是比較簡單的。完整代碼大家可以在KotlinDemo找到,commit message 爲【新增自定義NestedScrollViewGroup的Demo,並且完成CustomNestedScrollView的move事件處理】。
4. 支持多指滑動
如果要支持多指滑動,首先要引入新的action含義,如下:
- ACTION_POINTER_DOWN:表示非第一個手指落在屏幕中。
- ACTION_POINTER_UP: 表示非最後一個手指離開屏幕。
- ACTION_UP:表示最後一個手指離開屏幕
- ACTION_MOVE:表示任意一個手指在滑動。
- ACTION_DOWN:表示第一個手指落在屏幕中。
同時,從整體上來看,我們需要定義一個變量,表示最近一個落在屏幕中的手指;還有就是,我們需要在down和up時,實時的更新這個記錄的最近手指。最後就是,在獲取滑動座標時,需要傳入手指Id,不能像以前直接getY來獲取。
(1).使用手指Id
前面已經提到了,此時獲取座標不能直接調用getY方法,我們來看一下怎麼獲取,這裏只看onTouchEvent
方法:
override fun onTouchEvent(event: MotionEvent): Boolean {
// ······
when (action) {
MotionEvent.ACTION_DOWN -> {
mActivePointerId = event.getPointerId(0)
mLastMotionY = event.y.toInt()
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH)
}
MotionEvent.ACTION_MOVE -> {
val pointerIndex = event.findPointerIndex(mActivePointerId)
if (pointerIndex == -1) {
return true
}
val y = event.getY(pointerIndex).toInt()
var deltaY = mLastMotionY - y
//······
}
// ······
}
return true
}
我們發現在ACTION_DOWN
和ACTION_MOVE
中,都有一個mActivePointerId
,用來表示最近活躍的手指Id,可以通過這個id,找到一個index,然後event可以通過index,獲取對應的座標。同時,我們還發現一個小細節,就是在ACTION_DOWN
裏面初始化了mActivePointerId
了,這一點大家要注意。
(2). 更新手指Id
除去初始化手指Id和使用手指Id,還有必不可少的步驟就是:更新手指Id。更新時機體現在如下幾個地方:
- ACTION_CANCEL、ACTION_UP
- ACTION_POINTER_DOWN
- ACTION_POINTER_UP
我們直接看代碼:
override fun onTouchEvent(event: MotionEvent): Boolean {
// ······
when (action) {
// ······
MotionEvent.ACTION_UP -> {
mActivePointerId = INVALID_POINTER
// // ······
}
MotionEvent.ACTION_CANCEL -> {
mActivePointerId = INVALID_POINTER
endDrag()
}
MotionEvent.ACTION_POINTER_DOWN -> {
val newPointerIndex = event.actionIndex
mLastMotionY = event.getY(newPointerIndex).toInt()
mActivePointerId = event.getPointerId(newPointerIndex)
}
MotionEvent.ACTION_POINTER_UP -> {
onSecondaryPointerUp(event)
}
}
// ······
return true
}
從代碼中來看,三個地方的區別如下:
- ACTION_CANCEL、ACTION_UP:重置
mActivePointerId
- ACTION_POINTER_DOWN:更新
mActivePointerId
,因爲這個時機表示一個手指落入屏幕,所以直接更新爲當前手指。- ACTION_POINTER_UP:調用
onSecondaryPointerUp
方法,嘗試更新mActivePointerId
。分爲兩種情況來看待:如果離開屏幕的手指Id不是mActivePointerId
記錄的,那麼就直接忽略;如果是mActivePointerId
記錄的,就跟據pointerIndex
來判斷,將mActivePointerId
更新到第一個手指,還是其他手指。細節大家可以看onSecondaryPointerUp
方法實現。
總的來說,多指滑動的實現比較簡單,畢竟已經有單指滑動的基礎。完整代碼大家可以在KotlinDemo找到,commit message 爲【支持多指滑動】。
5. 支持Fling滑動
其實支持了單指滑動和多指滑動,就能滿足大部分的要求,Fling滑動算是比較特別的需求了,大概只有列表類View才需要支持。不過,我們也來看看怎麼實現。
要想一個View支持Fling,我們需要準備兩個東西:
- 需要計算手指離開View時,滑動的速度。這個速度可以作爲Fling滑動的初速度。
- 在初速度基礎上,來實現Fling滑動。
(1). VelocityTracker
要計算手指離開的屏幕時滑動的速度,這個非常簡單,官方已經提供對應的工具了,那就是VelocityTracker
,我們將對應的Event傳入到這個工具類裏面就可以計算出我們想要的速度。
不過,在這裏我需要強調一個事,那就是由於涉及到到嵌套滑動,那麼View就會屏幕中改變位置,直接傳入Event會導致我們計算的速度不是正確的。舉一個例子,假設上一個move event的Y座標是500,同時這個move事件產生了100px的滑動距離,將該View上移了100px,那麼下一個event的Y座標也是500,實際上是沒有改變的,因爲event.getY是相對於View的座標,手指相對於View的位置沒有變,所以event的座標沒有變。這也是爲什麼前面,我們需要實時更新mLastMotionY
,就是爲了避免下一次計算的滑動距離不正確;同時還定義了一個mNestedYOffset
,用來記錄的是,本次完整事件鏈中(down->move->up),該View在屏幕中移動了多少距離。
我們直接來看實現,還是onTouchEvent
方法:
override fun onTouchEvent(event: MotionEvent): Boolean {
val action = event.actionMasked
if (action == MotionEvent.ACTION_DOWN) {
mNestedYOffset = 0
}
val vtev = MotionEvent.obtain(event)
vtev.offsetLocation(0f, mNestedYOffset.toFloat())
// ······
mVelocityTracker?.let {
it.addMovement(vtev)
}
vtev.recycle()
return true
}
這裏爲了解決上面所說速度計算不對,在調用addMovement
之前,調整event的座標,調整所依賴的就是mNestedYOffset
。
只要我們理解到這一點就行,其他的就不需要過多的分析了。
(2). OverScroller
要想實現Fling滑動,我們需要使用到OverScroller
。OverScroller
的作用是,擁有一個初速度,然後不斷輪詢產生滑動的距離,我們可以這個滑動距離,用來滑動我們想要的東西,這裏就是兩部分:父View和子View的內容。
我們來看一下實現:
override fun onTouchEvent(event: MotionEvent): Boolean {
// ······
MotionEvent.ACTION_UP -> {
mActivePointerId = INVALID_POINTER
val velocityTracker = mVelocityTracker
velocityTracker?.computeCurrentVelocity(1000, mMaximumVelocity.toFloat())
// 向上滑動速度爲負,向下滑動速度爲正
val initVelocity = velocityTracker?.getYVelocity(mActivePointerId)?.toInt() ?: 0
if (abs(initVelocity) > mMinimumVelocity) {
if (!dispatchNestedPreFling(0F, -initVelocity.toFloat())) {
dispatchNestedFling(0F, -initVelocity.toFloat(), true)
fling(-initVelocity.toFloat())
}
}
endDrag()
}
// ······
return true
}
fling的觸發是在up裏面方法,但是我們從代碼實現上可以看出來,在正式調用fling方法之前,還通過嵌套滑動的方法--dispatchNestedPreFling
,將fling的滑動分發到父View,主要的目的是爲了詢問父View是否消費fling事件。如果返回的是false,那麼表示父View不消費fling,那就是子View自己消費了,消費的邏輯主要是體現在fling
發現裏面。不過在看fling方法之前,我們先關注另一個點,那就是endDrag:
private fun endDrag() {
mIsBeingDragged = false
stopNestedScroll(ViewCompat.TYPE_TOUCH)
}
在這個方法裏面,我們關注的是,這個通過stopNestedScroll
切斷了type爲TYPE_TOUCH
的傳遞鏈,這是爲了後面能夠重新建立TYPE_NON_TOUCH
的傳遞鏈做準備。
我們回過頭來看繼續看fling方法:
private fun fling(velocityY: Float) {
mOverScroller.fling(0, scrollY, 0, velocityY.toInt(), 0, 0, Int.MIN_VALUE, Int.MAX_VALUE)
runAnimatedScroll()
}
這裏通過OvserScrolled的fling方法開始觸發fling滑動,同時還調用了runAnimatedScroll
方法:
private fun runAnimatedScroll() {
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_NON_TOUCH)
mLastScrollY = scrollY
ViewCompat.postInvalidateOnAnimation(this)
}
這個方法裏面主要是做了三件事:
- 建立
TYPE_NON_TOUCH
的傳遞鏈。- 記錄scrollY,用以計算
OverScroller
產生的滑動距離。- 調用
postInvalidateOnAnimation
方法,從而觸發輪詢,回調computeScroll
方法。
fling真實的滑動實現邏輯都在computeScroll
方法裏面,我們看一下:
override fun computeScroll() {
if (mOverScroller.isFinished) {
return
}
mOverScroller.computeScrollOffset()
val y = mOverScroller.currY
var deltaY = y - mLastScrollY
mLastScrollY = y
mScrollConsumed[1] = 0
dispatchNestedPreScroll(0, deltaY, mScrollConsumed, null, ViewCompat.TYPE_NON_TOUCH)
deltaY -= mScrollConsumed[1]
val range = getScrollRange()
if (deltaY != 0) {
val oldScrollY = scrollY
overScrollBy(0, deltaY, 0, oldScrollY, 0, range, 0, 0, false)
val consumedY = scrollY - oldScrollY
deltaY -= consumedY
mScrollConsumed[1] = 0
dispatchNestedScroll(
0,
consumedY,
0,
deltaY,
null,
ViewCompat.TYPE_NON_TOUCH,
mScrollConsumed
)
deltaY -= mScrollConsumed[1]
}
if (deltaY != 0) {
abortAnimateScroll()
}
if (!mOverScroller.isFinished) {
ViewCompat.postInvalidateOnAnimation(this)
} else {
abortAnimateScroll()
}
}
從代碼實現上來看,整體框架跟move的實現比較類似。首先,先計算產生的滑動距離,然後通過dispatchNestedPreScroll
詢問父View是否消費滑動距離,然後更新滑動距離,調用overScrollBy
方法,自己消費滑動距離;當自己消費完成,再調用dispatchNestedScroll
詢問父View是否消費。
這裏有一個小細節,當到最後滑動距離沒有消費完成,表示當前已經滾到邊界了此時需要停掉Fling滑動。
fling 的整個邏輯就是這樣,整體來說還是比較清晰的。完整代碼大家可以在KotlinDemo找到,commit message 爲【支持Fling滑動】
6. 總結
到這裏,我對實現嵌套滑動的介紹就結束了。本文的重點介紹如何定義一個能夠產生嵌套滑動的View,並沒有介紹如何定義一個處理嵌套滑動的View。
這個可以留給大家,有興趣的同學可以參考CoordinatorLayout
的實現,自己實現一個。之前我定義過處理上下兩個RecyclerView的ViewGroup,這之間的聯動真是折騰人。類似於這種結構:
其中黃色部分的ViewGroup就是需要我來定義。有興趣的同學嘗試這種ViewGroup怎麼來定義。
我對本文的內容來做一個簡單的總結:
- 要定義一個產生嵌套滑動的View,實現需要實現
NestedScrollingChild
接口,並且調用setNestedScrollingEnabled
方法,設置爲true,表示該View可以產生嵌套滑動的事件。- 定義一個產生嵌套滑動的View,需要處理三個問題:單指滑動,多指滑動,Fling滑動。
- 單指滑動,需要在down時,調用
startNestedScroll
方法建立起嵌套滑動的傳遞鏈;在move時,計算產生的滑動距離,先調用dispatchNestedPreScroll
方法,詢問父View消費滑動的距離,然後在自己消費滑動距離,最後在調用dispatchNestedScroll
再次詢問父View消費滑動的距離。- 多指滑動,需要在down時,定義一個活躍的手指Id;在move時,使用這個手指Id計算event的Y座標,從而正確的計算滑動距離;最後就是在合適的時機(up、cancel)正確的更新這個手指Id。
- Fling 滑動需要處理兩個問題:計算滑動速度和觸發Fling滑動。滑動速度可以使用VelocityTracker來計算,Fling滑動可以使用OverScroller來實現。但是Fling滑動需要跟手指滑動區分的是,Fling滑動建立的嵌套滑動傳遞鏈,type是
TYPE_NON_TOUCH
;而單指滑動是TYPE__TOUCH
。