Android開發,觸控無所不在。對於一些 不咋看源碼的同學來說,多少對這塊都會有一些疑惑。 View事件的分發機制,不僅在做業務需求上會碰到這些問題,在一些面試筆試題中也常有人問,可謂是老生常談了。我以前也看過很多人寫的這方面的文章,不是說的太囉嗦就是太模糊,還有一些在細節上寫的也有爭議,故再次重新整理一下這塊內容,十分鐘讓你搞懂View事件的分發機制。
說白了這些觸控的事件分發機制就是弄清楚三個方法,dispatchTouchEvent(),OnInterceptTouchEvent(),onTouchEvent(),和這三個方法與n個ViewGroup和View堆疊在一起的問題,再復雜的結構都能拆分成1個ViewGroup+1個View。
其實ViewGroup和View都是大同小異,View只是沒有了子容器,自然不存在攔截問題,dispatch也很簡單,所以弄明白了ViewGroup其實就懂的差不多了。
三個關鍵方法
public boolean dispatchTouchEvent(MotionEvent ev)
View/ViewGroup處理事件分發的發起者,View/ViewGroup接收到觸控事件最先調起的判斷這個方法,然後在這個方法中是否處理攔截或是將事件分發給子容器
public boolean onInterceptTouchEvent(MotionEvent ev)
ViewGroup專用,透過此方法可以達到控制事件的分發方向,一般可以在該方法中判斷將事件給ViewGroup獨吞或是它繼續傳遞給子容器,是處理事件衝突的最佳地點
public boolean onTouchEvent(MotionEvent event)
觸控事件的真正處理者,最後每個事件都會在這裡被處理
核心問題
時間分發機制的難點在哪,我覺得難的地方以下幾點:三個方法呼叫規則,決定處理事件的物件以及事件衝突的解決方法。
事件傳遞規則
一般一次點擊會有一系列的MotionEvent,可以簡單分為:down->move->….->move->up,當一次event分發到ViewGroup時,上述三個方法之間的ViewGroup中呼叫順序可以用一段簡單程式碼表示
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; }
回傳結果true表示事件被處理了,回傳false表示沒有處理。上面的程式碼簡單易懂,看起來也很簡單,一句話就能概括,ViewGroup收到事件後呼叫dispatch,在dispatch中先檢查是否要攔截,若攔截則ViewGroup吃掉事件,否則交給有處理能力的子容器處理。
不過,簡單歸簡單,寫成這樣只是為了方便理解,ViewGroup的事件處理流程當然沒這麼簡單,這裡忽略了很多細節問題,接下來繼續補充。回到上面說的,一系列事件我們常處理的一般都是一個down,多個move和一個up,光靠上面的偽代碼是沒辦法把這些問題都給完美解決,直接來看ViewGroup的dispatchTouchEvent。
onInterceptTouchEvent呼叫條件
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暫時不攔截事件,事件有機會傳給子View處理,返回true表示父容器直接攔截了該系列事件,後續不會再傳遞給子View了。子View想取得事件只能讓該值為false
onInterceptTouchEvent呼叫回傳false(回傳false才能傳遞給子View,對應到上面偽代碼的else中的內容,叫事件傳遞到子容器需要滿足的內容較好理解一些)需要滿足兩個條件中的任意一個就有可能觸發(當然只是有可能):
一個是在down的時候,另一個就是mFirstTouchTarget! =null,那mFirstTouchTarget何時不為空,有興趣的同學可以看ViewGroup中的addTouchTarget這個方法的調用時機,mFirstTouchTarget就是在這裡賦值的,源碼太長我就不貼了。
mFirstTouchTarget是用來保存ViewGroup中消費了ACTION_DOWN事件的子View,即在上面偽代碼中child.dispatchTouchEvent(ev)在ACTION_DOWN的時候返回true的View,只要有子View的dispatch在ACTION_DOWNtrue,就不會為null(這個賦值過程只發生在ACTION_DOWN裡,如果子ViewACTION__DOWN不給它賦值後面序列的事件就不會再),反之,若無子View處理,該對象即為null。當然,滿足了上述兩個條件還不行,必須還要滿足!disallowIntercept。
disallowIntercept這個變數很有意思,它的值主要受FLAG_DISALLOW_INTERCEPT這個標記影響,這個值可以被ViewGroup的子View設置,ViewGroup的子View如果調用了requestDisallowInterceptTouch)這個方法,會改變FLAG_DISALL ,這種情況會跳過intercept,導致攔截失效。
但这事还没了,FLAG_DISALLOW_INTERCEPT这个标记有一个重置的机制,查看ViewGroup源码可以看到,在处理MotionEvent.ACTION_DOWN的时候会重置这个标记导致disallowIntercept失效,是不是丧心病狂,上面的一段这么简单的代码有这么多幺蛾子,这里还能得到一个结论,ACTION_DOWN的时候肯定可以执行onInterceptTouchEvent的。
所以拦截的intercepted很重要,能影响到底是让ViewGroup还子View处理这个事件。
上面的两个有可能触发拦截的条件说完了,那么当两个条件都不满足的话就不会再调用拦截了(拦截很重要,一般ViewGroup都返回false这样能把事件传递给子View,如果在ACTION_DOWN时不能走到OnInterceptTouchEvent并返回false告诉ViewGroup不要拦截,则事件再也不能传给子View了,所以拦截一般都是要走到的,而且一般都是返回false这样能让子View有机会处理),这种情况一般都是在ACTION_DOWN处理完之后没有子View当接盘侠消费ACTION_DOWN以及后续事件,从上面的伪代码可以看出来,这时候ViewGroup自己就很被动了,需要自己来调用onTouchEvent来处理,这锅就自己背了。
再继续说一下mFirstTouchTarget和intercepted是怎么影响事件方向的。看源码:
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不为空才能让后续事件传递给子View,否则根据上上面说的代码后续事件只能给父容器处理了。
mFirstTouchTarget就是我们后续事件传递的对象,很容易理解,如果在ACTION_DOWN中没有确定这个对象,则后续事件不知道传递给谁自然就交给父容器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; } }
看到没,只要参数里传的child为空,则ViewGroup调用super.dispatchTouchEvent(event),super是谁,ViewGroup继承自View,当然是View咯,View的dispatch调用的谁?当然是自己的onTouchEvent(后面会说),所以这个最后还是调用了ViewGroup自己的onTouchEvent。
那么当child!=null的时候呢,调用的是child的dispatchTouchEvent(event),如果child可能是View也可能是ViewGroup,如果是ViewGroup则继续按照上面的伪代码执行事件分发,如果也是View则调用自己的onTouchEvent。
所以,说到底事件到底给谁处理,还是和传进来的child有关,那这个方法在哪里调用的呢,继续看:
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能影响事件分发的方向的原因。就这样,整个伪代码的流程是不是很清楚了。
这里需要多说两句,在上上面代码流程中,intercepted决定了这个事件会不会调用ViewGroup的onTouchEvent,当intercepted为true则后续流程会调用ViewGroup的onTouchEvent,仔细看上面的代码能发现,只有两种情况为ture:一是调用了InterceptTouchEvent把事件拦截下来,另一个就是没有一个子View能够消费ActionDown。只有这两种情况父容器ViewGroup才会自己处理
那么问题来了,思考一个问题:如果子View处理了ACTION_DOWN但后续事件都返回false,这些没有被处理的事件最后传给谁处理了?各位思考之,后面再说这个问题。
孩子是谁的
继续来扩展我们的伪代码,拦截条件判断完之后,决定把事件继续传递给子View的时候,会调用childView.dispatchTouchEvent(ev),问题来了,child是哪来的,继续看源码、
if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); continue; }
ViewGroup通过判断所有的子View是否可见是否在播放动画和是否在点击范围内来决定它是否能够有资格接受事件。只有满足条件的child才能够调用dispatch。
再看伪代码,最后dispatch返回ViewGroup的isConsumed,若isConsume == true,说明ViewGroup处理了这个点击事件(ViewGroup自身或者子View处理的),并且这个系列的点击事件会继续传到这个ViewGroup来处理,若isConsume == false(ACTION_DOWN时),ViewGroup没办法处理这个点击事件,那么这个系类的点击事件就和该ViewGroup无缘了。会把这个事件上抛给自己的父容器或者Activity处理。
伪代码说完了,ViewGroup的事件传递规则也就差不多说完了,这么看是不是很简单了。View相对于ViewGroup来说就更简单了,没有拦截方法,dispatch基本上是直接调用了自身的onTouchEvent,处理起来一点难度都木有呀。
一些没说到但也很重要的点
上面解釋的東西都很簡單,是從一個ViewGroup+一個View開始的,事件分發的執行者是ViewGroup,子容器也只有一個View,但實際開發中當然沒這麼簡單,不過不要怕,再複雜的情況也能夠拆分成這種模式的,只不過層次多了一些遞歸複雜了一些而已,原理還是一樣的。
順帶補充幾點:
從用戶點擊螢幕開始觸發一個系列的點擊事件時,事件真正的傳遞流程是:Activity(PhoneWindow)->DecorView->ViewGroup->View,在到達ViewGroup之前還有一個DecorView,事件是從Activity傳過來的,但這些東西其實和ViewGroup的原理是一樣的,Activity能看做一個大的ViewGroup,當它的DecorView包含的所有子View沒有人能夠消耗事件的時候(這樣說有漏洞,大家懂我的意思就行了)最後還是會交給Activity處理。
事件衝突解決可以按照上面的原理在幾個point中處理。最容易想到的處理的時機是在onInterceptTouchEvent裡,例如當一個垂直方向滑動的ViewGroup裡嵌套一個橫向滑動的ViewGroup,可以在這裡的ACTION_MOVE裡來判斷後續事件應該傳遞給誰處理,當然,也可以根據上面所說的標記位FLAG_DISALLOW_INTERCEPT配合子View的dispatchTouchEvent來控制事件的流向,這都是比較容易想到的,不過看過別的大神,透過分享MotionEvent的方法來控制事件的流向,即在父容器中保存MotionEvent並在適當的時機傳入子View自訂的事件處理方法來分享事件,也是可行的。
任何View只要拒絕了一系列事件中的ACTION_DOWN(返回false),則後續事件都不會再傳遞過來了。但如果拒絕了其他的事件,後續事件還是可以傳過來的,例如View某次ACTION_MOVE沒處理,這個沒處理的事件最後會被Activity消耗掉(而不是View的父親容器),但後續的事件還是會繼續傳給該View。
合理的利用ACTION_CANCEL能夠控制一個事件的生命週期,讓事件處理更靈活。
理解事件分發的機制只要明白上面的原理基本就夠用了,github上很多牛逼的大神寫的各種炫酷的自定義控件的事分發根據這些也能夠看明白,當然還有很多擴充的東西和更深入的內容由於篇幅的關係在這裡就不囉嗦了,更重要的還是去看源碼吧。
最後送各位一句經典:紙上得來終覺淺,絕知此事要躬行!
以上就是Android View事件分發機制的資料整理,後續繼續補充相關資料,謝謝大家對本站的支持!
更多Android View 事件分發機制詳解相關文章請關注PHP中文網!