Android 事件分发介绍
- 一、目的
- 二、環(huán)境
-
三、相關(guān)概念
- 3.1 事件分發(fā)
-
四、詳細(xì)設(shè)計
-
4.1應(yīng)用布局
- 4.1.1 應(yīng)用布局結(jié)構(gòu)
- 4.1.2 LayoutInspector
-
4.2 關(guān)鍵View&方法
- 4.2.1 相關(guān)View
- 4.2.2 相關(guān)方法
- 4.2.3 View與方法關(guān)系
-
4.3 事件分發(fā)概念圖
- 4.3.1 事件分發(fā)類圖
- 4.3.2 事件分發(fā)模型圖
-
4.4 Activity組件
- 4.4.1 Activity->dispatchTouchEvent()
- 4.4.2 Activity->getWindow()
- 4.4.3 Activity->onTouchEvent()
-
4.5 ViewGroup組件
- 4.5.1 ViewGroup->dispatchTouchEvent()
- 4.5.2 ViewGroup->dispatchTransformedTouchEvent()
-
4.6 View組件
- 4.6.1 View->dispatchTouchEvent()
- 4.6.2 OnTouchListener->onTouch()
- 4.6.3 View->onTouchEvent()
- 4.7 例子-點(diǎn)擊事件時序圖
-
4.1應(yīng)用布局
- 五、小結(jié)&問題點(diǎn)
- 六、代碼倉庫地址
- 七、參考資料
一、目的
????????最開始接觸Android時,僅僅是知道Android系統(tǒng)存在的點(diǎn)擊事件、觸摸事件,但是并不清楚這些事件的由來。
????????之后,在面試Oppo和美圖時,皆有問到Android的事件分發(fā)機(jī)制,但是都被問得很懵逼,歸根到底都是對于其實現(xiàn)邏輯的不理解。
????????隨后,想去彌補(bǔ)該模塊的不足,瀏覽很多關(guān)于Android事件分發(fā)的博文,但仍存在一些疑惑,就想著去閱讀下源碼,整理下筆記,希望對同學(xué)們有幫助。
二、環(huán)境
- 版本:Android 11
- 平臺:展銳 SPRD8541E
三、相關(guān)概念
3.1 事件分發(fā)
????????Android 中 View 的布局是一個樹形結(jié)構(gòu),各個 ViewGroup 和 View 是按樹形結(jié)構(gòu)嵌套布局的,從而會出現(xiàn)用戶觸摸的位置坐標(biāo)可能會落在多個 View 的范圍內(nèi),這樣就不知道哪個 View 來響應(yīng)這個事件,為了解決這一問題,就出現(xiàn)了事件分發(fā)機(jī)制。
四、詳細(xì)設(shè)計
4.1應(yīng)用布局
4.1.1 應(yīng)用布局結(jié)構(gòu)
????????如下為一個Activity打開后,其對應(yīng)視圖的層級結(jié)構(gòu)。
4.1.2 LayoutInspector
????????Layout Inspector是google提供給我們進(jìn)行布局分析的一個工具,也是目前google在棄用Hierarchy View后推薦使用的一款布局分析工具。
4.2 關(guān)鍵View&方法
4.2.1 相關(guān)View
| 組件 | 描述 |
|---|---|
| Activity | Android事件分發(fā)的起始端,其為一個window窗口,內(nèi)部持有Decorder視圖,該視圖為當(dāng)前窗體的根節(jié)點(diǎn),同時,它也是一個ViewGroup容器。 |
| ViewGroup | Android中ViewGroup是一個布局容器,可以嵌套多個 ViewGroup 和 View,事件傳遞和攔截都由 ViewGroup 完成。 |
| View | 事件傳遞的最末端,要么消費(fèi)事件,要么不消費(fèi)把事件傳遞給父容器 |
4.2.2 相關(guān)方法
| 方法 | 描述 |
|---|---|
| dispatchTouchEvent | 分發(fā)事件 |
| onInterceptTouchEvent | 攔截事件 |
| onTouchEvent | 觸摸事件 |
4.2.3 View與方法關(guān)系
| 組件 | dispatchTouchEvent | onInterceptTouchEvent | onTouchEvent |
|---|---|---|---|
| Activity | ? | ? | ? |
| ViewGroup | ? | ? | ? |
| View | ? | ? | ? |
4.3 事件分發(fā)概念圖
4.3.1 事件分發(fā)類圖
4.3.2 事件分發(fā)模型圖
????????Android的ACTION_DOWN事件分發(fā)如圖,從1-9步驟,描述一個down事件的分發(fā)過程,如果大家能懂,就不用看下面文字描述了(寫完這個篇幅,感覺文字好多,不好理解!)
- ACTION_DOWN事件觸發(fā)。 當(dāng)我們手指觸摸屏幕,tp驅(qū)動會響應(yīng)中斷,通過ims輸入系統(tǒng),將down事件的相關(guān)信息發(fā)送到當(dāng)前的窗口,即當(dāng)前的Activity。
- Activity事件分發(fā)。 會引用dispatchTouchEvent()方法,對down事件分發(fā)。Activity本身會持有一個window對象,window對象的實現(xiàn)類PhoneWindow會持有一個DecorView對象,DecorView是一個ViewGroup對象,即我們可以理解為,Activity最終會將事件分發(fā)給下一個節(jié)點(diǎn)——ViewGroup。
- ViewGroup事件攔截。 ViewGroup接收到事件后,會先引用onInterceptTouchEvent(),查看當(dāng)前的視圖容器是否做事件攔截。
- ViewGroup消費(fèi)事件。 如當(dāng)前的ViewGroup對事件進(jìn)行攔截,即會調(diào)用onTouchEvent(),對事件消費(fèi)。
- ViewGroup事件不攔截。 則ViewGroup會繼續(xù)遍歷自身的子節(jié)點(diǎn),并且當(dāng)事件的坐標(biāo)位于子節(jié)點(diǎn)上,則繼續(xù)下發(fā)到下一個節(jié)點(diǎn)。ViewGroup的子節(jié)點(diǎn)有可能是View,也可能是ViewGroup(當(dāng)然,ViewGroup最后也是繼承于View的,突然感覺有點(diǎn)廢話)。
- ViewGroup事件分發(fā)。 目標(biāo)視圖如果是ViewGroup,會引用其super類的dispatchTouchEvent()方法,即事件下發(fā),不管目標(biāo)視圖是View或者ViewGroup最終引用的是View類的分發(fā)方法。
- View事件消費(fèi)。 在View的dispatchTouchEvent()方法中會根據(jù)當(dāng)前View是否可以點(diǎn)擊、onTouch()是否消費(fèi)、onTouchEvent()是否消費(fèi)等條件,來判斷當(dāng)前是否為目標(biāo)View。
- View事件未消費(fèi)。 View事件未消費(fèi),則其父節(jié)點(diǎn),即ViewGroup會調(diào)用onTouchEvent()方法,并根據(jù)返回值來決定是否消費(fèi)事件。
- ViewGroup事件未消費(fèi)。 ViewGroup事件未消費(fèi),擇其父節(jié)點(diǎn),即Actviity會調(diào)用onTouchEvent()方法
PS:
(1) ACTION_MOVE和ACTION_UP事件,流程與ACTION_DOWN的分發(fā)過程基本一致,MOVE和UP事件也是通過Activity開始,借助DOWN事件產(chǎn)生的目標(biāo)View,逐級分發(fā)。
(2) ACTION_CANCEL事件,是在down與up、move事件切換過程中,事件被攔截,兩次的touchTarget目標(biāo)view不一致,而產(chǎn)生的事件。用于對之前的目標(biāo)View做恢復(fù)處理,避免down與up/move事件不對稱。
4.4 Activity組件
4.4.1 Activity->dispatchTouchEvent()
????????底層上報的事件信息,最終會引用到該方法。Activity會持有一個根視圖DecordView,事件最終會往該ViewGroup分發(fā),如所有的View都未消費(fèi)該事件,則最終由Activity的onTouchEvent()
來兜底處理。
@frameworks\base\core\java\android\app\Activity.java
public boolean dispatchTouchEvent(MotionEvent ev) {
...
if (getWindow().superDispatchTouchEvent(ev)) {//Step 1. 查看Window對應(yīng)的View是否分發(fā)該事件
return true;
}
return onTouchEvent(ev);//Step 2. 如果沒有組件消費(fèi)事件,則由Activity兜底處理
}
4.4.2 Activity->getWindow()
????????我們每次啟動一個Activity的組件,會先打開一個window窗口,而PhoneWindow是Window唯一的實現(xiàn)類。
@frameworks\base\core\java\android\app\Activity.java
public Window getWindow() {
return mWindow;
}
final void attach(Context context, ActivityThread aThread...) {
...
mWindow = new PhoneWindow(this, window, activityConfigCallback);//PhoneWindow是Window窗口唯一的實現(xiàn)類
...
}
????????PhoneWindow對象內(nèi)部持有DecorView對象,而該View正是該窗口對應(yīng)的視圖容器,也是根節(jié)點(diǎn)。(此部分不具體分析)
@frameworks\base\core\java\com\android\internal\policy\PhoneWindow.java
public class PhoneWindow extends Window implements MenuBuilder. Callback {
...
private DecorView mDecor;//
...
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);//往View的根節(jié)點(diǎn)分發(fā)事件
}
}
4.4.3 Activity->onTouchEvent()
????????Activity的onTouchEvent方法,是在沒有任何組件消費(fèi)事件的情況下,觸發(fā)的方法。
@frameworks\base\core\java\android\app\Activity.java
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
4.5 ViewGroup組件
????????ViewGroup組件在整個事件分發(fā)的模型中,既有分發(fā)事件的責(zé)任,又要具備處理事件的能力,真的典型的當(dāng)?shù)之?dāng)媽。
????????當(dāng)Activity調(diào)用superDispatchTouchEvent,即最終會使用到DecorView的superDispatchTouchEvent方法,而DecorView是繼承于ViewGroup,即最終會引用ViewGroup的dispatchTouchEvent方法。
4.5.1 ViewGroup->dispatchTouchEvent()
此方法為事件分發(fā)最核心的代碼。其主要處理如下四件事情:
Setp 1. 重置事件。 一次完整觸摸的事件:DOWN -> MOVE -> UP,即我們可以理解為DOWN是所有觸摸事件的起始事件。當(dāng)輸入事件是ACTION_DOWN時,重置觸摸事件狀態(tài)信息,避免產(chǎn)生干擾。
Step 2. 攔截事件。 攔截事件是ViewGroup特有的方法,用于攔截事件,并將該事件分發(fā)給自己消費(fèi),防止事件繼續(xù)下發(fā)。
Step 3.查找目標(biāo)View。 查找目標(biāo)View主要針對于Down事件。當(dāng)ViewGroup未攔截事件,且輸入事件是ACTION_DOWN時,會遍歷該ViewGroup的所有子節(jié)點(diǎn),并根據(jù)觸摸位置的坐標(biāo),來決定當(dāng)前子節(jié)點(diǎn)是否是下一級目標(biāo)View。當(dāng)找到目標(biāo)View節(jié)點(diǎn)后,會分發(fā)Down事件,并記錄該節(jié)點(diǎn)信息。
Step 4.下發(fā)事件。 如果目標(biāo)View未找到的話,則會將事件交由自己的onTouchEvent()處理;如果目標(biāo)View已經(jīng)找到,則Down事件就此結(jié)束(此處暫不考慮多指場景);Move和Up事件將繼續(xù)下發(fā)(默認(rèn)情況下Move、Up和Down事件是成對出現(xiàn)的,如果目標(biāo)View已經(jīng)存在,則Down事件已經(jīng)下發(fā),即意味著Move和Up事件也需要下發(fā)給對應(yīng)的目標(biāo)View)。
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
if (actionMasked == MotionEvent.ACTION_DOWN) {//Step 1.重置事件信息,避免影響下一次事件
cancelAndClearTouchTargets(ev);
resetTouchState();
}
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);//Step 2.攔截事件
ev.setAction(action); // restore action in case it was changed
}
}
...
if (!canceled && !intercepted) {//Step 3.查找目標(biāo)View
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
...
if (newTouchTarget == null && childrenCount != 0) {
...
for (int i = childrenCount - 1; i >= 0; i--) {//遍歷所有的子節(jié)點(diǎn)
...
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {// 子節(jié)點(diǎn)不可以接收事件,或者觸摸位置不在子節(jié)點(diǎn)的范圍上
continue;
}
...
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {//找到目標(biāo)View
...
break;
}
}
...
}
...
}
}
//Step 4.根據(jù)找到的目標(biāo)View情況,繼續(xù)下發(fā)事件
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);//沒有找到目標(biāo)View或者事件被攔截,事件下發(fā)給自己
} else {
...
while (target != null) {//多組數(shù)據(jù),一般是指多指場景
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {//此場景一般是down事件
handled = true
} else {
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {//此場景一般是move、up事件
handled = true;
}
...
}
predecessor = target;
target = next;
}
...
}
...
return handled;
}
4.5.2 ViewGroup->dispatchTransformedTouchEvent()
事件分發(fā)關(guān)鍵方法,主要用于向目標(biāo)View分發(fā)事件,具體邏輯如下:
Step 1.Cancel事件分發(fā)。 之前我們提過Down和Up事件是成對存在的,如果Down事件已經(jīng)下發(fā)的情況下,Up事件卻因為事件攔截等原因,未能下發(fā)給目標(biāo)View,目標(biāo)View未收到Up事件,此時就可能產(chǎn)生一些按壓狀態(tài)的異常問題,故,在當(dāng)前場景下,將會分發(fā)一個ACTION_CANCEL事件給目標(biāo)View。
Step 2.事件處理。 如果事件未找到目標(biāo)View,則child會為null,此時的事件將由自身處理。
Step 3.事件分發(fā)。 如果事件還存在目標(biāo)View,則此時的事件會再分發(fā)。
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
...
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {//Step 1.下發(fā)取消事件
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
...
if (child == null) {//Step 2.如果事件未找到目標(biāo)View,則觸摸事件會發(fā)給自己
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
handled = child.dispatchTouchEvent(transformedEvent);//Step 3.找到目標(biāo)View,事件下發(fā)給子節(jié)點(diǎn)
}
...
return handled;
}
4.6 View組件
????????View組件在事件處理模型中,主要是處理事件。我們知道ViewGroup,也是繼承于View,所以ViewGroup也是同樣具備View的處理事件能力。
4.6.1 View->dispatchTouchEvent()
Step 1.觸發(fā)onTouch()方法。 如果當(dāng)前的View是可點(diǎn)擊的,且配置了onTouch事件監(jiān)聽,則觸發(fā)該View的onTouch()方法。
Step 2.觸發(fā)onTouchEvent()方法。 如果該事件在上一步的onTouch()函數(shù)中未被消費(fèi),則觸發(fā)onTouchEvent()方法。
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
...
if (onFilterTouchEventForSecurity(event)) {
...
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {//Step 1.觸發(fā)onTouch事件
result = true;
}
if (!result && onTouchEvent(event)) {//Step 2.如onTouch未消費(fèi),觸發(fā)onTouchEvent事件
result = true;
}
}
...
return result;
}
4.6.2 OnTouchListener->onTouch()
????????View可以設(shè)置事件監(jiān)聽,用于監(jiān)聽onTouch事件的回調(diào),當(dāng)然,像我們常見的onClick()、onLongClick()等事件也可監(jiān)聽,其相關(guān)源碼如下:
@frameworks\base\core\java\android\view\View.java
public void setOnTouchListener(OnTouchListener l) {//設(shè)置onTouch監(jiān)聽
getListenerInfo().mOnTouchListener = l;
}
ListenerInfo getListenerInfo() {
if (mListenerInfo != null) {
return mListenerInfo;
}
mListenerInfo = new ListenerInfo();
return mListenerInfo;
}
public interface OnTouchListener {//Touch接口,用于回調(diào)onTouch事件
boolean onTouch(View v, MotionEvent event);
}
4.6.3 View->onTouchEvent()
????????事件如未被onTouch消費(fèi)掉,則會引用到onTouchEvent()方法,該方法會涉及ACTION_UP、ACTION_DOWN、ACTION_CANCEL、ACTION_MOVE事件的處理,View的onClick()、onLongClick()也是由該方法觸發(fā)。此外,如果當(dāng)前的View是可點(diǎn)擊的話,則直接消費(fèi)該事件。
public boolean onTouchEvent(MotionEvent event) {
...
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;//當(dāng)前View是否可點(diǎn)擊
...
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP://抬起
...
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
if (!focusTaken) {
removeLongPressCallback();//若有長按事件未處理,則移除長按事件
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {//通過Hanlder將點(diǎn)擊事件發(fā)送到主線程執(zhí)行
performClickInternal();//如果不成功,則直接引用點(diǎn)擊事件
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();//更新按鈕的按壓事件
}
...
break;
case MotionEvent.ACTION_DOWN://按下
...
if (isInScrollingContainer) {//在可滾動的容器內(nèi),為了容錯,延遲點(diǎn)擊
...
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
setPressed(true, x, y);//設(shè)置按下的狀態(tài)
checkForLongClick(
ViewConfiguration.getLongPressTimeout(),
x,
y,
TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);//開啟一個長按延時事件
}
break;
case MotionEvent.ACTION_CANCEL://取消
...
break;
case MotionEvent.ACTION_MOVE://移動
...
break;
}
return true;//如果是可點(diǎn)擊的View,即消費(fèi)事件
}
...
return false;
}
4.7 例子-點(diǎn)擊事件時序圖
????????如下是Android的點(diǎn)擊事件時序圖,如果能夠理解單擊事件的由來,對于整個事件分發(fā)的知識要點(diǎn)已大體掌握。
五、小結(jié)&問題點(diǎn)
- 事件分發(fā)流程?包括ACTION_DWON、ACTION_UP、ACTION_MOVE事件的處理過程;
- ACTION_CANCEL事件的使用場景?父控件對move事件攔截場景?
- 單擊、長按、觸摸事件的產(chǎn)生過程?
- 點(diǎn)擊一個View未抬起,同時move該事件直至離開當(dāng)前View的范圍,處理過程如何?
- 如果所有View都未消費(fèi)事件,流程如何?
- ViewPage+ListView,左右滑動和上下滑動沖突的解決問題?即事件攔截過程?
- 普通的View是根據(jù)什么來決定是否消費(fèi)事件,例如Button?
=>答:如無重寫onTouchEvent事件,根據(jù)當(dāng)前的View是否可點(diǎn)擊,來決定是否消費(fèi)事件。
????????我最開始沒有看源碼,直接去看博客上的內(nèi)容,彎彎繞繞,似懂非懂。在面試的過程中,面試官舉個場景分析流程,我都懵逼,分析不出來,現(xiàn)場很尷尬。之后看源碼,整體流程代碼量很少,感嘆于Android事件分發(fā)流程的設(shè)計,很少的代碼量,卻承載了很重要的功能,而沒有見過該模塊發(fā)生過異常。
????????多讀書,多看報,少吃零食,多睡覺!
六、代碼倉庫地址
Demo地址:? https://gitee.com/linzhiqin/custom-demo
七、參考資料
https://zhuanlan.zhihu.com/p/623664769?utm_id=0
事件分發(fā)視頻(總結(jié)很好,但是得先理解基本概念,才方便學(xué)習(xí))
https://www.bilibili.com/video/BV1sy4y1W7az?p=1&vd_source=f222e3bf3083cad8d9f660629bc47c16
總結(jié)
以上是生活随笔為你收集整理的Android 事件分发介绍的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Golang实现JAVA虚拟机-指令集和
- 下一篇: Prometheus 监控告警系统搭建(