Android View 事件分發(fā)機(jī)制詳解
Android開發(fā),觸控?zé)o處不在。對于一些 不咋看源碼的同學(xué)來說,多少對這塊都會有一些疑惑。View事件的分發(fā)機(jī)制,不僅在做業(yè)務(wù)需求中會碰到這些問題,在一些面試筆試題中也常有人問,可謂是老生常談了。我以前也看過很多人寫的這方面的文章,不是說的太啰嗦就是太模糊,還有一些在細(xì)節(jié)上寫的也有爭議,故再次重新整理一下這塊內(nèi)容,十分鐘讓你搞明白View事件的分發(fā)機(jī)制。
說白了這些觸控的事件分發(fā)機(jī)制就是弄清楚三個方法,dispatchTouchEvent(),OnInterceptTouchEvent(),onTouchEvent(),和這三個方法與n個ViewGroup和View堆疊在一起的問題,再復(fù)雜的結(jié)構(gòu)都能拆分成1個ViewGroup+1個View。
其實ViewGroup和View都是大同小異,View只是沒有了子容器,自然不存在攔截問題,dispatch也很簡單,所以弄明白了ViewGroup其實就懂的差不多了。
三個關(guān)鍵方法
public boolean dispatchTouchEvent(MotionEvent ev)
View/ViewGroup處理事件分發(fā)的發(fā)起者,View/ViewGroup接收到觸控事件最先調(diào)起的就是這個方法,然后在該方法中判斷是否處理攔截或是將事件分發(fā)給子容器
public boolean onInterceptTouchEvent(MotionEvent ev)
ViewGroup專用,通過該方法可以達(dá)到控件事件的分發(fā)方向,一般可以在該方法中判斷將事件給ViewGroup獨(dú)吞或是它繼續(xù)傳遞給子容器,是處理事件沖突的最佳地點(diǎn)
public boolean onTouchEvent(MotionEvent event)
觸控事件的真正處理者,最后每個事件都會在這里被處理
核心問題
時間分發(fā)機(jī)制的難點(diǎn)在哪,我覺得難的地方以下幾點(diǎn):三個方法調(diào)用規(guī)則,確定處理事件的對象以及事件沖突的解決方法。
事件傳遞規(guī)則
一般一次點(diǎn)擊會有一系列的MotionEvent,可以簡單分為:down->move->….->move->up,當(dāng)一次event分發(fā)到ViewGroup時,上述三個方法之間的 ViewGroup中調(diào)用順序可以用一段簡單代碼表示
MotionEvent ev;//down or move or up or others... viewgroup.dispatchTouchEvent(ev); public boolean dispatchTouchEvent(MotionEvent ev){ boolean isConsumed = false; if(onInterceptTouchEvent(ev)){ isCousumed = this.onTouchEvent(ev); }else{ isConsumed = childView.dispatchTouchEvent(ev); } return isConsumed; }
返回結(jié)果true表示事件被處理了,返回false表示沒有處理。上面的代碼通俗易懂,看起來也很簡單,一句話就能概括,ViewGroup收到事件后調(diào)用dispatch,在dispatch中先檢查是否要攔截,若攔截則ViewGroup吃掉事件,否則交給有處理能力的子容器處理。
不過,簡單歸簡單,寫成這樣只是為了方便理解,ViewGroup的事件處理流程當(dāng)然沒這么簡單,這里忽略了很多細(xì)節(jié)問題,接下來繼續(xù)補(bǔ)充。回到上面說的,一系列事件我們經(jīng)常處理的一般都是一個down,多個move和一個up,光靠上面的偽代碼是沒辦法把這些問題都給完美解決,直接來看ViewGroup的dispatchTouchEvent。
onInterceptTouchEvent調(diào)用條件
final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) { intercepted = onInterceptTouchEvent(ev); ev.setAction(action); // restore action in case it was changed } else { intercepted = false; } } else { // There are no touch targets and this action is not an initial down // so this view group continues to intercept touches. intercepted = true; }
解釋一下上面的代碼,看起來好像很簡單,但真的很簡單嗎。。在解釋之前先說一下intercepted代表的含義,intercepted == false表示父容器ViewGroup暫時不攔截事件,事件有機(jī)會傳給子View處理,返回true表示父容器直接攔截了該系列事件,后續(xù)不會再傳遞給子View了。子View想獲取事件只能讓該值為false
onInterceptTouchEvent調(diào)用返回false(返回false才能傳遞給子View,對應(yīng)到上面?zhèn)未a的else中的內(nèi)容,叫事件傳遞到子容器需要滿足的內(nèi)容更好理解一些)需要滿足兩個條件中的任意一個就有可能觸發(fā)(當(dāng)然只是有可能):
一個是在down的時候,另一個就是mFirstTouchTarget!=null,那mFirstTouchTarget何時不為空,有興趣的同學(xué)可以看ViewGroup中的addTouchTarget這個方法的調(diào)用時機(jī),mFirstTouchTarget就是在這里賦值的,源碼太長我就不貼了。
mFirstTouchTarget是用來保存ViewGroup中消費(fèi)了ACTION_DOWN事件的子View,即在上面?zhèn)未a中child.dispatchTouchEvent(ev)在ACTION_DOWN的時候返回true的View,只要有子View的dispatch在ACTION_DOWN返回true,就不會為null(這個賦值過程只發(fā)生在ACTION_DOWN里,如果子ViewACTION__DOWN不給它賦值后面序列的事件就不會再),反之,若無子View處理,該對象即為null。當(dāng)然,滿足了上述兩個條件還不行,必須還要滿足!disallowIntercept。
disallowIntercept這個變量很有意思,它的值主要受FLAG_DISALLOW_INTERCEPT這個標(biāo)記影響,這個值可以被ViewGroup的子View設(shè)置,ViewGroup的子View如果調(diào)用了requestDisallowInterceptTouchEvent這個方法,會改變FLAG_DISALLOW_INTERCEPT,導(dǎo)致disallowIntercept這個值就是ture了,這種情況會跳過intercept,導(dǎo)致攔截失效。
但這事還沒了,F(xiàn)LAG_DISALLOW_INTERCEPT這個標(biāo)記有一個重置的機(jī)制,查看ViewGroup源碼可以看到,在處理MotionEvent.ACTION_DOWN的時候會重置這個標(biāo)記導(dǎo)致disallowIntercept失效,是不是喪心病狂,上面的一段這么簡單的代碼有這么多幺蛾子,這里還能得到一個結(jié)論,ACTION_DOWN的時候肯定可以執(zhí)行onInterceptTouchEvent的。
所以攔截的intercepted很重要,能影響到底是讓ViewGroup還子View處理這個事件。
上面的兩個有可能觸發(fā)攔截的條件說完了,那么當(dāng)兩個條件都不滿足的話就不會再調(diào)用攔截了(攔截很重要,一般ViewGroup都返回false這樣能把事件傳遞給子View,如果在ACTION_DOWN時不能走到OnInterceptTouchEvent并返回false告訴ViewGroup不要攔截,則事件再也不能傳給子View了,所以攔截一般都是要走到的,而且一般都是返回false這樣能讓子View有機(jī)會處理),這種情況一般都是在ACTION_DOWN處理完之后沒有子View當(dāng)接盤俠消費(fèi)ACTION_DOWN以及后續(xù)事件,從上面的偽代碼可以看出來,這時候ViewGroup自己就很被動了,需要自己來調(diào)用onTouchEvent來處理,這鍋就自己背了。
再繼續(xù)說一下mFirstTouchTarget和intercepted是怎么影響事件方向的??丛创a:
if (!canceled && !intercepted) { .... if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { .... for(child : childList){ if(!child satisfied condition....){ continue; } newTouchTarget = addTouchTarget(child, idBitsToAssign);//在這里給mFirstTouchTarget賦值 } } }
可以在這里看到intercepted為false在ACTION_DOWN里才能給上面說過的mFirstTouchTarget賦值,只有mFirstTouchTarget不為空才能讓后續(xù)事件傳遞給子View,否則根據(jù)上上面說的代碼后續(xù)事件只能給父容器處理了。
mFirstTouchTarget就是我們后續(xù)事件傳遞的對象,很容易理解,如果在ACTION_DOWN中沒有確定這個對象,則后續(xù)事件不知道傳遞給誰自然就交給父容器ViewGroup處理了,真正處理事件傳遞的方法是dispatchTransformedTouchEvent,再看源碼:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { final boolean handled; // Canceling motions is a special case. We don't need to perform any transformations // or filtering. The important part is the action, not the contents. final int oldAction = event.getAction(); if (cancel || oldAction == MotionEvent.ACTION_CANCEL) { event.setAction(MotionEvent.ACTION_CANCEL); if (child == null) { handled = super.dispatchTouchEvent(event); } else { handled = child.dispatchTouchEvent(event); } event.setAction(oldAction); return handled; } }
看到?jīng)],只要參數(shù)里傳的child為空,則ViewGroup調(diào)用super.dispatchTouchEvent(event),super是誰,ViewGroup繼承自View,當(dāng)然是View咯,View的dispatch調(diào)用的誰?當(dāng)然是自己的onTouchEvent(后面會說),所以這個最后還是調(diào)用了ViewGroup自己的onTouchEvent。
那么當(dāng)child!=null的時候呢,調(diào)用的是child的dispatchTouchEvent(event),如果child可能是View也可能是ViewGroup,如果是ViewGroup則繼續(xù)按照上面的偽代碼執(zhí)行事件分發(fā),如果也是View則調(diào)用自己的onTouchEvent。
所以,說到底事件到底給誰處理,還是和傳進(jìn)來的child有關(guān),那這個方法在哪里調(diào)用的呢,繼續(xù)看:
if (mFirstTouchTarget == null) { // No touch targets so treat this as an ordinary view. handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } else { ... dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits) }
這就是為什么mFirstTouchTarget能影響事件分發(fā)的方向的原因。就這樣,整個偽代碼的流程是不是很清楚了。
這里需要多說兩句,在上上面代碼流程中,intercepted決定了這個事件會不會調(diào)用ViewGroup的onTouchEvent,當(dāng)intercepted為true則后續(xù)流程會調(diào)用ViewGroup的onTouchEvent,仔細(xì)看上面的代碼能發(fā)現(xiàn),只有兩種情況為ture:一是調(diào)用了InterceptTouchEvent把事件攔截下來,另一個就是沒有一個子View能夠消費(fèi)ActionDown。只有這兩種情況父容器ViewGroup才會自己處理
那么問題來了,思考一個問題:如果子View處理了ACTION_DOWN但后續(xù)事件都返回false,這些沒有被處理的事件最后傳給誰處理了?各位思考之,后面再說這個問題。
孩子是誰的
繼續(xù)來擴(kuò)展我們的偽代碼,攔截條件判斷完之后,決定把事件繼續(xù)傳遞給子View的時候,會調(diào)用childView.dispatchTouchEvent(ev),問題來了,child是哪來的,繼續(xù)看源碼
if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); continue; }
ViewGroup通過判斷所有的子View是否可見是否在播放動畫和是否在點(diǎn)擊范圍內(nèi)來決定它是否能夠有資格接受事件。只有滿足條件的child才能夠調(diào)用dispatch。
再看偽代碼,最后dispatch返回ViewGroup的isConsumed,若isConsume == true,說明ViewGroup處理了這個點(diǎn)擊事件(ViewGroup自身或者子View處理的),并且這個系列的點(diǎn)擊事件會繼續(xù)傳到這個ViewGroup來處理,若isConsume == false(ACTION_DOWN時),ViewGroup沒辦法處理這個點(diǎn)擊事件,那么這個系類的點(diǎn)擊事件就和該ViewGroup無緣了。會把這個事件上拋給自己的父容器或者Activity處理。
偽代碼說完了,ViewGroup的事件傳遞規(guī)則也就差不多說完了,這么看是不是很簡單了。View相對于ViewGroup來說就更簡單了,沒有攔截方法,dispatch基本上是直接調(diào)用了自身的onTouchEvent,處理起來一點(diǎn)難度都木有呀。
一些沒說到但也很重要的點(diǎn)
上面解釋的東西都很簡單,是從一個ViewGroup+一個View開始的,事件分發(fā)的執(zhí)行者是ViewGroup,子容器也只有一個View,但實際開發(fā)中當(dāng)然沒這么簡單,不過不要怕,再復(fù)雜的情況也能夠拆分成這種模式的,只不過層次多了一些遞歸復(fù)雜了一些而已,原理還是一樣的。
順帶補(bǔ)充幾點(diǎn):
從用戶點(diǎn)擊屏幕開始觸發(fā)一個系列的點(diǎn)擊事件時,事件真正的傳遞流程是:Activity(PhoneWindow)->DecorView->ViewGroup->View,在到達(dá)ViewGroup之前還有一個DecorView,事件是從Activity傳過來的,但這些東西其實和ViewGroup的原理是一樣的,Activity能看做一個大的ViewGroup,當(dāng)它的DecorView包含的所有子View沒有人能夠消耗事件的時候(這樣說有漏洞,大家懂我的意思就行了)最后還是會交給Activity處理。
事件沖突解決可以按照上面的原理在幾個point中進(jìn)行處理。最容易想到的處理的時機(jī)是在onInterceptTouchEvent里,比如當(dāng)一個豎直方向滑動的ViewGroup里嵌套一個橫向滑動的ViewGroup,可以在這里的ACTION_MOVE里來判斷后續(xù)事件應(yīng)該傳遞給誰處理,當(dāng)然,也可以根據(jù)上面說的標(biāo)記位FLAG_DISALLOW_INTERCEPT配合子View的dispatchTouchEvent來控制事件的流向,這都是比較容易想到的,不過看過別的大神,通過分享MotionEvent的方法來控制事件的流向,即在父容器中保存MotionEvent并在適當(dāng)?shù)臅r機(jī)傳入子View自定義的事件處理方法來分享事件,也是可行的。
任何View只要拒絕了一系列事件中的ACTION_DOWN(返回false),則后續(xù)事件都不會再傳遞過來了。但如果拒絕了其他的事件,后續(xù)事件還是可以傳過來的,比如View某次ACTION_MOVE沒處理,這個沒處理的事件最后會被Activity消耗掉(而不是View的父容器),但后續(xù)的事件還是會繼續(xù)傳給該View。
合理的利用ACTION_CANCEL能夠控制一個系列事件的生命周期,讓事件處理更加靈活。
理解事件分發(fā)的機(jī)制只要明白上面的原理基本就夠用了,github上很多牛逼的大神寫的各種炫酷的自定義控件的事分發(fā)根據(jù)這些也能夠看明白,當(dāng)然還有很多擴(kuò)展的東西和更深入的內(nèi)容由于篇幅的關(guān)系在這里就不羅嗦了,更重要的還是去看源碼吧。
最后送各位一句經(jīng)典:紙上得來終覺淺,絕知此事要躬行!
以上就是對Android View事件分發(fā)機(jī)制的資料整理,后續(xù)繼續(xù)補(bǔ)充相關(guān)資料,謝謝大家對本站的支持!
相關(guān)文章
eclipse中運(yùn)行monkeyrunner腳本之環(huán)境搭建(4)
這篇文章主要為大家詳細(xì)介紹了eclipse中運(yùn)行monkeyrunner腳本之環(huán)境搭建的相關(guān)資料,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-12-12android 實現(xiàn)ScrollView自動滾動的實例代碼
這篇文章主要介紹了android 實現(xiàn)ScrollView自動滾動的實例代碼,有需要的朋友可以參考一下2014-01-01Android RecycleView添加head配置封裝的實例
這篇文章主要介紹了Android RecycleView添加head配置封裝的實例的相關(guān)資料,這里提供實例幫助大家實現(xiàn)這樣的功能,需要的朋友可以參考下2017-08-08Android9 清除最近進(jìn)程列表實現(xiàn)方法
這篇文章主要為大家介紹了Android9 清除最近進(jìn)程列表實現(xiàn)方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-06-06flutter InheritedWidget使用方法總結(jié)
這篇文章主要為大家介紹了flutter InheritedWidget使用方法總結(jié)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11Android入門之動態(tài)BroadCast的使用教程
系統(tǒng)自己在很多時候都會發(fā)送廣播,比如電量低或者充足,剛啟動完,插入耳機(jī),你有一條新的微信消息,這種都是使用BroadCast機(jī)制去實現(xiàn)的。BroadCast分為靜態(tài)和動態(tài)BroadCast兩種,本文就來聊聊動態(tài)BroadCast的使用,需要的可以參考一下2022-12-12