Android事件體系
一切的起源
ofollow,noindex">https://blog.csdn.net/a992036795/article/details/51690303 這篇文章中對Android事件的起源進行了詳細的分析,雖說程式碼版本應該是Android5.0以前的,但也具有一定的參考價值。
總結起來大致可以概括為:Activity啟動過程中會建立PhoneWindow和DecorView,然後通過ViewRootImpl向InputManagerService註冊一個InputChannel以監聽螢幕觸控事件,後續的工作原理涉及硬體層的知識就不再追溯。從這個流程裡面我們可以提取出我們所關心的問題: Android事件的源頭是在Activity啟動過程中註冊的,事件流向應用層可以簡單認為是Activity->View。
事件傳遞機制

event.png
自上而下的過程,當某一層攔截了事件傳遞便就此結束,如果下面的層級都不需要攔截事件,則再次一層層向上回傳,最終回到Acitivity的onTouchEvent方法。整個過程和現實中工作安排是一模一樣的,老闆把任務安排給部門領導,部門領導再安排給手下,一層層向下委派,當底層手下無法解決這個問題時,再一層層向上反饋,最終回到老闆處。其中ViewGroup的事件分發虛擬碼如下:
public boolean dispatchTouchEvent(MotionEvent event){ //是否處理事件標誌 boolean handled = false; if(!onInterceptTouchEvent()){ //遍歷子view分發 handled = childView.dispatchTouchEvent(event); } if(!handled) handled = onTouchEvent(event); return handled; }
事件序列
Android的touch事件從手指觸控式螢幕幕開始,到手指離開螢幕,可以視為一個完整的事件序列,從MotionEvent的型別來看,一個事件序列包含一個ACTION_DOWN事件、n個ACTION_MOVE事件、一個ACTION_UP事件:
ACTION_DOWN:手指剛接觸螢幕
ACTION_MOVE:手指在螢幕上滑動
ACTION_UP:手指離開螢幕
一個完整的事件序列只能被同一個view消耗,簡而言之,一旦某個view決定攔截ACTION_DOWN事件之後,這個事件序列之後的事件都將由該view消耗,其他的view即使想要單獨攔截ACTION_MOVE、ACTION_DOWN事件也是做不到的,我們可以寫個簡單的demo來論證這個理論。
public class CustomViewGroup extends ViewGroup { @Override public boolean dispatchTouchEvent(MotionEvent ev) { Log.e("TAG","ViewGroup dispatchTouchEvent:"+ev.getAction()); return super.dispatchTouchEvent(ev); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { Log.e("TAG","ViewGroup onInterceptTouchEvent:"+ev.getAction()); //第二次測試 //if(event.getAction() == MotionEvent.ACTION_DOWN) { //return true; //}else{ //第三次測試 //return false; //} return super.onInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent event) { Log.e("TAG","ViewGroup onTouchEvent:"+event.getAction()); //if(event.getAction() == MotionEvent.ACTION_DOWN) { //return true; //}else{ //第三次測試 //return false; //} return super.onTouchEvent(event); } } public class CustomView extends TextView { @Override public boolean dispatchTouchEvent(MotionEvent ev) { Log.e("TAG","View dispatchTouchEvent:"+ev.getAction()); return super.dispatchTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent event) { Log.e("TAG","View onTouchEvent:"+event.getAction()); //if(event.getAction() == MotionEvent.ACTION_DOWN) { //return true; //} return super.onTouchEvent(event); } }
初次測試:CustomViewGroup,CustomView均設定為不可點選,不攔截任何事件,日誌如下:
//0-ACTION_DOWN,1-ACTION_UP,2-ACTION_MOVE 11-13 23:05:05.751 17354-17354/ E/TAG: ViewGroup dispatchTouchEvent:0 11-13 23:05:05.751 17354-17354/ E/TAG: ViewGroup onInterceptTouchEvent:0 11-13 23:05:05.751 17354-17354/ E/TAG: View dispatchTouchEvent:0 11-13 23:05:05.751 17354-17354/ E/TAG: View onTouchEvent:0 11-13 23:05:05.751 17354-17354/ E/TAG: ViewGroup onTouchEvent:0
正如事件分發機制所述,事件經過 CustomViewGroup#dispatchTouchEvent->CustomViewGroup#onInterceptTouchEvent->子View#dispatchTouchEvent->子View#onTouchEvent ,子View無法處理後再次回到 CustomViewGroup#onTouchEvent 。(這邊發現日誌只有ACTION_DOWN的事件分發,暫且記下後續會有說明)
第二次測試:CustomViewGroup,CustomView均設定攔截ACTION_DOWN事件,日誌如下:
// 11-13 23:17:00.691 22798-22798/ E/TAG: ViewGroup dispatchTouchEvent:0 11-13 23:17:00.691 22798-22798/ E/TAG: ViewGroup onInterceptTouchEvent:0 11-13 23:17:00.691 22798-22798/ E/TAG: ViewGroup onTouchEvent:0 11-13 23:17:00.751 22798-22798/ E/TAG: ViewGroup dispatchTouchEvent:1 11-13 23:17:00.751 22798-22798/ E/TAG: ViewGroup onTouchEvent:1
由日誌可看出,ViewGroup決定攔截ACTION_DOWN事件之後,都沒有呼叫子view的dispatch,而且在分發ACTION_UP事件時都沒有呼叫onInterceptTouchEvent。
第三次測試:CustomViewGroup,CustomView均設定攔截ACTION_DOWN事件,並且CustomViewGroup設定只攔截ACTION_DOWN事件,日誌如下:
// 11-13 23:22:39.181 24428-24428/ E/TAG: ViewGroup dispatchTouchEvent:0 11-13 23:22:39.181 24428-24428/ E/TAG: ViewGroup onInterceptTouchEvent:0 11-13 23:22:39.181 24428-24428/ E/TAG: ViewGroup onTouchEvent:0 11-13 23:22:39.261 24428-24428/ E/TAG: ViewGroup dispatchTouchEvent:2 11-13 23:22:39.261 24428-24428/ E/TAG: ViewGroup onTouchEvent:2 11-13 23:22:39.261 24428-24428/ E/TAG: ViewGroup dispatchTouchEvent:1 11-13 23:22:39.261 24428-24428/ E/TAG: ViewGroup onTouchEvent:1
由日誌可看出,ViewGroup決定攔截ACTION_DOWN事件之後,即使設定不攔截其他型別事件,整個事件序列的日誌還是到ViewGroup就結束了。
綜上所述,Android對於事件序列的分發有某種特殊的機制:一個事件序列只會由攔截ACTION_DOWN的那個View消耗,同時在事件分發時有做過優化,能夠在分發ACTION_UP、ACTION_MOVE事件時找到攔截ACTION_DOWN的那個View並跳過onIntercept方法。得出該結論之後,再回到原始碼也能找到相應的依據,具體程式碼片段見ViewGroup的dispatchTouchEvent方法:
//處理ACTION_DOWN事件時會記錄touchTarget if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { ... newTouchTarget = addTouchTarget(child, idBitsToAssign); ... } //後續會對touchTarget進行判斷 if (mFirstTouchTarget == null) { // No touch targets so treat this as an ordinary view. handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); }else { // Dispatch to touch targets, excluding the new touch target if we already // dispatched to it.Cancel touch targets if necessary. TouchTarget predecessor = null; TouchTarget target = mFirstTouchTarget; while (target != null) { final TouchTarget next = target.next; if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { handled = true; } else { final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted; if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) { handled = true; }
由此可見,在一次事件序列分發過程中,ViewGroup會記錄最初的touchTarget,有利於提高後續的事件分發效率。
滑動衝突
對Android View的事件分發體系有了完整的瞭解之後,比較經典的應用就是滑動衝突的解決,核心思想就是ACTION_UP事件攔截的特殊處理,方法有內部攔截法和外部攔截法兩種,有興趣的童鞋可以查查資料瞭解下。