天天看點

Android高頻面試專題 - 提升篇(三)事件分發機制

關于事件分發機制的流程,網上部落格已經講爛了。但是對于這個流程,還是建議大家都自己親自動手,跟着源碼走一遍,不然面試官一問,Activity中,dispatchTouchEvent(event)中的MotionEvent是哪裡來的,還不一下就露餡了?

1、事件分發機制分發的是什麼

當使用者點選螢幕裡View或者ViewGroup的時候,将會産生一個事件對象,這個事件對象就是MotionEvent對象,這個對象記錄了事件的類型,觸摸的位置,以及觸摸的時間等。MotionEvent裡面定義了事件的類型,其實很容易了解,因為使用者可以在螢幕觸摸,滑動,離開螢幕動作,分别對應:

  • MotionEvent.ACTION_DOWN:使用者觸摸View&ViewGroup。
  • MotionEvent.ACTION_MOVE:使用者手指移動View&ViewGroup。
  • MotionEvent.ACTION_UP:使用者手指離開螢幕。
  • MotionEvent.ACTION_CANCEL:事件退出了,不是使用者導緻的。

 是以使用者在觸摸螢幕到離開螢幕會産生一系列事件,ACTION_DOWN->ACTION_MOVE(0個或者多個)->ACTION_UP,那麼ACTION_ CANCEL事件是怎麼回事呢?請看下面的圖你就懂的更徹底了:

Android高頻面試專題 - 提升篇(三)事件分發機制

2、ACTION_CANCEL什麼時候觸發

如果某一個子View處理了Down事件,那麼随之而來的Move和Up事件也會交給它處理。但是交給它處理之前,父View還是可以攔截事件的,如果攔截了事件,那麼子View就會收到一個Cancel事件,并且不會收到後續的Move和Up事件。常見場景就是ListView中Item内部有一個Button,我們讓ACTION_DOWN落在這個Button上,然後上下滑動,此時MOVE事件就會被ListView攔截,那麼Button就會收到ACTION_CANCEL事件了。

3、MotionEvent在哪裡産生

我們知道,觸摸螢幕,首先肯定是硬體産生的一個電信号,但是我們能接觸到的觸摸事件直接就到了MotionEvent,那麼這個MotionEvent在哪裡産生?其實是在framework層做的處理,如果不做系統應用開發,基本上接觸不到framework的。螢幕對應Android來說,擔任了鍵盤的作用,就是我們計算機組成的輸入裝置,我們知道Android是基于Linux系統的,當我們的輸入裝置可用時(我們這裡隻來講解觸摸屏),我們對觸摸屏進行操作時,Linux就會收到相應的硬體中斷,然後将中斷加工成原始的輸入事件并寫入相應的裝置節點中。而我們的Android 輸入系統所做的事情概括起來說就是監控這些裝置節點,當某個裝置節點有資料可讀時,将資料讀出并進行一系列的翻譯加工,然後在所有的視窗中找到合适的事件接收者,并派發給它。這裡所說的Android輸入系統,就是InputManagerService(IMS),它和我們熟知的ActivityManagerService(AMS)一樣,作為系統服務,都是在SystemServer中建立。

前面我們講過,Activity、Window和View之間的關系,我們知道,我們的Activity建立是,會建立對應的PhoneWindow,建立完成之後,我們也在該Window上注冊了InputChannel并與IMS通信,IMS把事件寫入InputChannel,WindowInputEventReceiver對事件進行處理并最終還是通過InputChannel回報給IMS。

具體細節:https://segmentfault.com/a/1190000012227736

篇幅原因,這裡不貼細節源碼,我們在ViewRoot調用setView時,會建立WindowInputEventReceiver(簡稱receiver),IMS寫入事件時,receiver就會回調onInputEvent(InputEvent event, int displayId),這個時候我們收到的還是InputEvent,最後交由processPointerEvent()方法處理,這個方法内部會将InputEvent強轉成MotionEvent(繼承自InputEvent),然後調用mView.dispatchPointerEvent(event), 由于都是ViewRoot的内部類,這裡的mView其實就是DecorView了,而DecorView的dispatchPointerEvent直接是從View繼承而來。

//View.javapublic final boolean dispatchPointerEvent(MotionEvent event) {    if (event.isTouchEvent()) {        return dispatchTouchEvent(event);    } else {        return dispatchGenericMotionEvent(event);    }}           

複制

這裡又直接調用了dispatchTouchEvent(event),而DecorView又重寫了這個方法。

//DecorView.java@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {    final Window.Callback cb = mWindow.getCallback();    return cb != null && !mWindow.isDestroyed() && mFeatureId < 0            ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);}           

複制

可以看到,這裡最終又是交由Window.Callback來進行分發,實際上這裡的callback就是Activity,在Activity的attach()方法中,會通過mWindow.setCallback(this), 毫無疑問,Activity肯定是實作了Window.Callback這個接口的,至此,MotionEvent傳遞到了Activity,也就是調用了Activityity.dispatchTouchEvent()。

4、MotionEvent的傳遞順序

從上面可以看到,MotionEvent最開始是從DecorView傳遞到Activity的,那麼Activity中又是怎樣處理的

//Activity.java public boolean dispatchTouchEvent(MotionEvent ev) {    if (ev.getAction() == MotionEvent.ACTION_DOWN) {        onUserInteraction();    }    //在這裡我們又把事件給了PhoneWindow.superDispatchTouchEvent方法根據其傳回值,    //若傳回值為true,那麼dispatchTouchEvent傳回true,我們Activity的onTouchEvent方法無法得到執行    if (getWindow().superDispatchTouchEvent(ev)) {        return true;    }    //這裡就是我們的Activity的onTouchEvent方法    return onTouchEvent(ev);}           

複制

Activity又調用了getWindow().superDispatchTouchEvent(ev)也就是PhoneWindow。

@Overridepublic boolean superDispatchTouchEvent(MotionEvent event) {    //兜兜轉轉一大圈,還是把事件交給我們的DecorView,    //DecorView繼承自FrameLayout,FrameLayout呢又繼承自ViewGroup,    //是以作為一個ViewGroup,DecorView繼續向其子View派發事件,其流程我在文章的開頭就已經給了    return mDecor.superDispatchTouchEvent(event);}           

複制

這裡又調用了DecorView的superDispatchTouchEvent(event),這裡面其實就是直接繼承自ViewGroup的dispatchTouchEvent(MotionEvent ev)方法,也就是說,事件從DecorView傳遞到Activity,最終又回到DecorView,最後按照分發機制分發到ViewGroup再到所有的子View。

是以完整的事件分發順序應該是IMS→WindowInputEventReceiver(ViewRoot)→DecorView→Activity→DecorView→ViewGroup→View

是不是豁然開朗,網上的部落格都隻告訴你,事件分發從Activity開始,原來并不是從Activity開始的。

5、事件分發流程

事件分發機制使用的是責任鍊設計模式,從Activity如果傳到最下層的View都沒有元件處理該事件,該事件會依次回傳到Activity。這裡面就涉及到3個重要的方法:

Android高頻面試專題 - 提升篇(三)事件分發機制
  • dispatchTouchEvent

 用來進行事件的分發。如果事件能夠傳遞給目前View,那麼此方法一定會被調用,傳回結果受目前View的onTouchEvent和下級的dispatchTouchEvent方法影響,表示是否消耗此事件。

  • onInterceptTouchEvent

 在上述方法dispatchTouchEvent内部調用,用來判斷是否攔截某個事件,如果目前View攔截了某個事件,那麼同一個事件序列當中,此方法不會被再次調用,傳回結果表示是否攔截目前事件。

  • onTouchEvent

 同樣也會在dispatchTouchEvent内部調用,用來處理點選事件,傳回結果表示是否消耗目前事件,如果不消耗,則在同一個事件序列中,目前View無法再次接收到事件。

//事件分發機制僞代碼public boolean dispatchTouchEvent(MotionEvent ev){    boolean consume = false;//記錄傳回值    if(onInterceptTouchEvent(ev)){//判斷是否攔截此事件        consume = onTouchEvent(ev);//如果目前确認攔截此事件,那麼就處理這個事件     }else{        consume = child.dispatchToucnEvent(ev);//如果目前确認不攔截此事件,那麼就将事件分發給下一級    }    return consume;}           

複制

這段經典的僞代碼,就可以诠釋整個分發過程:對于一個根ViewGroup而言,點選事件産生後,首先會傳遞給它,這時它的dispatchTouch就會被調用,如果這個ViewGroup的onInterceptTouchEvent方法傳回true就表示它要攔截目前的事件,接着事件就會交給這個ViewGroup處理,即它的onTouch方法就會被調用;如果這個ViewGroup的onInterceptTouchEvent方法傳回false就表示它不攔截目前事件,這時目前事件就會繼續傳遞給它的子元素,接着子元素的dispatchTouchEvent方法就會被調用,如此直到事件被最終處理。

6、onTouchListener,onTouchEvent和onClick的優先級别

這個從View的onTouchEvent源碼可以看到整個過程,如果mTouchListener.onTouch()方法傳回true,那麼事件就會被onTouchListener.onTouch消費掉,而onClick是在onTouchEvent()的ACTION_UP中處理的,是以優先級是onTouchListener>onTouchEvent>onclick

7、事件分發3個方法傳回值的作用

  • dispatchTouchEvent:方法傳回值為true表示事件被目前視圖消費掉;傳回為super.dispatchTouchEvent表示繼續分發該事件,傳回為false表示交給父類的onTouchEvent處理。
  • onInterceptTouchEvent:方法傳回值為true表示攔截這個事件并交由自身的onTouchEvent方法進行消費;傳回false表示不攔截,需要繼續傳遞給子視圖。如果return super.onInterceptTouchEvent(ev), 事件攔截分兩種情況:  

    1.如果該View存在子View且點選到了該子View, 則不攔截, 繼續分發 給子View 處理, 此時相當于return false。

    2.如果該View沒有子View或者有子View但是沒有點選中子View(此時ViewGroup 相當于普通View), 則交由該View的onTouchEvent響應,此時相當于return true。

注意:一般的LinearLayout、 RelativeLayout、FrameLayout等ViewGroup預設不攔截, 而 ScrollView、ListView等ViewGroup則可能攔截,得看具體情況。
  • onTouchEvent:方法傳回值為true表示目前視圖可以處理對應的事件;傳回值為false表示目前視圖不處理這個事件,它會被傳遞給父視圖的onTouchEvent方法進行處理。如果return super.onTouchEvent(ev),事件處理分為兩種情況:

    1.如果該View是clickable或者longclickable的,則會傳回true, 表示消費 了該事件, 與傳回true一樣;

    2.如果該View不是clickable或者longclickable的,則會傳回false, 表示不 消費該事件,将會向上傳遞,與傳回false一樣。

8、幾個重要結論

  • 同一次觸摸事件序列是從手指接觸螢幕的那一刻起,到手指離開螢幕的那一刻結束,在這個過程中所産生的一系列事件,這個事件的序列以down開始,中間含有數量不定的move事件,最終以up事件結束。
  • 正常情況下,一個事件序列隻能被一個View攔截且消耗。這一條的原因可以參考3,因為一旦一個元素攔截了某個事件,那麼同一個事件序列的所有事件都會直接交給它處理,是以同一個事件序列中的事件不能分别由兩個View同時處理,但是通過特殊手段可以做到,比如一個View将本該自己處理的事件通過onTouchEvent強行傳遞給其他View處理。
  • 某個View一旦決定攔截,那麼這個事件序列都隻能由它來處理(如果事件序列能夠傳遞給它的話),并且它的onInterceptTouchEvent不會被調用。這條也很好了解,就是說當一個View決定攔截一個事件後,那麼系統會把同一個事件序列内的其他方法都直接交給它來處理,是以就不用再調用這個View的onInterceptTouchEvent去詢問它是否攔截了。
  • 某個View一旦開始處理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent傳回了false),那麼同一件序列中的其他事件都不會再交給它處理,并且事件 将重新交由它的父元素去處理,即父元素的onTouchEvent會被調用。意思就是事件一旦交給一個View處理,那麼它就必須消耗掉,否則同一事件序列中剩下的事件就不再交給它處理了,這就好比上級交給程式員一件事,如果這件事沒有處理好,短時間内上級就不敢再把事件交給這個程式員做了,二者是類似的道理。
  • 如果View不消耗ACTION_DOWN以外的事件,那麼這個點選事件會消失,此時父元素的onTouchEvent并不會調用,并且目前View可以持續收到後續的事件,最終這些消失的點選事件會傳遞給Activity處理。
  • ViewGroup預設不攔截任何事件。Android源碼中ViewGroup的onInterceptTouchEvent方法預設傳回false。
  • View沒有onInterceptTouchEvent方法,一旦點選事件傳遞給它,那麼它的onTouchEvent方法就會被調用。
  • View的onTouchEvent預設都會消耗事件(傳回true),除非它是不可點選的(clickable和longClickable同時為false)。View的longClickable屬性預設為false,clickable屬性要分情況,比如Button的clickable屬性預設為true,而TextView的clickable屬性預設為false。
  • View的enable屬性不影響onTouchEvent的預設傳回值。哪怕一個View是disable狀态的,隻要它的clickable或者longClickable有一個為true,那麼它的onTouchEvent就傳回true。
  • onClick會發生的前提是目前View是可點選的,并且它接收到了down和up事件。
  • 事件傳遞過程是由外向内的,即事件總是先傳遞給父元素,然後再由父元素分發給子View,通過requestDisallowInterTouchEvent方法可以在子元素中幹預父元素的事件分發過程,但是ACTION_DOWN事件除外。

9、如何解決View的事件沖突?舉個開發中遇到的例子?

常見開發中事件沖突的有ScrollView與RecyclerView的滑動沖突、RecyclerView内嵌同時滑動同一方向。

滑動沖突的處理規則:

  • 對于由于外部滑動和内部滑動方向不一緻導緻的滑動沖突,可以根據滑動的方向判斷誰來攔截事件。
  • 對于由于外部滑動方向和内部滑動方向一緻導緻的滑動沖突,可以根據業務需求,規定何時讓外部View攔截事件,何時由内部View攔截事件。
  • 對于上面兩種情況的嵌套,相對複雜,可同樣根據需求在業務上找到突破點。

滑動沖突的實作方法:

  • 外部攔截法:指點選事件都先經過父容器的攔截處理,如果父容器需要此事件就攔截,否則就不攔截。具體方法:需要重寫父容器的onInterceptTouchEvent方法,在内部做出相應的攔截。
  • 内部攔截法:指父容器不攔截任何事件,而将所有的事件都傳遞給子容器,如果子容器需要此事件就直接消耗,否則就交由父容器進行處理。具體方法:需要配合requestDisallowInterceptTouchEvent方法。
Android高頻面試專題 - 提升篇(三)事件分發機制