search
尋找貓咪~QQ 地點 桃園市桃園區 Taoyuan , Taoyuan

Android事件分發與消費

之前寫過一篇Android 事件分發機制詳解 ,感覺比較亂,這裡再總結一下。網上已經有很多前輩分析過源碼,大家可以參考,我這裡盡量不做過多的源碼分析,僅僅從流程上分析。

0x01 基礎部分

事件分發和消費我們主要涉及到以下三個方法:

dispatchTouchEvent:分發事件
onInterceptTouchEvent:攔截事件
onTouchEvent:處理事件

還需要注意常用的兩個介面對以上方法的影響:

OnClickListener :點擊事件監聽
OnTouchListener :觸摸事件監聽

最後再認識一下MotionEvent,他表示用戶的觸摸事件,用戶的點擊、觸摸或者滑動都會產生相應的MotionEvent,而我們所要關心得主要有三種:

MotionEvent.ACTION_DOWN :int類型,值為0,表示用戶的手指剛接觸到屏幕
MotionEvent.ACTION_UP :int 類型,值為1,表示用戶的手指從屏幕上抬起
MotionEvent.ACTION_MOVE :int 類型,值為2,表示用戶的手指正在屏幕上移動

例如當我們觸摸屏幕,可能產生一系列的事件:
點擊一下屏幕,會產生ACTION_DOWN,ACTION_UP
點擊屏幕並滑動然後鬆開,會產生ACTION_DOWN,一系列的ACTION_MOVE,ACTION_UP事件。
當然還有其他的動作,我們不做多的講解,這三個動作是我們在操作手機時最常見到的,在處理滑動衝突中很關鍵,下文將會講到。

0x02 View的事件分發與處理

這裡的View一般指的是像Button,TextView這種,處於事件分發的最後一級。主要包含dispatchTouchEvent和onTouchEvent兩個方法。我們注意到他沒有onInterceptTouchEvent方法所以也就不需要判斷是否需要攔截事件。為了方便講解,這裡舉一個小例子來輔助說明。
首先定義一個CustomButton並重寫其dispatchTouchEvent和onTouchEvent方法: publicclassCustomButtonextendsAppCompatButton { String TAG = "CustomButton"; publicCustomButton(Context context) { super(context); } publicCustomButton(Context context, AttributeSet attrs) { super(context, attrs); } @OverridepublicbooleanonTouchEvent(MotionEvent event) { Log.e(TAG, "onTouchEvent:" + "action:" + event.getAction); boolean result = super.onTouchEvent(event); Log.e(TAG, "super.onTouchEvent:result=" + result); return result; } @OverridepublicbooleandispatchTouchEvent"dispatchTouchEvent:" + "action:"boolean result = super.dispatchTouchEvent(event); Log.e(TAG, "super.dispatchTouchEvent:result=" + result); return result; } }

在activity_main布局文件中使用該View,運行程序,點擊該View一下,得以下結果: CustomButton: dispatchTouchEvent:action:0CustomButton: onTouchEvent:action:0CustomButton: super.onTouchEvent:result=trueCustomButton: super.dispatchTouchEvent:result=trueCustomButton: dispatchTouchEvent:action:1CustomButton: onTouchEvent:action:1CustomButton: CustomButton:

從上邊的日誌可以得出:
1.dispatchTouchEvent會先於onTouchEvent方法回調。
2.對於該點擊動作,先處理action為0,后處理action為1的事件。上文中講到了0是MotionEvent.ACTION_DOWN事件,1是MotionEvent.ACTION_UP事件。
3.默認情況下dispatchTouchEvent和onTouchEvent方法都返回的true。這表示該View將要消費這些接收到的事件
閱讀過源碼的同學應該知道,該View的父ViewGroup將事件分發到它,也就是回調該View的dispatchTouchEvent方法。源碼中要處理的事情很多,這裡就不詳細的介紹了,當然還是希望有興趣的同學去閱讀一下源碼的。View的dispatchTouchEvent的方法被回調時也就是事件被交給View處理了,它可以選擇去處理消費這些事件,也可以選擇不去消費(onTouchEvent返回false)。如果返回false那麼它又會將事件傳遞給父ViewGroup這是后話。
接下來我們討論OnTouchListener,OnClickListener的影響。我們在MainActivity中給CustomButton設置一下監聽: publicclassMainActivityextendsAppCompatActivityimplementsView.OnTouchListener, View.OnClickListener {@OverrideprotectedvoidonCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); CustomButton cb = (CustomButton) findViewById(R.id.btn_1); cb.setOnClickListener(this); cb.setOnTouchListener(this); } @OverridepublicbooleanonTouch(View v, MotionEvent event) { Log.e("onTouch", "onTouch:action:"+event.getAction); returnfalse; } @OverridepublicvoidonClick(View v) { Log.e("onClick", "Button click"); } }

再點擊一下CustomButton會得到以下Log: CustomButton: dispatchTouchEvent:action:0onTouch: onTouch:action:0CustomButton: onTouchEvent:action:0CustomButton: dispatchTouchEvent:action:1onTouch: onTouch:action:1CustomButton: onTouchEvent:action:1onClick: Button click

可以看出執行順序為dispatchTouchEvent->onTouch->onTouchEvent->onClick,此時onTouch方法返回的為false,也就是表示它不會處理這些事件。那麼我們將onTouch方法返回為true在重複上邊的操作會得到什麼結果呢? CustomButton: dispatchTouchEvent:action:0onTouch: onTouch:action:0CustomButton: dispatchTouchEvent:action:1onTouch: onTouch:action:1

執行結果dispatchTouchEvent->onTouch。可以看到onTouchEvent和onClick沒有執行。要解釋清楚這個就必須看看源碼了。下邊是我整理的View.dispatchTouchEvent方法的部分源碼: public boolean dispatchTouchEvent(MotionEvent event) { ...... boolean result = false; ...... //noinspection SimplifiableIfStatement ListenerInfo li = mListenerInfo; if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { result = true; } if (!result && onTouchEvent(event)) { result = true; } ...... return result; }

最主要的就是if的判斷條件 li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)如果我們設置了OnTouchListener監聽,那麼li != null && li.mOnTouchListener != null就為true,此時判斷條件可以簡化為
true&& (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)

接下來會判斷View是否是ENABLED的(在xml文件中和代碼中都可以設置enable屬性)。從代碼中如果設置該屬性為false那麼onTouch(onClick也不會,可以自己看源碼)方法將不會回調,也就是設置OnTouchListener(OnClickListener)監聽將失效! 但是要注意,根據下一句代碼onTouchEvent還是可以回調的。如果不設置該屬性,默認是true的,那麼判斷條件又可以簡化 true&&true&&li.mOnTouchListener.onTouch(this, event)

此時就很清楚了,如果onTouch返回false那麼result就為false。 if (!result && onTouchEvent(event)) { result = true; }onTouchEvent就會回調,並且還會導致dispatchTouchEvent方法的返回值和onTouchEvent方法相同。相反如果onTouch返回為true那麼onTouchEvent方法就沒有辦法執行了。所以onTouchEvent能不能執行還得看onTouch方法的返回值。
注意:View的這下回調方法的優先順序非常重要,務必搞清楚,dispatchTouchEvent->onTouch->onTouchEvent->onClick。不僅如此,還得非常清楚每個方法返回值的結果對整個事件流的影響。
下圖是自己畫的View事件分發的一部分流程圖,希望能幫忙理解:

0x03 ViewGroup事件分發

上文介紹了View的分發,由於View一般是在事件分發中的最後一層,所以相對而言要簡單一些,但是ViewGroup就要複雜多了。我們一般用的比較多的ViewGroup就是RelativeLayout,LinearLayout等常用布局。因為這些布局很可能會包含其他View,所以除了dispatchTouchEvent,onTouchEvent之外還多了一個onInterceptTouchEvent用來攔截事件。我們還是簡單的自定義ViewGroupOne,ViewGroupTwo,ViewGroupThree繼承自LinearLayout。 publicclassViewGroupOneextendsLinearLayout {privatefinal String TAG = "ViewGroupOne"; publicViewGroupOne(Context context) { super(context); } publicViewGroupOne(Context context, @Nullable AttributeSet attrs) { super(context, attrs); } @OverridepublicbooleandispatchTouchEvent(MotionEvent ev) { Log.e(TAG, "dispatchTouchEvent:" + "action:" + ev.getAction); returnsuper.dispatchTouchEvent(ev); } @OverridepublicbooleanonInterceptTouchEvent"onInterceptTouchEvent:" + "action:" + ev.getAction); returnsuper.onInterceptTouchEvent(ev); } @OverridepublicbooleanonTouchEvent"onTouchEvent:" + "action:" + ev.getAction); returnsuper.onTouchEvent(ev); } }然後按照下圖進行嵌套使用:

點擊CustomButton可以得到以下信息: 默認情況下,CustomButton是消費事件的,這種情況下的分發還是很清晰的。

當事件傳遞到ViewGroupOne之後先回調dispatchTouchEvent,在該方法中會調用onInterceptTouchEvent方法來判斷是否攔截手勢,這裡默認是不攔截的,那麼會繼續傳遞到ViewGroupTwo並調用其dispatchTouchEvent方法,接下來就和ViewGroupOne一樣的流程,直到傳遞到View(CustomButton)並消費該事件(onTouchEvent返回true)。那麼我們再來看看當View(CustomButton)不消費事件(onTouchEvent返回false)時是什麼樣: ViewGroupOne: dispatchTouchEvent:action:0ViewGroupOne: onInterceptTouchEvent:action:0ViewGroupTwo: dispatchTouchEvent:action:0ViewGroupTwo: :action:0ViewGroupThree: dispatchTouchEvent:action:0ViewGroupThree: :action:0CustomButton: dispatchTouchEvent:action:0CustomButton: onTouchEvent:action:0ViewGroupThree: onTouchEvent:action:0ViewGroupTwo: onTouchEvent:action:0ViewGroupOne: onTouchEvent:action:0

這次的結果在事件分發的時流程大體一樣,但是也有和上一次的有很大的不同的地方:
1.只有MotionEvent.ACTION_DOWN的事件
2.onTouchEvent由ViewGroupThree到ViewGroupOne依次執行。
對於第一點是因為我們將CustomView的onTouchEvent的返回值修改為false,也就是CustomView不消費此事件,而默認情況下ViewGroup也不消費(onTouchEvent返回false),所以就沒有人消費事件了,那麼後邊的事件也就不再處理了。
對於對二點事件的消費和之前的分發流程至父到子傳遞過程不同,它是從子到父傳遞,也即對於自己不處理的事件,會傳遞給父布局讓其處理,以此類推。直到傳遞到有人處理或者壓根就沒人處理就和上邊的情況一樣。

上邊講了半天都沒有見到onInterceptTouchEvent這個方法有什麼用,現在我們選擇將ViewGroupTwo的onInterceptTouchEvent返回值從默認的false該為true。看看程序執行結果: ViewGroupOne: dispatchTouchEvent:action:0ViewGroupOne: onInterceptTouchEvent:action:0ViewGroupTwo: dispatchTouchEvent:action:0ViewGroupTwo: :action:0ViewGroupTwo: onTouchEvent:action:0ViewGroupOne: onTouchEvent:action:0結果很明顯ViewGroupThree和CustomButton的方法都沒有回調(其實收到了一個ACTION_CANCEL的事件),這就是intercept的意義所在,直接在ViewGroupTwo這一層將事件攔截了,然後直接交由ViewGroupTwo的onTouchEvent處理。

如果ViewGroupTwo的onTouchEvent消費事件(返回true)那麼觸摸產生的一系列事件都將傳遞到ViewGroupTwo的onTouchEvent中進行消費,並且不會再向上傳遞給ViewGroupOne的onTouchEvent:

ViewGroupOne: dispatchTouchEvent:action:0ViewGroupOne: onInterceptTouchEvent:action:0ViewGroupTwo: dispatchTouchEvent:action:0ViewGroupTwo: :action:0ViewGroupTwo: onTouchEvent:action:0ViewGroupOne: dispatchTouchEvent:action:1ViewGroupOne: :action:1ViewGroupTwo: dispatchTouchEvent:action:1ViewGroupTwo: onTouchEvent:action:1

以上結果印證上邊的分析。如下圖來表示該流程:

最後在強調一下OnTouchListener和OnClickListener對分發流程和事件消費的影響。如果閱讀ViewGroup.dispatchTouchEvent方法源碼不難得出優先順序:
dispatchTouchEvent->onInterceptTouchEvent->onTouch->onTouchEvent->onClick
也就是說OnTouchListener不會對dispatch分發過程產生影響,而會影響事件的消費,因為在分析View時已經講過了,如果設置了OnTouchListener監聽,並且在onTouch方法中進行事件消費(返回true)的話,將不會再回調對應View的onTouchEvent方法。大家可以底下試驗一下,這裡篇幅原因不再給出了。

0x04 Activity的事件分發

本來不想講Activity的事件分發的,因為要涉及到WMS比較複雜,不過想真正理解Android的事件的分發這一塊還真的繞不過。因為上文中的事件流都是來源自Activity的。不過我還是不深入源碼,如果對源碼感興趣可以參考

Android FrameWork——Touch事件派發過程詳解

我們都知道通常的Activity的UI框架,類似下圖:

如果了解過AMS機制我們就知道,Activity實際並不會去控制UI視圖,它主要是去控制生命周期和處理事件。真正的視圖控制是Window。Window代表一個窗口,它持有一個DecorView,這個DecorView繼承自FrameLayout,它是Activity的根布局。而他的內部通常又包含一個LinearLayout,這個LinearLayout有包括title和id為content的部分(當然這個也和Android系統版本和你設置的style有關係)。我們在Activity中調用setContentView設置的布局就是被添加到content部分了。Android給我提供了方便獲取content對應布局的方式,只需要調用ViewGroup content=(ViewGroup)findViewById(R.android.id.content)即可。

在WindowManager(WindowManagerImpl)體系中,有一個ViewRoot(ViewRootImpl)它是連接WindowManagerService和DecorView的紐帶 ,View的三大流程(測量(measure),布局(layout),繪製(draw))均通過ViewRoot來完成。ViewRoot 實現了ViewParent介面,這讓它可以作為View的名義上的父視圖 。RootView繼承了Handler類,可以接收事件並分發,Android的所有

觸屏事件、按鍵事件、界面刷新

等事件都是通過ViewRoot進行分發的。ViewRoot可以被理解為「View樹的管理者」——它有一個mView成員變數,指向的就是DecorView。

簡單點說當有觸摸事件產生,會通過WMS傳遞到ViewRoot,ViewRoot再分發給對應的DecorView(FrameLayout它是一個ViewGroup),再按照上文ViewGroup的分發機制,向下依次傳遞,最終會傳遞到我們setContentView設置布局的根布局,接下來就是我們上文已經熟悉的流程了。

掌握了這些,在開發中遇到的很多手勢衝突的問題就知道產生衝突的原因和解決衝突的思路了。



熱門推薦

本文由 yidianzixun 提供 原文連結

寵物協尋 相信 終究能找到回家的路
寫了7763篇文章,獲得2次喜歡
留言回覆
回覆
精彩推薦