堅持原創日更,短平快的 Android 進階系列,敬請直接在微信公眾號搜尋:nanchen,直接關注並設為星標,精彩不容錯過。
在 Android 開發中,滑動衝突總是我們一個無法避免的話題。而對於解決方案卻是眾說紛紜。比如 RecyclerView
巢狀 RecyclerView
,直接通過相關方法禁掉內部 RecyclerView
的滑動;ScrollView
巢狀 RecyclerView
直接把 ScrollView
替換為 NestedScrollView
等等。但我們今天要說的是在自定義 View 中遇到滑動衝突時,我們又應該如何處理呢?
當然,今天的話題需要 View 的事件分發機制做理論前提,還不瞭解 View 的事件分發機制的小夥伴可以移步我之前面試系列的一篇文章:面試系列:講講 Android 的事件分發機制。
簡單介紹 View 的時間分發機制
當然,這裡也可以簡單地提一下,基本的流程就是下面的虛擬碼。
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
if (onInterceptTouchEvent(ev)) {
consume = onTouchEvent(ev);
}else{
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
當一個 ViewGroup
接收到一個事件的時候,首先會呼叫 dispatchTouchEvent()
方法進行事件分發,如果 onInterceptTouchEvent()
返回 true
,則代表當前 View 會攔截事件,則直接回撥 onTouchEvent()
方法進行事件處理。如果不攔截,則直接回撥子 View 的 dispatchTouchEvent()
方法,如此反覆,一直到最裡面的子 View。
當一個點選事件產生後,它的傳遞過程遵循以下順序:Activity
=> Window
=> View
,即事件總是先傳遞給 Activity
,Activity
再傳遞給 Window
,最後 Window
再傳遞給頂層 DocorView
,然後遵循上面的方式一直在最裡層 View
。
而處理事件則從最裡層 View
不斷回傳給自己的外層 View
,如果一直沒有 View
進行處理,則直接會回傳到 Activity
中。
onTouchEvent()
返回true
代表自己要處理。
既然都提了這麼一點,也就突然想給出一些結論,參考自 Android 開發藝術探索:
- 同一個事件序列是指從手指接觸螢幕(ACTION_DOWN)的那一刻起,到手指離開螢幕(ACTION_UP)的那一刻結束,中間含不定數量的
ACTION_MOVE
事件。- 某個 View 一旦決定攔截事件,那麼這一個事件序列都只能由它處理,並且它的
onInterceptTouchEvent()
方法也不會再呼叫。換句話說,比如一個 ViewGroup 裡面有數個子 View,一旦 ACTION_DOWN 事件從 Activity 傳到這個 ViewGroup 被其攔截,則後續的 MOVE 和 UP 等事件也不會傳遞到裡面的子 View 中。- 如果一個 View 一旦開始處理事件,如果它不消耗 ACTION_DOWN 事件,即
onTouchEvent()
返回為 false,那麼同一事件序列中的其他時間也不會再交給它處理,直接會呼叫其父 View 的onTouchEvent()
。- 如果 View 不消耗除 ACTION_DOWN 以外的其他事件,那麼這個點選事件會消失,此時父元素的
onTouchEvent()
並不會被呼叫,並且當然 View 可以持續收到後續的事件,最終這些消失的點選事件會傳遞給Activity
處理。- ViewGroup 預設不攔截事件,View 沒有
onInterceptTouchEvent()
方法,一旦有事件傳遞給它,則直接會呼叫onTouchEvent()
,並且起預設都會消耗掉事件。除非它是不可點選的(即clickable
和longClickable
均為false
)。View 的longClickable
預設都為false
,而clickable
分情況,比如Button
預設為true
,TextView
預設為false
。- View 的
enable
屬性不會影響onTouchEvent()
的預設返回值,哪怕一個View
是disable
狀態的,只要它的clickable
或者longClickable
有一個為true
,那麼它的onTouchEvent()
就會返回true
。requestDisallowInterceptTouchEvent()
可以在子元素中干預父元素的事件分發過程,但是無法干預 ACTION_DOWN 事件。- 事件優先順序:
setOnTouchListener()
=>onTouchEvent()
=>onClickListener()
一不小心發現還是挺多的,當然這些都是結論,具體可以跟著 面試系列:講講 Android 的事件分發機制 進行原始碼流程探討,你會發現上面的結論很容易得到。
處理自定義 View 中的滑動衝突
對於大多數 Android 開發來說,處理滑動衝突好像很難,但實戰一下又發現,好像也挺簡單,因為這個實際上是有套路可循的。基本就兩種方案:外部攔截法 && 內部攔截法。
外部攔截法
所謂外部攔截法,顧名思義,就是直接在父容器中直接攔截掉我們的滑動事件,讓其不能進入到子元素中,這似乎和我們 RecyclerView
巢狀 RecyclerView
時禁用內部 RecyclerView
滑動有那麼一絲相似之處,就是內部不處理就完事兒了。但細細品來又完全不一樣,這裡的外部攔截法會讓內部元素根本就收不到滑動事件。
這種方法明顯非常適合我們上面講的事件分發機制。我們在接收 ACTION_MOVE
事件的時候,直接通過使 onInterceptTouchEvent()
方法返回 true
來直接攔截掉事件就可以了,虛擬碼想必大家也知道了:
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
ev?.run {
if (action == MotionEvent.ACTION_MOVE && 父容器需要點選事件){
return true
}
}
return super.onInterceptTouchEvent(ev)
}
程式碼很簡單,我們僅僅需要在事件 ACTION_MOVE
時去處理我們的邏輯就好了,當滿足我們的邏輯的時候,就攔截掉 ACTION_MOVE
事件給自己處理。
至於為什麼不去攔截 ACTION_DOWN
和 ACTION_UP
,想必大家也清楚了。上面說了,如果攔截了 ACTION_DOWN
事件,那後續的 ACTION_MOVE
、ACTION_UP
等其它事件均不會在呼叫 onInterceptTouchEvent()
方法,會直接交給當前容器處理。而如果我們攔截掉 ACTION_UP
的話,肯定會導致子元素的點選事件無法被處理,因為大家肯定都知道一個點選事件從 ACTION_DOWN
開始,從 ACTION_UP
結束,二者缺一不可。
內部攔截法
內部攔截法相對外部攔截法會複雜一些,所以我們通常來說,都更加推薦用外部攔截法進行處理。不過,內部攔截法依然有著它非常重要的地位,具體情況有可能會遇到。
內部攔截法的話,需要 requestDisallowInterceptTouchEvent()
方法的支援,這個方法是幹什麼的呢?顧名思義,請求是否不允許攔截事件,其接收一個 boolean
引數,表示是否不允許攔截。
我們直接重寫子元素的 dispatchTouchEvent()
方法,得到虛擬碼如下:
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
ev?.run {
when(action){
MotionEvent.ACTION_DOWN -> parent.requestDisallowInterceptTouchEvent(true)
MotionEvent.ACTION_MOVE ->{
if(滿足需要讓外部容器攔截事件){
parent.requestDisallowInterceptTouchEvent(false)
}
}
}
}
return super.dispatchTouchEvent(ev)
}
想必程式碼也是非常簡單易懂的,我們給父容器的 requestDisallowInterceptTouchEvent()
傳遞的引數代表是否不允許其攔截事件,當引數為 true
的時候代表不允許攔截,為 false
的時候代表攔截。所以看起來和外部攔截法也就如出一轍了。
不過僅僅有這點修改還不夠,我們通過前面的理論基礎知道,當我們的父容器攔截掉 ACTION_DOWN
事件的時候,所有的事件都無法再傳遞到子元素中,自然也就不會呼叫上面我們寫的 dispatchTouchEvent()
方法了。所以我們在內部攔截法的時候還需要重寫父容器的 onInterceptTouchEvent()
方法。
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
ev?.run {
if (action == MotionEvent.ACTION_DOWN){
return false
}
}
return super.onInterceptTouchEvent(ev)
}
至此,基本介紹了兩種處理滑動衝突的解決方案,在自定義 View 的時候結合實際場景也就可以得心應手了。
除了滑動衝突,滑動處理也是一項非常有意思的工作,感興趣的可以可以參考 NestedScrollingParent2 和 NestedScrollingChild2 喲。