(轉載請註明做者:RubiTree,地址:blog.rubitree.com )java
NestedScrolling 機制翻譯過來叫嵌套滑動機制(本文將混用),它提供了一種優雅解決嵌套滑動問題的方案,具體是什麼方案呢?咱們從嵌套的同向滑動提及。android
所謂嵌套同向滑動,就是指這樣一種狀況:兩個可滑動的View內外嵌套,並且它們的滑動方向是相同的。 git
這種狀況若是使用通常的處理方式,會出現交互問題,好比使用兩個ScrollView
進行佈局,你會發現,觸摸着內部的ScrollView
進行滑動,它是滑不動的 (不考慮後來 Google 給它加的NestedScroll
開關): github
(舒適提示:本文涉及事件分發的內容比較多,建議對事件分發不太熟悉的同窗先閱讀另外一篇透鏡《看穿 > 觸摸事件分發》)數組
若是你熟悉 Android 的觸摸事件分發機制,那麼緣由很好理解:兩個ScrollView
嵌套時,滑動距離終於達到滑動手勢斷定閾值(mTouchSlop
)的這個MOVE
事件,會先通過父 View 的onInterceptTouchEvent()
方法,父 View 因而直接把事件攔截,子 View 的onTouchEvent()
方法裏雖然也會在斷定滑動距離足夠後調用requestDisallowInterceptTouchEvent(true)
,但始終要晚一步。app
而這個效果顯然是不符合用戶直覺的 那用戶但願看到什麼效果呢?框架
ScrollView
進行滑動時,能先滑動內部的ScrollView
,只有當內部的ScrollView
滑動到盡頭時,才滑動外部的ScrollView
這看上去很是天然,也跟觸摸事件的處理方式一致,但相比觸摸事件的處理,要在滑動時實現一樣的效果卻會困難不少ide
那能不能把事件攔截機制變成雙向的呢?不是不行,但這顯然違背了攔截機制的初衷,並且它很快會發展成無限遞歸的:雙向的事件攔截機制自己是否也須要一個攔截機制呢?因而有了攔截的攔截,而後再有攔截的攔截的攔截... 佈局
換一個更直接的思路,若是咱們的需求始終是內部滑動優先,那是否可讓外部 View「攔截滑動的斷定條件」比內部 View「申請外部不攔截的斷定條件」更嚴格,從而讓滑動距離每次都先達到「申請外部不攔截的斷定條件」,子 View 就可以在父 View 攔截事件前申請外部不攔截了。 能看到在ScrollView
中,「攔截滑動的斷定條件」和「申請外部不攔截的斷定條件」都是Math.abs(deltaY) > mTouchSlop
,咱們只須要增大「攔截滑動的斷定條件」時的mTouchSlop
就好了。post
但實際上這樣作並很差,由於mTouchSlop
到底應該增長多少,是件不肯定的事情,手指滑動的快慢和屏幕的分辨率可能都會對它有影響。 因此能夠換一種實現,那就是讓第一次「攔截滑動的斷定條件」成立時,先不進行攔截,若是內部沒有申請外部不攔截,第二次條件成立時,再進行攔截,這樣也一樣實現了開始的思路。 因而繼承 ScrollView
,覆寫它的onInterceptTouchEvent()
:
class SimpleNestedScrollView(context: Context, attrs: AttributeSet) : ScrollView(context, attrs) {
private var isFirstIntercept = true
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
if (ev.actionMasked == MotionEvent.ACTION_DOWN) {
isFirstIntercept = true
}
val result = super.onInterceptTouchEvent(ev)
if (result && isFirstIntercept) {
isFirstIntercept = false
return false
}
return result
}
}
複製代碼
它的效果是這樣,能看到確實實現了讓內部先獲取事件:
但咱們但願體驗能更好一點,從上圖能看到,內部即便在本身沒法滑動的時候,也會對事件進行攔截,沒法經過滑動內部來讓外部滑動。其實內部應該在本身沒法滑動的時候,直接在onTouchEvent()
返回false
,不觸發「申請外部不攔截的斷定條件」,就能讓內外都有機會滑動。 這個要求很是通用並且合理,在SimpleNestedScrollView
基礎上進行簡單修改,加上如下代碼:
private var isNeedRequestDisallowIntercept: Boolean? = null
override fun onTouchEvent(ev: MotionEvent): Boolean {
if (ev.actionMasked == MotionEvent.ACTION_DOWN) isNeedRequestDisallowIntercept = null
if (ev.actionMasked == MotionEvent.ACTION_MOVE) {
if (isNeedRequestDisallowIntercept == false) return false
if (isNeedRequestDisallowIntercept == null) {
val offsetY = ev.y.toInt() - getInt("mLastMotionY")
if (Math.abs(offsetY) > getInt("mTouchSlop")) { // 滑動距離足夠判斷滑動方向是上仍是下後
// 判斷本身是否能在對應滑動方向上進行滑動(不能則返回false)
if ((offsetY > 0 && isScrollToTop()) || (offsetY < 0 && isScrollToBottom())) {
isNeedRequestDisallowIntercept = false
return false
}
}
}
}
return super.onTouchEvent(ev)
}
private fun isScrollToTop() = scrollY == 0
private fun isScrollToBottom(): Boolean {
return scrollY + height - paddingTop - paddingBottom == getChildAt(0).height
}
複製代碼
getInt("mLastMotionY")
和getInt("mTouchSlop")
爲反射代碼,獲取私有的mLastMotionY
和mTouchSlop
屬性運行效果以下:
這樣就完成了對嵌套滑動View最基本的需求:你們都能滑了。
後來我發現了一種更野的路子,不用當心翼翼地讓改動儘可能小,既然內部優先,徹底可讓內部的ScrollView
在DOWN
事件的時候就申請外部不攔截,而後在滑動一段距離後,若是判斷本身在該滑動方向沒法滑動,再取消對外部的攔截限制,思路是相似的但代碼更簡單。
class SimpleNestedScrollView(context: Context, attrs: AttributeSet) : ScrollView(context, attrs) {
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
if (ev.actionMasked == MotionEvent.ACTION_DOWN) parent.requestDisallowInterceptTouchEvent(true)
if (ev.actionMasked == MotionEvent.ACTION_MOVE) {
val offsetY = ev.y.toInt() - getInt("mLastMotionY")
if (Math.abs(offsetY) > getInt("mTouchSlop")) {
if ((offsetY > 0 && isScrollToTop()) || (offsetY < 0 && isScrollToBottom())) {
parent.requestDisallowInterceptTouchEvent(false)
}
}
}
return super.dispatchTouchEvent(ev)
}
}
複製代碼
運行的效果跟上面是同樣的,不重複貼圖了。
但這兩種方式目前爲止都沒有實現最好的交互體驗,最好的交互體驗應該讓內部不能滑動時,能接着滑動外部,甚至在你滑動過程當中快速擡起時,接下來的慣性滑動也能在兩個滑動View間傳遞。
因爲滑動這個交互的特殊性,咱們能夠在外部對它進行操做,因此連續滑動的實現很是簡單,只要重寫scrollBy
就行了,因此在已有代碼的基礎上再加上下面的代碼(上面的兩種思路都是加同樣的代碼):
override fun scrollBy(x: Int, y: Int) {
if ((y > 0 && isScrollToTop()) || (y < 0 && isScrollToBottom())) {
(parent as View).scrollBy(x, y)
} else {
super.scrollBy(x, y)
}
}
複製代碼
效果以下:
而慣性滑動的實現就會相對複雜一點,得對computeScroll()
方法下手,要作的修改會多一些,這裏暫時不去實現了,但作確定是沒問題的。
到這裏咱們對嵌套滑動交互的理解基本已經很是通透了,知道了讓咱們本身實現也就那麼回事,主要須要解決下面幾個問題:
這時就能夠來看看看系統提供的 NestedScrolling 機制是怎麼完成嵌套滑動需求的,跟咱們的實現相比,有什麼區別,是更好仍是更好?
(轉載請註明做者:RubiTree,地址:blog.rubitree.com )
與咱們不一樣,咱們只考慮了給ScrollView
增長支持嵌套滑動的特性,但系統開發者須要考慮給全部有滑動交互的 View 增長這個特性,因此一個直接的思路是在 View 里加入這個機制。
那麼要怎麼加,加哪些東西呢?
View
裏是不能放其餘View
的,它只能是內部的、主動的角色,而ViewGroup
既能夠放在另外一ViewGroup
裏,它裏邊也能夠放其餘的View
,因此它能夠是內部的也能夠是外部的角色View
和ViewGroup
的繼承關係,因此一個很天然的設計是:在View
中加入主動邏輯,在ViewGroup
中加入被動邏輯由於不是每一個View
和ViewGroup
都可以滑動,滑動只是衆多交互中的一種,View
和ViewGroup
不可能直接把全部事情都作了而後告訴你:Android 支持嵌套滑動了哦~ 因此 Google 加入的這些邏輯其實都是幫助方法,相關的View
須要選擇在合適的時候進行調用,最後才能實現嵌套滑動的效果。
先不說加了哪些方法,先說 Google 但願能幫助你實現一個什麼樣的嵌套滑動效果:
ns child
和ns parent
,對應了上面的內部 View 和外部 View
nested scroll
的縮寫;2)爲何叫邏輯上?由於實際上它容許你一個 View 同時扮演兩個角色ns child
會在收到DOWN
事件時,找到本身祖上中最近的能與本身匹配的ns parent
,與它進行綁定並關閉它的事件攔截機制ns child
會在接下來的MOVE
事件中斷定出用戶觸發了滑動手勢,並把事件流攔截下來給本身消費MOVE
事件增長的滑動距離:
ns child
並非直接本身消費,而是先把它交給ns parent
,讓ns parent
能夠在ns child
以前消費滑動ns parent
沒有消費或是沒有消費完,ns child
再本身消費剩下的滑動ns child
本身仍是沒有消費完這個滑動,會再把剩下的滑動交給ns parent
消費ns child
能夠作最終的處理ns child
的computeScroll()
方法中,ns child
也會把本身由於用戶fling
操做引起的滑動,與上一條中用戶滑動屏幕觸發的滑動同樣,使用「parent -> child -> parent -> child」的順序進行消費注:
- 以上過程參考當前最新的
androidx.core 1.1.0-alpha01
中的NestedScrollView
和androidx.recyclerView 1.1.0-alpha01
中的RecyclerView
實現,與以前的版本細節略有不一樣,後文會詳述其中差別- 爲了理解上的方便,有幾處細節的描述作了簡化:其實在
NestedScrollView
、RecyclerView
這類經典實現中: 1. 在ns child
滾動時,只要用戶手指一按下,ns child
就會攔截事件流,不用等到判斷出滑動手勢(具體能夠關注源碼中的mIsBeingDragged
字段) 1. 這個細節是合理的,會讓用戶體驗更好 2. (後文將不會對這個細節再作說明,而是直接用簡化的描述,實現時若是要提升用戶體驗,須要注意這個細節) 1. 按照 Android 的觸摸事件分發規則,若是ns child
內部沒有要消費事件的 View,事件也將直接交給ns child
的onTouchEvent()
消費。這時在NestedScrollView
等ns child
的實現中,接下來在onTouchEvent()
裏判斷出用戶是要滑動本身以前,就會把用戶的滑動交給ns parent
進行消費(回到4.4) 1. 這個設計我我的以爲不太合理,既然是傳遞滑動那就應該在判斷出用戶確實在滑動以後纔開始傳遞,而不是這樣直接傳遞,並且在後文的實踐部分,你確實能看到這種設計帶來的問題 1. (後文的描述中若是沒有特別說明,也是默認忽略這個細節)- 描述中省略了關於直接傳遞 fling 的部分,由於這塊的設計存在問題,並且最新版本這部分機制的做用已經很是小了,後面這點會詳細講
你會發現,這跟咱們本身實現嵌套滑動的方式很是像,但它有這些地方作得更好(具體怎麼實現的見後文)
ns child
使用更靈活的方式找到和綁定本身的ns parent
,而不是直接找本身的上一級結點ns child
在DOWN
事件時關閉ns parent
的事件攔截機制單獨用了一個 Flag 進行關閉,這就不會關閉ns parent
對其餘手勢的攔截,也不會遞歸往上關閉祖上們的事件攔截機制。ns child
直到在MOVE
事件中肯定本身要開始滑動後,纔會調用requestDisallowInterceptTouchEvent(true)
遞歸關閉祖上們所有的事件攔截MOVE
事件傳遞來的滑動,都使用「parent -> child -> parent -> child」機制進行消費,讓ns child
在消費滑動時與ns parent
配合更加細緻、緊密和靈活fling
操做引起的滑動,與用戶滑動屏幕觸發的滑動使用一樣的機制進行消費,實現了完美的慣性連續效果到這一步,咱們再來看看 Google 給 View 和 ViewGroup 加了哪些方法?又但願咱們何時怎麼去調用它們?
加入的須要你關心的方法一共有這些(只註明了關鍵返回值和參數,參考當前最新的版本 androidx.core 1.1.0-alpha01
):
// 『View』
setNestedScrollingEnabled(true) // 調用
startNestedScroll() // 調用
dispatchNestedPreScroll(int delta, int[] consumed) // 調用
dispatchNestedScroll(int unconsumed, int[] consumed) // 調用
stopNestedScroll() // 調用
// 『ViewGroup』
boolean onStartNestedScroll() // 覆寫
int getNestedScrollAxes() // 調用
onNestedPreScroll(int delta, int[] consumed) // 覆寫
onNestedScroll(int unconsumed, int[] consumed) // 覆寫
複製代碼
怎麼調用這些方法取決於你要實現什麼角色
ns child
角色時,你須要:
setNestedScrollingEnabled(true)
,啓用嵌套滑動機制DOWN
事件時調用startNestedScroll()
方法,它會「找到本身祖上中最近的與本身匹配的ns parent
,進行綁定並關閉ns parent
的事件攔截機制」dispatchNestedPreScroll()
方法,傳入用戶的滑動距離,這個方法會「觸發ns parent
對滑動的消費,而且把消費結果返回」ns child
能夠開始本身消費剩下滑動ns child
本身消費完後調用dispatchNestedScroll()
方法,傳入最後沒消費完的滑動距離,這個方法會繼續「觸發ns parent
對剩下滑動的消費,而且把消費結果返回」ns child
拿到最後沒有消費完的滑動,作最後的處理,好比顯示 overscroll 效果,好比在 fling 的時候中止scroller
ns parent
,那麼在View
的computeScroll()
方法中,對於每一個scroller
計算到的滑動距離,與MOVE
事件中處理滑動同樣,按照這個順序進行消費:「dispatchNestedPreScroll()
-> 本身 -> dispatchNestedScroll()
-> 本身」UP
、CANCEL
事件中以及computeScroll()
方法中慣性滑動結束時,調用stopNestedScroll()
方法,這個方法會「打開ns parent
的事件攔截機制,並取消與它的綁定」ns parent
角色時,你須要:
boolean onStartNestedScroll(View child, View target, int nestedScrollAxes)
,經過傳入的參數,決定本身對這類嵌套滑動感興趣,在感興趣的狀況中返回true
,ns child
就是經過遍歷全部ns parent
的這個方法來找到與本身匹配的ns parent
getNestedScrollAxes()
,它會返回你某個方向的攔截機制是否已經被ns child
關閉了,若是被關閉,你就不該該攔截事件了onNestedPreScroll
和onNestedScroll
方法中耐心等待ns child
的消息,沒錯,它就對應了你在ns child
中調用的dispatchNestedPreScroll
和dispatchNestedScroll
方法,你能夠在有必要的時候進行本身的滑動,而且把消耗掉的滑動距離經過參數中的數組返回這麼實現的例子能夠看 ScrollView
,只要打開它的setNestedScrollingEnabled(true)
開關,你就能看到嵌套滑動的效果:(實際上ScrollView
實現的不是完美的嵌套滑動,緣由見下一節)
ns parent
還好,但ns child
的實現還會有大量的細節(包括實踐部分會提到的「ns parent
偏移致使的 event
校訂」等等),光是描述可能不夠直接,爲此我也爲ns child
準備了一份參考模板:NestedScrollChildSample
注意
- 雖然模板在IDE裏不會報錯,但這不是能夠運行的代碼,這是剔除
NestedScrollView
中關於ns parent
的部分,獲得的能夠認爲是官方推薦的ns child
實現- 同時,爲了讓主線邏輯更加清晰,刪去了多點觸控相關的邏輯,實際開發若是須要,能夠直接參考
NestedScrollView
中的寫法,不會麻煩太多*(有空會寫多點觸控的透鏡系列XD)*- 其中的關鍵部分是在觸摸和滾動時怎麼調用
NestedScrollingChild
接口的方法,也就是onInterceptTouchEvent()
、onTouchEvent()
、computeScroll()
中大約 200 行的代碼
另外,以上都說的是單一角色時的使用狀況,有時候你會須要一個 View 扮演兩個角色,就須要再多作一些事情,好比對於ns parent
,你要時刻注意你也是 ns child
,在來生意的時候也照顧一下本身的ns parent
,這些能夠去看 NestedScrollView
的實現,不在這展開了。
(轉載請註明做者:RubiTree,地址:blog.rubitree.com )
可是有人就問了:(回到答案)
NestedScrollingParent
和NestedScrollingChild
這兩個接口,而後利用上NestedScrollingParentHelper
和NestedScrollingChildHelper
這兩個幫助類,才能實現一個支持嵌套滑動的自定義 View 啊,並且你們都稱讚這是一種很棒的設計呢,怎麼到你這就變成了直接加在View和 ViewGroup 裏的方法了,這麼普通的 DISCO 嘛?並且題圖裏也看到有這幾個接口的啊,你難道是標題黨嗎?NestedScrollingParent
和NestedScrollingChild
這兩個接口裏放了那麼多方法,你卻只講9個呢?NestedScrollingChild
,有NestedScrollingChild2
,工做不飽和的同窗會發現最近 Google 還增長了NestedScrollingChild3
,這都是在幹哈?改了些什麼啊?彆着急,要解釋這些問題,還得先來了解下歷史,翻翻sdk
和support library
家的老黃曆: (嫌棄太長也能夠直接前往觀看小結) (事情要從五年前提及...)
在 Android 5.0 / API 21 (2014.9)
時, Google 第一次加入了 NestedScrolling 機制。
雖然在版本更新裏徹底沒有提到,可是在View
和 ViewGroup
的源碼裏你已經能看到其中的嵌套滑動相關方法。 並且此時使用了這些方法實現了嵌套滑動效果的 View 其實已經有很多了,除了咱們講過的ScrollView
,還有AbsListView
、ActionBarOverlayLayout
等,而這些也基本是當時全部跟滑動有關的 View 了。 因此,如上文嵌套ScrollView
的例子所示,在Android 5.0
時你們其實就能經過setNestedScrollingEnabled(true)
開關啓用 View 的嵌套滑動效果。
這是 NestedScrolling 機制的初版實現。
由於第一個版本的 NestedScrolling 機制是加在 framework 層的 View 和 ViewGroup 中,因此能享受到嵌套滑動效果的只能是Android 5.0
的系統,也就是當時最新的系統。 你們都知道,這樣的功能不會太受開發者待見,因此在當時 NestedScrolling 機制基本沒有怎麼被使用。(因此你們一說嵌套滑動就提後來才發佈的NestedScrollView
而不不知道ScrollView
早就能嵌套滑動也是很是正常了)
Google 就以爲,這可不行啊,嵌套滑不動的Bug不能老留着啊 好東西得你們分享啊,因而一狠心,梳理了下功能,重構出來兩個接口(NestedScrollingChild
、NestedScrollingParent
)兩個 Helper (NestedScrollingChildHelper
、NestedScrollingParentHelper
)外加一個開箱即用的NestedScrollView
,在 Revision 22.1.0 (2015.4)
到來之際,把它們一塊加入了v4 support library
豪華午飯。
這下大夥就開心了,奔走相告:嵌套滑動卡了嗎,趕忙上NestedScrollView
吧,Android 1.6
也能用。 同時NestedScrollingChild
和NestedScrollingParent
也被你們知曉了,要本身整個嵌套滑動,那就實現這兩接口吧。
隨後,在下一個月 Revision 22.2.0 (2015.5)
時,Google又隆重推出了 Design Support library
,其中的殺手級控件CoordinatorLayout
更是把 NestedScrolling 機制玩得出神入化。
NestedScrolling 機制終於走上臺前,一時風頭無兩。
但注意,我比較了一下,這時的 NestedScrolling 機制相比以前放在 View 和 ViewGroup 中的第一個版本,其實徹底沒有改動,只是把 View 和 ViewGroup 裏的方法分紅兩部分放到接口和 Helper 裏了,NestedScrollView
裏跟嵌套滑動有關的部分也跟ScrollView
裏的沒什麼區別,因此此時的 NestedScrolling 機制本質仍是第一個版本,只是形式發生了變化。
而 NestedScrolling 機制形式的變化帶來了什麼影響呢?
isNestedScrollingEnabled()
、onNestedScrollAccepted()
),有的是設計彆扭用得不多的(好比dispatchNestedFling()
),有的是須要特別優化細節才須要的(好比hasNestedScrollingParent()
),一開始開發者其實徹底不用關心。Android 1.6
也用上了嵌套滑動,老奶奶開心得合不攏嘴。但你們用着用着,新鮮感過去以後,也開始不知足了起來,因而就有了初版 NestedScrolling 機制的著名Bug:「慣性不連續」(回到小結)
什麼是慣性不連續?以下圖
簡單說就是:你在滑動內部 View 時快速擡起手指,內部 View 會開始慣性滑動,當內部 View 慣性滑動到本身頂部時便中止了滑動,此時外部的可滑動 View 不會有任何反應,即便外部 View 能夠滑動。 原本這個體驗也沒多大問題,但由於你手動滑動的時候,內部滑動到頂部時能夠接着滑動外邊的 View,這就造成了對比,有對比就有差距,有差距羣衆就不滿意了,你不能在慣性滑動的時候也把裏面的滑動傳遞到外面去嗎? 因此這個問題也不能算是 Bug,只是體驗沒有作到那麼好罷了。
其實 Google 不是沒有考慮過慣性,其中關於 fling 的4個 API 更是存在感十足地告訴你們,我就是來處理大家說的這檔子事的,但爲何仍是有 Bug 呢,那就不得不提這4個 API 的奇葩設計和用法了。
這四個 API 長這樣,看名字對應上 scroll 的4個 API 大概能知道是幹什麼的(但實際上有很大區別,見下文):
dispatchNestedPreFling
、dispatchNestedFling
onNestedPreFling
、onNestedFling
前面我在講述的時候默認是讓ns child
直接消費用戶快速擡起時產生的慣性滑動,這沒有什麼問題,由於咱們還在computeScroll
方法中把慣性引發的滑動也傳遞給了ns parent
,讓父子配合進行慣性滑動。 但實際上此時的NestedScrollView
是這麼寫的:
public boolean onTouchEvent(MotionEvent ev) {
...
case MotionEvent.ACTION_UP:
if (mIsBeingDragged) {
...
if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
flingWithNestedDispatch(-initialVelocity);
}
stopNestedScroll();
}
break;
...
}
private void flingWithNestedDispatch(int velocityY) {
final int scrollY = getScrollY();
final boolean canFling = (scrollY > 0 || velocityY > 0) && (scrollY < getScrollRange() || velocityY < 0);
if (!dispatchNestedPreFling(0, velocityY)) {
dispatchNestedFling(0, velocityY, canFling);
if (canFling) fling(velocityY);
}
}
public void fling(int velocityY) {
if (getChildCount() > 0) {
...
mScroller.fling(getScrollX(), getScrollY(), 0, velocityY, 0, 0, 0, Math.max(0, bottom - height), 0, height/2);
ViewCompat.postInvalidateOnAnimation(this);
}
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
... // 沒有關於把滑動分發給 ns parent 的邏輯
}
}
複製代碼
來讀一下其中的邏輯
ns child
並快速擡起手指產生慣性的時候,看flingWithNestedDispatch()
方法,ns child
會先問ns parent
是否消費此速度
ns parent
不消費,那麼將再次把速度交給ns parent
,而且告訴它本身是否有消費速度的條件*(根據系統類庫一向的寫法,若是ns child
消費這個速度,ns parent
都不會對這個速度作處理)*,同時本身在有消費速度的條件時,對速度進行消費mScroller
進行慣性滑動,可是在computeScroll()
中並無把滑動分發給 ns parent
stopNestedScroll()
解除與ns parent
的綁定,宣告此次協同合做到此結束那麼總結一下:
ns parent
有機會攔截處理慣性,它並不能在慣性滑動過程當中讓ns child
和ns parent
協同消費慣性引起的滑動,也就是實現不了前面人們指望的慣性連續效果,因此初版的開發者想用直接傳遞慣性的方式實現慣性連續可能不是個好主意
ns child
沒法進行滑動的時候起到必定的做用(雖然徹底能夠用滑動的協同消費機制替代),而在以後的版本中,這個做用基本也沒有被用到,它確實被滑動的協同消費機制替代了ns child
進行慣性滑動時,把滑動傳遞出來,就能夠了ns child
角色使用了嵌套滑動機制的系統控件,慣性相關的 API 和處理邏輯均可以保留,只要在computeScroll()
中把滑動用dispatchNestedPreScroll()
和dispatchNestedScroll()
方法分發給 ns parent
,再更改一下解除與ns parent
綁定的時機,放在 fling 結束以後ns child
View 能夠直接改,但系統提供的NestedScrollView
、RecyclerView
等控件,你就只能提個 issue 等官方修復了,不過也能夠拷貝一份出來本身改Google表示纔不想搭理這些人,給你用就不錯了哪來那麼多事兒?我還要忙着搞AI呢 直到兩年多後的2017年9月,Revision 26.1.0
才悄咪咪 (更新日誌裏沒有提,可是文檔的添加記錄裏能看到,後來發現做者本身卻是寫了篇博客說這事,說是Revision 26.0.0-beta2
時加的,跟文檔裏寫的不一致,不過這不重要) 更新了一版NestedScrollingChild2
和NestedScrollingParent2
,而且處理了初版中系統控件的Bug,這即是第二個版本的 NestedScrolling 機制了
來看看第二版是怎麼處理初版 Bug 的,大牛的救火思路果真比通常人要健壯。
首先看接口是怎麼改的:
ns child
在computeScroll
中分發滑動給ns parent
沒有問題(這是關鍵),可是我要區分開是用戶手指移動觸發的滑動仍是由慣性觸發的滑動(這是錦上添花)NestedScrollingChild
中滑動相關的 (確切地說是除了「fling相關、滑動開關」外的) 5個方法、全部NestedScrollingParent
中滑動相關的 (確切地說是除了「fling相關、獲取滑動軸」外的) 5個方法,都增長了一個參數type
,type
有兩個取值表明上述的兩種滑動類型:TYPE_TOUCH
、TYPE_NON_TOUCH
type
參數,而且對舊的接口作了個兼容,讓它們的type
是TYPE_TOUCH
改完了接口固然還要改代碼了,Helper 類首先要改
NestedScrollingChildHelper
裏邊原本持有了一個ns parent
域 mNestedScrollingParentTouch
,做爲綁定關係,第二版 又再加了一個ns parent
域 mNestedScrollingParentNonTouch
,爲何是兩個而不是公用一個,大概是避免對兩類滑動的生命週期有過於嚴格的要求,好比在 NestedScrollView
的實現裏,就是先開啓TYPE_NON_TOUCH
類型的滑動,而後關閉了 TYPE_TOUCH
類型的滑動,若是公用一個 ns parent
域,就作不到這樣了NestedScrollingChildHelper
裏邊主要就作了這一點額外的改動,其餘的改動都是增長參數後的常規變換,NestedScrollingParentHelper
裏就更沒有特別的變化了前面在分析初版 Bug 的時候說過「初版 NestedScrolling 機制自己是沒有問題的,有問題的是那些系統控件使用這個機制的方式不對」,因此此次改動最大的仍是那些使用了嵌套滑動機制的系統控件了,咱們就以 NestedScrollView
爲例來具體看看系統是怎麼修復 Bug、建議你們如今應該怎麼建立 ns child
角色的。 相同的部分不說了,在調用相關方法的時候要傳入 type
也不細說了,主要的變化基本出如今預期的位置:
public boolean onTouchEvent(MotionEvent ev) {
...
case MotionEvent.ACTION_UP:
if (mIsBeingDragged) {
...
if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
flingWithNestedDispatch(-initialVelocity);
}
stopNestedScroll(ViewCompat.TYPE_TOUCH);
}
break;
...
}
private void flingWithNestedDispatch(int velocityY) {
final int scrollY = getScrollY();
final boolean canFling = (scrollY > 0 || velocityY > 0) && (scrollY < getScrollRange() || velocityY < 0);
if (!dispatchNestedPreFling(0, velocityY)) {
dispatchNestedFling(0, velocityY, canFling);
fling(velocityY); // 華點
}
}
public void fling(int velocityY) {
if (getChildCount() > 0) {
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_NON_TOUCH);
mScroller.fling(getScrollX(), getScrollY(), 0, velocityY, 0, 0, Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 0);
mLastScrollerY = getScrollY();
ViewCompat.postInvalidateOnAnimation(this);
}
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
final int x = mScroller.getCurrX();
final int y = mScroller.getCurrY();
int dy = y - mLastScrollerY;
// Dispatch up to parent
if (dispatchNestedPreScroll(0, dy, mScrollConsumed, null, ViewCompat.TYPE_NON_TOUCH)) {
dy -= mScrollConsumed[1];
}
if (dy != 0) {
final int range = getScrollRange();
final int oldScrollY = getScrollY();
overScrollByCompat(0, dy, getScrollX(), oldScrollY, 0, range, 0, 0, false);
final int scrolledDeltaY = getScrollY() - oldScrollY;
final int unconsumedY = dy - scrolledDeltaY;
if (!dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, null, ViewCompat.TYPE_NON_TOUCH)) {
if (canOverscroll()) showOverScrollEdgeEffect();
}
}
ViewCompat.postInvalidateOnAnimation(this);
} else {
stopNestedScroll(ViewCompat.TYPE_NON_TOUCH);
}
}
複製代碼
computeScroll()
方法的代碼貼得比較多,由於它不只是此次Bug修復的主要部分,它仍是下一次Bug修復要改動的部分。 不過其實整個邏輯仍是很簡單的,符合預期,簡單說明一下:
UP
時候作的事情沒有變,仍是在這解除了與ns parent
的綁定,可是註明了類型是TYPE_TOUCH
flingWithNestedDispatch()
這個方法先不說fling()
方法中,調用startNestedScroll()
開啓了新一輪綁定,不過這時的類型變成了TYPE_NON_TOUCH
computeScroll()
方法中,但邏輯很清晰:對於每一個dy
,都會通過「parent -> child -> parent -> child」這個消費流程,從而實現了慣性連續,解決了 Bug最後的效果是這樣:
另外,從這版開始,View和 ViewGroup 裏的 NestedScrolling 機制就沒有更新過,一直維持着第一個版本的樣子。
看上去第二個版本改得很漂亮對吧,但此次改動其實又引入了兩個問題,至少有一個算是Bug,另外一個能夠說只是交互不夠好,不過這個交互不夠好的問題引入的緣由卻很是使人迷惑。
先說第一個問題:「二倍速」(回到小結)
NestedScrollView
中,RecyclerView
等類沒有這個問題,我極度懷疑它的引入是由於手滑NestedScrollView
跟以前的對比,你會很容易發現flingWithNestedDispatch()
中(在我貼出來的代碼裏),fling(velocityY)
前的if (canFling)
離奇消失了而後是第二個問題:「空氣馬達」(回到小結)
flingWithNestedDispatch()
中的這段代碼:其中的dispatchNestedPreFling()
大部分時候會返回false
,因而幾乎全部的狀況下,內部 View 都會經過fling()
方法啓動本身mScroller
這個小馬達computeScroll()
方法中,你會看到,(若是你不直接觸摸內部View) 除非等到馬達本身中止,不然沒有外力能讓它停下,因而它會一直向外輸出dispatchNestedPreScroll()
和dispatchNestedScroll()
ns child
是主動的一方,ns parent
徹底是被動的,ns parent
無法主動通知ns child
:啊我被摁住了,啊我撞牆了ns parent
並非沒辦法告知ns child
信息,經過方法的返回值和引用類型的參數,ns child
仍然能夠從ns parent
中獲取信息ns child
詢問ns parent
是否可以滑動,問題應該就解決了:若是ns parent
滑不動了,ns child
本身也滑不動,那就趕忙關閉馬達吧,ns parent
是否可以滑動不是有現成的方法嗎?dispatchNestedPreScroll()
會先讓ns parent
在ns child
以前進行滑動,並且滑動的距離被記錄在它的數組參數consumed
中,拿到數組中的值ns child
就能知道ns parent
是否在這時滑動了dispatchNestedScroll()
會讓ns parent
在ns child
以後進行滑動,它有沒有數組參數記錄滑動距離,它只有一個返回值記錄是否消費了滑動...不對,這個返回值不是記錄是否消費滑動用的,它表示的是ns parent
是否能順利聯繫上,若是能,就返回true
,並不關心它是否消費了滑動。在NestedScrollingChild Helper
中你也能看到這個邏輯的清晰實現,同時你也會看到在NestedScrollingParent2
中它對應的方法是void onNestedScroll()
,沒有返回值*(考慮過能不能經過dispatchNestedScroll()
中int[] offsetInWindow
沒被使用的數組位置來傳遞信息,結果也由於 parent 中對應的方法不帶這個參數而了結;並且ns parent
也沒法主動解除本身與ns child
的綁定,這條路也不通)*。總之,dispatchNestedScroll()
沒法讓ns child
得知ns parent
對事件的消費狀況,此路不通dispatchNestedScroll()
的消費結果直接放在ns child
的 View 中,用這個後門解決了Bug,但這種方式使用的侷限比較大,並且下面要介紹的最新的第三版已經修復了這個問題,我就很少寫了)第二版的 Bug 雖然比初版的嚴重,但好像沒有太多人知道,可能這種使用場景仍是沒有那麼多。 不過期隔一年多,Google 終因而意識到了這個問題,在最近也就是2018年11月5日androidx.core 1.1.0-alpha01
的更新中,給出了最新的修復——NestedScrollingChild3
和NestedScrollingParent3
,以及一系列系統組件也陸續進行了更新。
這就是第三個版本的 NestedScrolling 機制了,這個版本確實對上面兩個 Bug 進行了處理,但惋惜的是,第二個 Bug 並無修理乾淨 (爲 Google 大佬獻上一首つづく,期待第四版) (在本文快要完成的時候正好看到新一任消防員在18年12月3日發了條 twitter 說已經發布了第三版,結果評論區你們已經在歡樂地期待 NestedScrollingChild42
NestedScrollingChildX
NestedScrollingParentXSMax
NestedScrollingParentFinalFinalFinal
NestedScrollingParent2019
了 )
繼續來看看在這個版本中,大佬是怎麼救火的
照例先看接口,一看接口的改動你可能就笑了,真的是哪裏不通改哪裏
NestedScrollingChild3
中,沒有增長方法,只是給dispatchNestedScroll
方法增長了一個參數int[] consumed
,而且把它的boolean
返回值改爲了void
,有了能獲取更詳細信息的途徑,天然就不須要這個boolean
了NestedScrollingParent3
一樣只是改了一個方法,給onNestedScroll
增長了int[] consumed
參數(它返回值就是 void
,沒變)下面是NestedScrollingChild3
中的對比:
// 2
boolean dispatchNestedScroll( int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow, @NestedScrollType int type );
// 3
void dispatchNestedScroll( int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow, @NestedScrollType int type, @NonNull int[] consumed // 這個 );
複製代碼
再看下 Helper ,NestedScrollingChildHelper
除了適配新的接口基本沒有改動,NestedScrollingParentHelper
也只是加強了一點邏輯的嚴謹性(大概是被review了233)
最後看用法,仍是經過咱們的老朋友NestedScrollView
來看,改動部分跟預期基本一致:
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type, @NonNull int[] consumed) {
final int oldScrollY = getScrollY();
scrollBy(0, dyUnconsumed);
final int myConsumed = getScrollY() - oldScrollY;
if (consumed != null) consumed[1] += myConsumed; // 就加了這一句
final int myUnconsumed = dyUnconsumed - myConsumed;
mChildHelper.dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null, type, consumed);
}
// ---
// onTouchEvent 中邏輯沒有變化
private void flingWithNestedDispatch(int velocityY) {
if (!dispatchNestedPreFling(0, velocityY)) {
dispatchNestedFling(0, velocityY, true);
fling(velocityY); // fling 中的邏輯沒有變化
}
}
@Override
public void computeScroll() {
if (mScroller.isFinished()) return;
mScroller.computeScrollOffset();
final int y = mScroller.getCurrY();
int unconsumed = y - mLastScrollerY;
// Nested Scrolling Pre Pass
mScrollConsumed[1] = 0;
dispatchNestedPreScroll(0, unconsumed, mScrollConsumed, null, ViewCompat.TYPE_NON_TOUCH);
unconsumed -= mScrollConsumed[1];
final int range = getScrollRange();
if (unconsumed != 0) {
// Internal Scroll
final int oldScrollY = getScrollY();
overScrollByCompat(0, unconsumed, getScrollX(), oldScrollY, 0, range, 0, 0, false);
final int scrolledByMe = getScrollY() - oldScrollY;
unconsumed -= scrolledByMe;
// Nested Scrolling Post Pass
mScrollConsumed[1] = 0;
dispatchNestedScroll(0, scrolledByMe, 0, unconsumed, mScrollOffset, ViewCompat.TYPE_NON_TOUCH, mScrollConsumed);
unconsumed -= mScrollConsumed[1];
}
// 處理最後還有 unconsumed 的狀況
if (unconsumed != 0) {
if (canOverscroll()) showOverScrollEdgeEffect();
mScroller.abortAnimation(); // 關停小馬達
stopNestedScroll(ViewCompat.TYPE_NON_TOUCH);
}
if (!mScroller.isFinished()) ViewCompat.postInvalidateOnAnimation(this);
}
複製代碼
修改最多的仍是computeScroll()
,不過其餘地方也有些變化,簡單說明一下:
onNestedScroll()
增長了記錄距離消耗的參數,因此ns parent
就須要把這個數據記錄上而且繼續傳遞給本身的ns parent
flingWithNestedDispatch()
是以前有蜜汁 Bug 的方法,原本個人預期是恢復初版的寫法,也就是把fling(velocityY)
前的if (canFling)
加回來,結果這下倒好,連canFling
也不判斷了,dispatchNestedFling(0, velocityY, true)
直接傳true
,fling(velocityY)
始終調用。這意味着什麼呢?須要結合大部分View的寫法來看
API 28
的代碼你就會看到:
onNestedPreFling()
方法,除了ResolverDrawerLayout
會在某些狀況下消費fling並返回true
,以及CoordinatorLayout
會象徵性地問一遍本身孩子們的Behavior
,其它的寫法都是直接返回false
onNestedFling(boolean consumed)
方法,全部的寫法都是,只要consumed
爲true
,就什麼都不會作,這種作法也很是天然computeScroll()
,它基本把咱們在討論怎麼修復第二版中 Bug 時的思路實現了:由於能從dispatchNestedPreScroll()
和dispatchNestedScroll()
得知ns parent
消耗了多少這一次分發出去的滑動距離,同時也有本身消耗了多少,二者一合計,若是還有沒消耗的滑動距離,那確定不管內外都滑到頭了,因而就該果斷就把小馬達關停如今的效果是這樣的,能看到第二版中的Bug確實解決了
那麼爲何我還說第二個 Bug 沒有解決完全呢?
DOWN
事件的處理相對第二版沒有變化,它沒有加入觸摸外部 View 後關閉內部 View 馬達的機制,更確切地說是沒有加入「觸摸外部 View 後阻止對內部 View 傳遞過來的滑動進行消費的機制」雖然現象與「空氣馬達」相似,但仍是按照慣例給它也起個好聽的新名字,就叫:...「摁不住」吧 (回到小結)
實際體驗跟分析結果同樣這樣,當經過滑動內部 View 觸發外部 View 滑動時,你沒法經過觸摸外部 View 把它停下來,外部 View 比較長的時候容易復現,以下圖(換了一個方向)
不過這個問題只有能夠響應觸摸的ns parent
須要考慮,能夠響應觸摸的ns parent
主要就是NestedScrollView
了,因此這個問題主要仍是NestedScrollView
的問題。並且它也跟機制無關,只是NestedScrollView
的用法不對,因此前面說的會有第四版 NestedScrolling 機制可能性也不大,大概只會給NestedScrollView
上個普通的更新吧(順手給 Google 大佬遞了瓶 可樂)
而這個問題本身改也很是好改,只須要在DOWN
事件後能給ns child
反饋本身被摁住了就行,能夠用反射,或是直接把NestedScrollView
挪出來改,關鍵代碼以下
private boolean mIsBeingTouched = false;
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
mIsBeingTouched = true;
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
mIsBeingTouched = false;
break;
}
return super.onTouchEvent(ev);
}
private void onNestedScrollInternal(int dyUnconsumed, int type, @Nullable int[] consumed) {
final int oldScrollY = getScrollY();
if (!mIsBeingTouched) scrollBy(0, dyUnconsumed); // 只改了這一句
final int myConsumed = getScrollY() - oldScrollY;
if (consumed != null) {
consumed[1] += myConsumed;
}
final int myUnconsumed = dyUnconsumed - myConsumed;
childHelper.dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null, type, consumed);
}
複製代碼
我把用反射改好的放在這裏了,你也能夠直接使用 改完以後效果以下:
歷史終於講完了,小結一下(回去看詳細歷史)
Android 5.0( API 21)
中的 View 和 ViewGroup 中加入了第一個版本的 NestedScrolling 機制,此時可以經過啓用嵌套滑動,讓嵌套的ScrollView
不出現交互問題,但這個機制只有 API 21 以上才能使用NestedScrollingChild
、NestedScrollingParent
)和兩個 Helper (NestedScrollingChildHelper
、NestedScrollingParentHelper
),而且用這套新的機制重寫了一個默認啓用嵌套滑動的NestedScrollView
,並把它們都放入了Revision 22.1.0
的v4 support library
,讓低版本的系統也能使用嵌套滑動機制,不過此時的初版機制有「慣性不連續」的 BugRevision 26.1.0
的v4 support library
中發佈了第二個版本的 NestedScrolling 機制,增長了接口NestedScrollingChild2
、NestedScrollingParent2
,主要是給本來滑動相關的方法增長了一個參數type
,表示了兩種滑動類型TYPE_TOUCH
、TYPE_NON_TOUCH
。而且使用新的機制重寫了嵌套滑動相關的控件。此次更新解決了第一個版本中「慣性不連續」的Bug,但也引入了新的Bug:「二倍速」(僅NestedScrollView
)和「空氣馬達」AndroidX
家族的 NestedScrolling 機制更新了第三個版本,具體版本是androidx.core 1.1.0-alpha01
,增長了接口NestedScrollingChild3
、NestedScrollingParent3
,改動只是給原來的dispatchNestedScroll()
和onNestedScroll()
增長了int[] consumed
參數。而且後續把嵌套滑動相關的控件用新機制進行了重寫。此次更新解決了第二個版本中 NestedScrollView
的「二倍速」Bug,同時指望解決「空氣馬達」Bug,可是沒有解決完全,還遺留了「摁不住」Bug因此前面的問題你們應該都有了答案:
(轉載請註明做者:RubiTree,地址:blog.rubitree.com )
第二節中其實已經講過了實踐,而且提供了實現 ns child
的模板。 這裏我準備用剛發現的一個更有實際意義的例子來說一下 ns parent
的實現,以及系統庫中 ns child
的幾個細節。
這個例子是「懸停佈局」 你叫它粘性佈局、懸浮佈局、摺疊佈局都行,總之它理想的效果應該是這樣:
用文字描述是這樣:
在當前這個時間點(2019.1.13),這個例子還有很多實際意義,由於它雖然是比較常見的一個交互效果,但如今市場上的主流APP,竟然是這樣的...(餓了麼v8.9.3)
這樣的...(知乎v5.32.2) 這樣的...(騰訊課堂v3.24.0.5) 這樣的...(嗶哩嗶哩v5.36.0)先無論它們是否是用 Native 實現的,只看實現的效果
其餘還有一些千奇百怪的 Bug 就不舉例了。 因此,就讓咱們來看看,這個功能實現起來是否是真有那麼難。
若是內容區只有一個 Tab 頁,一種簡單直接的實現思路是:頁面整個就是一個滑動控件,懸停區域會在滑動過程當中不斷調整本身的位置,實現懸停的效果。 它的實現很是簡單,效果也徹底符合要求,不舉例了,能夠本身試試。
但這裏的需求是有多個 Tab 頁,它用一整個滑動控件的思路是沒法實現的,須要用多個滑動控件配合實現
在瞭解 NestedScrolling 機制以前,你可能以爲這個需求不太對勁,確實,從大的角度看,用戶的一次觸摸操做,卻讓多個 View 前後對其進行消費,它違背了事件分發的原則,也超出了 Android 觸摸事件處理框架提供的功能:父 View 沒用完的事件子 View 繼續用,子 View 沒用完的事件父 View 繼續用
但具體到這個需求中
CoordinatorLayout
了,它就是用來幫助開發者去實現他們精心設計的多個 View 消費同一個事件流的效果的NestedScrolling
機制實現。另外CoordinatorLayout
讓多個滑動控件配合對同一個事件流進行消費也是利用NestedScrolling
機制OK,既然需求提得沒問題,並且咱們也能實現,那下面就來看看具體要怎麼實現。
可能有同窗立刻就舉手了:我知道我知道,用CoordinatorLayout
! 對,當前這個效果最多見的實現方式就是使用基於CoordinatorLayout
的AppBarLayout
全家桶,這是它的自帶效果,經過簡單配置就能實現,並且還附送更多其餘特效,很是酷炫,前面看到的效果比較好的嗶哩嗶哩視頻詳情頁就是用它實現的。 而AppBarLayout
實現這個功能的方式實際上是也使用了CoordinatorLayout
提供的NestedScrolling
機制(雖然實現的具體方法跟上面的分析有些區別,但並不重要,感興趣的同窗能夠看AppBarLayout
的Behavior
),若是你嫌棄AppBarLayout
全家桶過重了,只想單獨實現懸停功能,如前文所述,你也能夠直接使用NestedScrolling
機制去實現。
這裏就直接使用NestedScrolling
機制來實現出一個相似嗶哩嗶哩這樣正常一些的懸停佈局。
用NestedScrolling
機制一想,你會發現實現起來很是簡單,上面的分析過程在機制中直接就有對應的接口,咱們只要實現一個符合要求的 ns parent
就行了,NestedScrolling
機制會自動管理 ns parent
與 ns child
的綁定和 scroll 的傳遞,即便 ns child
與 ns parent
相隔好幾層 View。
我把要實現的 ns parent
叫作 SuspendedLayout
,其中的關鍵代碼以下,它剩下的代碼以及佈局和頁面代碼就不寫出來了,能夠在這裏查看(簡單把第一個 child view 做爲 Header,第二個 child view 會天然懸停)。
override fun onNestedScroll(target: View, dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int, type: Int, consumed: IntArray) {
if (dyUnconsumed < 0) scrollDown(dyUnconsumed, consumed)
}
override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) {
if (dy > 0) scrollUp(dy, consumed)
}
/*-------------------------------------------------*/
private fun scrollDown(dyUnconsumed: Int, consumed: IntArray?) {
val oldScrollY = scrollY
scrollBy(0, dyUnconsumed)
val myConsumed = scrollY - oldScrollY
if (consumed != null) {
consumed[1] += myConsumed
}
}
private fun scrollUp(dy: Int, consumed: IntArray) {
val oldScrollY = scrollY
scrollBy(0, dy)
consumed[1] = scrollY - oldScrollY
}
override fun scrollTo(x: Int, y: Int) {
val validY = MathUtils.clamp(y, 0, headerHeight)
super.scrollTo(x, validY)
}
複製代碼
這麼快就實現了,效果很是完美,與嗶哩嗶哩幾乎同樣:
但效果同樣好也同樣壞,嗶哩嗶哩的那個容易誤操做的問題這裏也有。 先看看爲何會出現這樣的問題?
ViewPager
攔截了事件,也就是 ns child
沒有及時「申請外部不攔截事件流」,因而到 NestScrollView
和 RecyclerView
中查看,問題其實就出在前面描述的ns child
在 onTouchEvent()
中的邏輯上 ns child
會在判斷出用戶在滑動後「申請外部不攔截事件流」,但 onTouchEvent()
中又在判斷出用戶在滑動前就把滑動用 dispatchNestedPreScroll()
方法傳遞給了 ns parent
,因而你就會看到,明明已經識別出我在上下滑動ns child
了,並且已經滑了一段距離,竟然會突然切換成滑動 ViewPager
因此這個問題要怎麼修復呢?
NestScrollView
代碼拷貝出來,並把其中的 dispatchNestedPreScroll()
方法放在判斷出滑動以後進行調用,確實解決了問題parent.requestDisallowInterceptTouchEvent(true)
便可,完整代碼見此,其中關鍵代碼以下:private int downScreenOffset = 0;
private int[] offsetInWindow = new int[2];
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
downScreenOffset = getOffsetY();
}
if (ev.getActionMasked() == MotionEvent.ACTION_MOVE) {
final int activePointerIndex = ev.findPointerIndex(getInt("mActivePointerId"));
if (activePointerIndex != -1) {
final int y = (int) ev.getY(activePointerIndex);
int mLastMotionY = getInt("mLastMotionY");
int deltaY = mLastMotionY - y - (getOffsetY() - downScreenOffset);
if (!getBoolean("mIsBeingDragged") && Math.abs(deltaY) > getInt("mTouchSlop")) {
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
setBoolean("mIsBeingDragged", true);
}
}
}
return super.onTouchEvent(ev);
}
private int getOffsetY() {
getLocationInWindow(offsetInWindow);
return offsetInWindow[1];
}
複製代碼
這裏有個細節值得一提:在計算deltaY
時不僅是用mLastMotionY - y
,還減去了(getOffsetY() - downScreenOffset)
,這裏的offsetInWindow
其實也出如今 NestedScrolling 機制裏的dispatchNestedScroll()
等接口中
offsetInWindow
的做用很是關鍵,由於當 ns child
驅動 ns parent
滑動時,ns child
其實也在移動,此時ns child
中獲取到的手指觸發的motion event
中 x
和y
值是相對ns child
的,因此此時若是直接使用y
值,你會發現y
值幾乎沒有變化,這樣算到的deltaY
也會沒有變化,因此須要再獲取ns child
相對窗口的偏移,把它算入deltaY
,才能獲得你真正須要的deltaY
ViewPager
爲何會在豎直滑動那麼遠以後還能對橫滑進行攔截,也是這個緣由,它獲取到的deltaY
其實很小改完以後的效果以下,能看到解決了問題:
RecyclerView
等其餘的ns child
若是須要的話,也能夠作相似的改動(不過這裏的反射代碼對性能有所影響,建議實現上作一些優化)
(轉載請註明做者:RubiTree,地址:blog.rubitree.com )
若是你沒有跳過地看到這裏,關於 NestedScrolling 機制,我相信如今不管是使用、仍是原理、甚至八卦歷史,你都瞭解得一清二楚了,不然我只能懷疑你的個人語文老師表達水平了。
而關於代碼的設計,你大概也能學到一點,Google 工程師三入火場英勇救火的身影應該給你留下了深入的印象。
最後關於使用多說兩句:
NestedScrollView
,認爲第三版的 Bug 也會影響到你寶貴而敏感的用戶,那不如試試 implementation 個人項目 :D最後的最後,G 家的消防員都有顧不過來的時候,更況且是本菜雞,本文內容確定會有疏漏和不當之處,歡迎你們提 issue 啦~
(以爲寫得好的話,不妨點個贊再走呀~ 給做者一點繼續寫下去的動力)