概述
Android事件傳遞機制也是Android系統中比較重要的一塊,事件類型有很多種,這里主要討論TouchEvent的事件在framework層的傳遞處理機制。因為對於App開發人員來說,理解framework層的事件傳遞機制,就差不多了。
帶着問題來思考整個事件分發過程。
1、為什么要有事件分發過程?
當Android設備的屏幕,接收到觸摸的動作時,屏幕驅動把壓力信號(包括壓力大小,壓力位置等)傳遞給系統底層,然后操作系統經過一系列的處理,然后把觸摸事件一層一層的向上傳遞,最終事件會被准確的傳遞到產生事件的對象上,系統會遍歷每一個View對象,然后計算觸摸點在哪一個View中。比如A和B兩個View,是兄弟View,AView產生的觸摸事件,是不會被分發到B上面的。
2、怎么看待事件序列?
在Android系統中,一個單獨的事件基本上是沒什么作用的,只有一個事件序列,才有意義。一個事件序列正常情況下,定義為 DOWN、MOVE(0或者多個)、UP/CANCEL。事件序列以DOWN事件開始,中間會有0或者多個MOVE事件,最后以UP事件或者CANCEL事件結束。
DOWN事件作為序列的開始,有一個很重要的職責,就是尋找事件序列的接受者,怎么理解呢?framework 在DOWN事件的傳遞過程中,需要根據View事件處理方法(onTouchEvent)的返回值來確定事件序列的接受者。如果一個View的onTouchEvent事件,在處理DOWN事件的時候返回true,說明它願意接受並處理該事件序列。
3、Android的framework層如何處理事件的分發過程?
觸摸事件到了framework層之后,首先會被傳遞到Activity,然后Activity會把事件委托給它內部的Window對象進行分發處理,而Window對象又會委托它內部的DecorView進行事件分發處理。我們都知道,DecorView是整棵View樹的根節點,所以整個事件傳遞過程的復雜度就是事件在View樹種分發傳遞的復雜度。 Android View框架提供了3個對事件的主要操作概念。
1、事件的分發機制,dispatchTouchEvent。主要是parent根據觸摸事件的產生位置,以及child是否願意負責處理該系列事件等狀態,向其child分發事件的機制。
2、事件的攔截機制,onInterceptTouchEvent。主要是parent根據它內部的狀態、或者child的狀態,來把事件攔截下來,阻止其進一步傳遞到child的機制。
3、事件的處理機制,onTouchEvent。主要是事件序列的接受者(可以是一個View或者ViewGroup),對事件作出處理,並且向其parent傳遞處理結果的機制。
4、上述三個機制,是怎么向其調用者傳遞處理結果的?
在Java中,傳遞計算結果,有很多種途徑,這里采用的是一種適用於同步調用的方法,返回值的方法。每個機制都使用boolean類型作為其返回值,那么每個機制的每個返回值是什么含義呢。
1、事件的分發機制,dispatchTouchEvent。
true-事件被以該節點為根節點的View樹成功處理,此時該事件就算是處理完成了,事件不會再向上返還給View的父節點(把事件分發過來的那個節點)。
false-以該節點為根節點的View樹種,沒有一個View(包括該View)成功處理了此事件,所以事件會向上返還給View的父節點(把事件分發過來的那個節點)。
2、事件的攔截機制,onInterceptTouchEvent。主要是parent根據它內部的狀態、或者child的狀態,來把事件攔截下來,阻止其進一步傳遞到child的機制。
true-當前ViewGroup(因為View中沒有該方法,而沒有child的VIew也不需要有攔截機制)希望該事件不再傳遞給其child,而是希望自己處理。
false-當前ViewGroup不准備攔截該事件,事件正常向下分發給其child。
3、事件的處理機制,onTouchEvent。主要是事件序列的接受者(可以是一個View或者ViewGroup),對事件作出處理,並且向其parent傳遞處理結果的機制。
true-表示該View成功處理了該事件,該處理結果會向上通知給其parent。
false-表示該View沒有成功處理該事件,那么它的parent會有機會來處理該事件(parent標記為事件序列接受者,parent 的 onTouchEvent 在 Down 事件時返回true)。
源代碼分析
源代碼基於SDK 23
View:
1、dispatchTouchEvent:
/** 把事件分發到目標對象,因為這里是View對象,默認不含有child,所以這里他會把事件分發給自己 */
public boolean dispatchTouchEvent(MotionEvent event);
源代碼:
不給出,有興趣的讀者執行查閱SDK
偽代碼:
public boolean dispatchTouchEvent(MotionEvent event){ boolean result = false; //如果有事件監聽器,先讓監聽器處理事件。 if (mOnTouchListener.onTouch(event)) { //如果監聽器成功處理了該事件,處理結果設置為true。 result = true; } //如果沒有監聽器,就調用自身的onTouchEvent方法來處理事件。 if (!resutlt && onTouchEvent(event)) { //如果自身的onTouchEvent成功處理事件,處理結果設置為true。 result = true; } return result; }
ViewGroup:
1、onInterceptTouchEvent
/** 默認實現是返回false,也就是默認不攔截任何事件 */
public boolean onInterceptTouchEvent(MotionEvent ev);
2、dispatchTouchEvent
/** 根據內部攔截狀態,向其child或者自己分發事件 */
public boolean dispatchTouchEvent(MotionEvent ev);
源代碼:
不給出,有興趣的讀者執行查閱SDK
偽代碼:
public boolean dispatchTouchEvent(MotionEvent ev) { if (ACTION_DOWN事件 || 沒有事件處理對象) { if (允許攔截事件,該標志位由child調用requestDisallowInterceptTouchEvent<span style="font-family:微軟雅黑;font-size:14px;">設置</span>) { //查詢攔截機制的結果,根據該結果來判斷是否需要攔截 intercepted = onInterceptTouchEvent(ev); } else { //不允許攔截,那么不攔截 intercepted = false; } } else { //不是DOWN,並且有處理對象,允許攔截,中斷事件傳遞 intercepted = true; } if (不取消 && 不攔截) { if (ACTION_DOWN) { //找尋接收事件序列的對象,其他事件不需要再計算事件產生對象,試想一下滑動一個ListView,當手指滑動出ListView的范圍時,依然還是ListView響應后續事件。 for (遍歷所有childView) { if (觸摸點不在childView內部) { continue; } if (childView.dispatchTouchEvent(event)) { 保存處理該事件的View,后續事件直接傳遞到該View,不要重新計算; } } } if (還沒有事件處理對象) { //當前View樹中沒找到合適的child處理對象,把事件給自己處理,View.dispatchTouchEvent()就是把事件分發給自己 super.dispatchTouchEvent(event); } else { //傳遞給child childView.dispatchTouchEvent(event); } } else if (攔截) { //攔截事件,把事件給自己處理,View.dispatchTouchEvent()就是把事件分發給自己 super.dispatchTouchEvent(event); } return 處理結果; }
3、requestDisallowInterceptTouchEvent
/** 干澀parent的事件分發機制,通知parent,是否攔截后續事件,如果設置為true,parent就不會攔截該事件,不管什么狀態。設置為false,parent走正常的攔截流程 */
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept);
源代碼:
不給出,有興趣的讀者執行查閱SDK
偽代碼:
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { if (已經是當前要設置的狀態) { // 已經處於這個狀態, 假設我們的parent也是這個狀態 return; } 設置該狀態; // 傳遞給parent if (有父容器) { 設置父容器的攔截狀態; } }
自己動手
我們都知道,如果ScrollView內部嵌套ListView,那么ListView是不可以滑動的,效果如下圖所示:
那么其實這就是典型的事件沖突問題,就是說,原本應該被ListView用來上下滑動的事件,被ScrollView攔截了。就導致ListView不能正常滑動。
我們來看一下ScrollView的源代碼:
onInterceptTouchEvent的偽代碼:
public boolean onInterceptTouchEvent(MotionEvent ev) { /* * 這個方法決定了我們是否要攔截事件. * 如果返回true, onTouchEvent會被調用並且我們開始做實際的Scroll操作. */ /* * 大部分循環的狀態: 用戶在再拖拽的狀態並且正在移動手指, * 我們希望攔截這個事件 */ final int action = ev.getAction(); if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) { return true; } //其他操作 ...................... }
所以正常的上下拖拽,ScrollView都會攔截。
那么我們下面改進一下,就是當我們滑動ScrollView中非ListView的區域時,ScrollView滑動,而我們滑動ListView的時候,ListView滑動,效果看起來如下圖所示:
這里解決方法如下:
既然ScrollView會攔截事件,那么當我們滑動ListView的時候,我們不希望ScrollView攔截事件,這里我們繼承ListView,在onTouchEvent中,請求ScrollView不要攔截事件。
部分代碼如下:
@Override public boolean onTouchEvent(MotionEvent ev) { super.onTouchEvent(ev); switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: getParent().requestDisallowInterceptTouchEvent(true); break; case MotionEvent.ACTION_MOVE: break; case MotionEvent.ACTION_UP: getParent().requestDisallowInterceptTouchEvent(false); break; default: break; } return true; }
這樣就可以很好的解決事件沖突的問題。
還有一種方法就是覆寫parent的onInterceptTouchEvent方法,來修改事件攔截的狀態。