前言
Android事件分發(fā)機(jī)制是Android開發(fā)者必須了解的基礎(chǔ)缕碎。
目錄
一. 基礎(chǔ)認(rèn)知
1.1 事件分發(fā)的由來
安卓的View是樹形結(jié)構(gòu)的,View可能會(huì)重疊在一起池户,當(dāng)我們點(diǎn)擊的地方有多個(gè)View都可以響應(yīng)的時(shí)候咏雌,這個(gè)點(diǎn)擊事件應(yīng)該給誰呢?為了解決這一個(gè)問題校焦,就有了事件分發(fā)機(jī)制赊抖。
1.2 事件分發(fā)的 "事件" 是指什么?
點(diǎn)擊事件 (Touch事件) 寨典。具體介紹如下:
特別說明:事件列氛雪,即指從手指接觸屏幕至手指離開屏幕這個(gè)過程產(chǎn)生的一系列事件。一般情況下耸成,事件列都是以DOWN事件開始报亩、UP事件結(jié)束,中間有無數(shù)的MOVE事件井氢。
1.3 事件分發(fā)的本質(zhì)
將點(diǎn)擊事件(MotionEvent)傳遞到某個(gè)具體的View & 處理的整個(gè)過程弦追。
即 事件傳遞的過程 = 分發(fā)過程。
1.4 事件在哪些對(duì)象之間進(jìn)行傳遞花竞?
答:Activity劲件、ViewGroup、View左胞。
1.5 事件分發(fā)的順序
即 事件傳遞的順序:Activity
-> ViewGroup
-> View
即:1個(gè)點(diǎn)擊事件發(fā)生后定罢,事件先傳到
Activity
蟀淮、再傳到ViewGroup
、最終再傳到View
1.6 事件分發(fā)過程由哪些方法協(xié)作完成婿崭?
答:dispatchTouchEvent()
俭嘁、onInterceptTouchEvent()
和 onTouchEvent()
二. 事件分發(fā)機(jī)制流程詳細(xì)分析
主要包括:Activity
事件分發(fā)機(jī)制躺枕、ViewGroup
事件分發(fā)機(jī)制、View
事件分發(fā)機(jī)制
2.1 Activity事件分發(fā)機(jī)制
事件分發(fā)機(jī)制供填,首先會(huì)將點(diǎn)擊事件傳遞到Activity
中拐云,具體是執(zhí)行dispatchTouchEvent()
進(jìn)行事件分發(fā)。
源碼分析
/**
* 源碼分析:Activity.dispatchTouchEvent()
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
// 僅貼出核心代碼
// ->>分析1
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
// 若getWindow().superDispatchTouchEvent(ev)的返回true
// 則Activity.dispatchTouchEvent()就返回true近她,則方法結(jié)束叉瘩。即 :該點(diǎn)擊事件停止往下傳遞 & 事件傳遞過程結(jié)束
// 否則:繼續(xù)往下調(diào)用Activity.onTouchEvent
}
// ->>分析3
return onTouchEvent(ev);
}
/**
* 分析1:getWindow().superDispatchTouchEvent(ev)
* 說明:
* a. getWindow() = 獲取Window類的對(duì)象
* b. Window類是抽象類,其唯一實(shí)現(xiàn)類 = PhoneWindow類
* c. Window類的superDispatchTouchEvent() = 1個(gè)抽象方法粘捎,由子類PhoneWindow類實(shí)現(xiàn)
*/
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
// mDecor = 頂層View(DecorView)的實(shí)例對(duì)象
// ->> 分析2
}
/**
* 分析2:mDecor.superDispatchTouchEvent(event)
* 定義:屬于頂層View(DecorView)
* 說明:
* a. DecorView類是PhoneWindow類的一個(gè)內(nèi)部類
* b. DecorView繼承自FrameLayout薇缅,是所有界面的父類
* c. FrameLayout是ViewGroup的子類危彩,故DecorView的間接父類 = ViewGroup
*/
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
// 調(diào)用父類的方法 = ViewGroup的dispatchTouchEvent()
// 即將事件傳遞到ViewGroup去處理,詳細(xì)請(qǐng)看后續(xù)章節(jié)分析的ViewGroup的事件分發(fā)機(jī)制
}
// 回到最初的分析2入口處
/**
* 分析3:Activity.onTouchEvent()
* 調(diào)用場(chǎng)景:當(dāng)一個(gè)點(diǎn)擊事件未被Activity下任何一個(gè)View接收/處理時(shí)泳桦,就會(huì)調(diào)用該方法
*/
public boolean onTouchEvent(MotionEvent event) {
// ->> 分析5
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
// 即 只有在點(diǎn)擊事件在Window邊界外才會(huì)返回true汤徽,一般情況都返回false,分析完畢
}
/**
* 分析4:mWindow.shouldCloseOnTouch(this, event)
* 作用:主要是對(duì)于處理邊界外點(diǎn)擊事件的判斷:是否是DOWN事件灸撰,event的坐標(biāo)是否在邊界內(nèi)等
*/
public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
if (mCloseOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN
&& isOutOfBounds(context, event) && peekDecorView() != null) {
// 返回true:說明事件在邊界外谒府,即 消費(fèi)事件
return true;
}
// 返回false:在邊界內(nèi),即未消費(fèi)(默認(rèn))
return false;
}
源碼總結(jié)
當(dāng)一個(gè)點(diǎn)擊事件發(fā)生時(shí)浮毯,從Activity的事件分發(fā)開始(Activity.dispatchTouchEvent()
)完疫,流程總結(jié)如下:
核心方法總結(jié)
主要包括:dispatchTouchEvent()、onTouchEvent() 總結(jié)如下:
那么ViewGroup
的dispatchTouchEvent()
什么時(shí)候返回true / false? 請(qǐng)繼續(xù)往下看ViewGroup
事件的分發(fā)機(jī)制债蓝。
2.2 ViewGroup事件分發(fā)機(jī)制
從上面Activity
的事件分發(fā)機(jī)制可知趋惨,在Activity.dispatchTouchEvent()
實(shí)現(xiàn)了將事件從Activity
->ViewGroup
的傳遞,ViewGroup
的事件分發(fā)機(jī)制從dispatchTouchEvent()
開始惦蚊。
源碼分析
/**
* 源碼分析:ViewGroup.dispatchTouchEvent()
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
// 僅貼出關(guān)鍵代碼
...
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
// 分析1:ViewGroup每次事件分發(fā)時(shí)器虾,都需調(diào)用onInterceptTouchEvent()詢問是否攔截事件
// 判斷值1-disallowIntercept:是否禁用事件攔截的功能(默認(rèn)是false),可通過調(diào)用requestDisallowInterceptTouchEvent()修改
// 判斷值2-!onInterceptTouchEvent(ev) :對(duì)onInterceptTouchEvent()返回值取反
// a. 若在onInterceptTouchEvent()中返回false蹦锋,即不攔截事件兆沙,從而進(jìn)入到條件判斷的內(nèi)部
// b. 若在onInterceptTouchEvent()中返回true,即攔截事件莉掂,從而跳出了該條件判斷
// c. 關(guān)于onInterceptTouchEvent() ->>分析1
// 分析2
// 1. 通過for循環(huán)葛圃,遍歷當(dāng)前ViewGroup下的所有子View
for (int i = count - 1; i >= 0; i--) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null) {
child.getHitRect(frame);
// 2. 判斷當(dāng)前遍歷的View是不是正在點(diǎn)擊的View,從而找到當(dāng)前被點(diǎn)擊的View
if (frame.contains(scrolledXInt, scrolledYInt)) {
final float xc = scrolledXFloat - child.mLeft;
final float yc = scrolledYFloat - child.mTop;
ev.setLocation(xc, yc);
child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
// 3. 條件判斷的內(nèi)部調(diào)用了該View的dispatchTouchEvent()
// 即 實(shí)現(xiàn)了點(diǎn)擊事件從ViewGroup到子View的傳遞(具體請(qǐng)看下面章節(jié)介紹的View事件分發(fā)機(jī)制)
if (child.dispatchTouchEvent(ev)) {
// 調(diào)用子View的dispatchTouchEvent后是有返回值的
// 若該控件可點(diǎn)擊憎妙,那么點(diǎn)擊時(shí)dispatchTouchEvent的返回值必定是true库正,因此會(huì)導(dǎo)致條件判斷成立
// 于是給ViewGroup的dispatchTouchEvent()直接返回了true,即直接跳出
// 即該子View把ViewGroup的點(diǎn)擊事件消費(fèi)掉了
mMotionTarget = child;
return true;
}
}
}
}
}
}
...
return super.dispatchTouchEvent(ev);
// 若無任何View接收事件(如點(diǎn)擊空白處)/ViewGroup本身攔截了事件(復(fù)寫了onInterceptTouchEvent()返回true)
// 會(huì)調(diào)用ViewGroup父類的dispatchTouchEvent()厘唾,即View.dispatchTouchEvent()
// 因此會(huì)執(zhí)行ViewGroup的onTouch() -> onTouchEvent() -> performClick() -> onClick()褥符,即自己處理該事件,事件不會(huì)往下傳遞
// 具體請(qǐng)參考View事件分發(fā)機(jī)制中的View.dispatchTouchEvent()
...
}
/**
* 分析1:ViewGroup.onInterceptTouchEvent()
* 作用:是否攔截事件
* 說明:
* a. 返回false:不攔截(默認(rèn))
* b. 返回true:攔截抚垃,即事件停止往下傳遞(需手動(dòng)復(fù)寫onInterceptTouchEvent()其返回true)
*/
public boolean onInterceptTouchEvent(MotionEvent ev) {
// 默認(rèn)不攔截
return false;
}
// 回到調(diào)用原處
源碼總結(jié)
Android事件分發(fā)傳遞到Acitivity
后喷楣,總是先傳遞到ViewGroup
、再傳遞到View
鹤树。流程總結(jié)如下:(假設(shè)已經(jīng)經(jīng)過了Acitivity事件分發(fā)傳遞并傳遞到ViewGroup
)
核心方法總結(jié)
主要包括:dispatchTouchEvent()
铣焊、onTouchEvent()
、onInterceptTouchEvent()
總結(jié)如下:
實(shí)例分析
1. 布局說明
2. 測(cè)試代碼
布局文件:activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/my_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:focusableInTouchMode="true"
android:orientation="vertical">
<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="按鈕1" />
<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="按鈕2" />
</LinearLayout>
核心代碼:MainActivity.java
public class MainActivity extends AppCompatActivity {
Button button1,button2;
ViewGroup myLayout;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
button1 = (Button)findViewById(R.id.button1);
button2 = (Button)findViewById(R.id.button2);
myLayout = (LinearLayout)findViewById(R.id.my_layout);
// 1.為ViewGroup布局設(shè)置監(jiān)聽事件
myLayout.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d("TAG", "點(diǎn)擊了ViewGroup");
}
});
// 2. 為按鈕1設(shè)置監(jiān)聽事件
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d("TAG", "點(diǎn)擊了button1");
}
});
// 3. 為按鈕2設(shè)置監(jiān)聽事件
button2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d("TAG", "點(diǎn)擊了button2");
}
});
}
}
3. 測(cè)試結(jié)果
// 點(diǎn)擊按鈕1罕伯,輸出如下
點(diǎn)擊了button1
// 點(diǎn)擊按鈕2曲伊,輸出如下
點(diǎn)擊了button2
// 點(diǎn)擊空白處,輸出如下
點(diǎn)擊了ViewGroup
4. 結(jié)果分析
- 點(diǎn)擊Button時(shí)追他,因?yàn)?code>ViewGroup默認(rèn)不攔截坟募,所以事件會(huì)傳遞到子View
Button
岛蚤,于是執(zhí)行Button.onClick()
。- 此時(shí)
ViewGroup. dispatchTouchEvent()
會(huì)直接返回true
婿屹,所以ViewGroup
自身不會(huì)處理該事件灭美,于是ViewGroupLayout
的dispatchTouchEvent()
不會(huì)執(zhí)行,所以注冊(cè)的onTouch()
不會(huì)執(zhí)行昂利,即onTouchEvent()
->performClick()
->onClick()
整個(gè)鏈路都不會(huì)執(zhí)行届腐,所以最后不會(huì)執(zhí)行ViewGroup
設(shè)置的onClick()
里。- 點(diǎn)擊空白區(qū)域時(shí)蜂奸,
ViewGroup. dispatchTouchEvent()
里遍歷所有子View希望找到被點(diǎn)擊子View時(shí)找不到犁苏,所以ViewGroup
自身會(huì)處理該事件,于是執(zhí)行onTouchEvent()
->performClick()
->onClick()
扩所,最終執(zhí)行ViewGroupLayout
的設(shè)置的onClick()
围详。
2.3 View事件分發(fā)機(jī)制
從上面ViewGroup
事件分發(fā)機(jī)制知道,View事件分發(fā)機(jī)制從dispatchTouchEvent()
開始
源碼分析
/**
* 源碼分析:View.dispatchTouchEvent()
*/
public boolean dispatchTouchEvent(MotionEvent event) {
if ( (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener != null &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}
// 說明:只有以下3個(gè)條件都為真祖屏,dispatchTouchEvent()才返回true助赞;否則執(zhí)行onTouchEvent()
// 1. (mViewFlags & ENABLED_MASK) == ENABLED
// 2. mOnTouchListener != null
// 3. mOnTouchListener.onTouch(this, event)
// 下面對(duì)這3個(gè)條件逐個(gè)分析
/**
* 條件1:(mViewFlags & ENABLED_MASK) == ENABLED
* 說明:
* 1. 該條件是判斷當(dāng)前點(diǎn)擊的控件是否enable
* 2. 由于很多View默認(rèn)enable,故該條件恒定為true(除非手動(dòng)設(shè)置為false)
*/
/**
* 條件2:mOnTouchListener != null
* 說明:
* 1. mOnTouchListener變量在View.setOnTouchListener()里賦值
* 2. 即只要給控件注冊(cè)了Touch事件袁勺,mOnTouchListener就一定被賦值(即不為空)
*/
public void setOnTouchListener(OnTouchListener l) {
mOnTouchListener = l;
}
/**
* 條件3:mOnTouchListener.onTouch(this, event)
* 說明:
* 1. 即回調(diào)控件注冊(cè)Touch事件時(shí)的onTouch()雹食;
* 2. 需手動(dòng)復(fù)寫設(shè)置,具體如下(以按鈕Button為例)
*/
button.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return false;
// 若在onTouch()返回true期丰,就會(huì)讓上述三個(gè)條件全部成立群叶,從而使得View.dispatchTouchEvent()直接返回true,事件分發(fā)結(jié)束
// 若在onTouch()返回false钝荡,就會(huì)使得上述三個(gè)條件不全部成立街立,從而使得View.dispatchTouchEvent()中跳出If,執(zhí)行onTouchEvent(event)
// onTouchEvent()源碼分析 -> 分析1
}
});
/**
* 分析1:onTouchEvent()
*/
public boolean onTouchEvent(MotionEvent event) {
... // 僅展示關(guān)鍵代碼
// 若該控件可點(diǎn)擊埠通,則進(jìn)入switch判斷中
if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
// 根據(jù)當(dāng)前事件類型進(jìn)行判斷處理
switch (event.getAction()) {
// a. 事件類型=抬起View(主要分析)
case MotionEvent.ACTION_UP:
performClick();
// ->>分析2
break;
// b. 事件類型=按下View
case MotionEvent.ACTION_DOWN:
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
break;
// c. 事件類型=結(jié)束事件
case MotionEvent.ACTION_CANCEL:
refreshDrawableState();
removeTapCallback();
break;
// d. 事件類型=滑動(dòng)View
case MotionEvent.ACTION_MOVE:
final int x = (int) event.getX();
final int y = (int) event.getY();
int slop = mTouchSlop;
if ((x < 0 - slop) || (x >= getWidth() + slop) ||
(y < 0 - slop) || (y >= getHeight() + slop)) {
removeTapCallback();
if ((mPrivateFlags & PRESSED) != 0) {
removeLongPressCallback();
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
}
}
break;
}
// 若該控件可點(diǎn)擊赎离,就一定返回true
return true;
}
// 若該控件不可點(diǎn)擊,就一定返回false
return false;
}
/**
* 分析2:performClick()
*/
public boolean performClick() {
if (mOnClickListener != null) {
// 只要通過setOnClickListener()為控件View注冊(cè)1個(gè)點(diǎn)擊事件
// 那么就會(huì)給mOnClickListener變量賦值(即不為空)
// 則會(huì)往下回調(diào)onClick() & performClick()返回true
playSoundEffect(SoundEffectConstants.CLICK);
mOnClickListener.onClick(this);
return true;
}
return false;
}
源碼總結(jié)
這里需要特別注意的是植阴,onTouch()
的執(zhí)行 先于onClick()
核心方法總結(jié)
主要包括: dispatchTouchEvent()
蟹瘾、onTouchEvent()
實(shí)例分析
在本示例中,將分析2種情況:
- 注冊(cè)Touch事件監(jiān)聽 且 在
onTouch()
返回false
- 注冊(cè)Touch事件監(jiān)聽 且 在
onTouch()
返回true
分析1:注冊(cè)Touch事件監(jiān)聽 且 在onTouch()返回false
代碼示例
// 1. 注冊(cè)Touch事件監(jiān)聽setOnTouchListener 且 在onTouch()返回false
button.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
System.out.println("執(zhí)行了onTouch(), 動(dòng)作是:" + event.getAction());
return false;
}
});
// 2. 注冊(cè)點(diǎn)擊事件OnClickListener()
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
System.out.println("執(zhí)行了onClick()");
}
});
測(cè)試結(jié)果
執(zhí)行了onTouch(), 動(dòng)作是:0
執(zhí)行了onTouch(), 動(dòng)作是:1
執(zhí)行了onClick()
測(cè)試結(jié)果說明
- 點(diǎn)擊按鈕會(huì)產(chǎn)生兩個(gè)類型的事件-按下View與抬起View掠手,所以會(huì)回調(diào)兩次
onTouch()
; - 因?yàn)?code>onTouch()返回了
false
狸捕,所以事件無被消費(fèi)喷鸽,會(huì)繼續(xù)往下傳遞,即調(diào)用View.onTouchEvent()
灸拍; - 調(diào)用
View.onTouchEvent()
時(shí)做祝,對(duì)于抬起View事件砾省,在調(diào)用performClick()
時(shí),因?yàn)樵O(shè)置了點(diǎn)擊事件混槐,所以會(huì)回調(diào)onClick()
编兄。
分析2:注冊(cè)Touch事件監(jiān)聽 且 在onTouch()返回true
代碼示例
// 1. 注冊(cè)Touch事件監(jiān)聽setOnTouchListener 且 在onTouch()返回false
button.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
System.out.println("執(zhí)行了onTouch(), 動(dòng)作是:" + event.getAction());
return true;
}
});
// 2. 注冊(cè)點(diǎn)擊事件OnClickListener()
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
System.out.println("執(zhí)行了onClick()");
}
});
測(cè)試結(jié)果
執(zhí)行了onTouch(), 動(dòng)作是:0
執(zhí)行了onTouch(), 動(dòng)作是:1
測(cè)試結(jié)果說明
- 點(diǎn)擊按鈕會(huì)產(chǎn)生兩個(gè)類型的事件-按下
View
與抬起View
,所以會(huì)回調(diào)兩次onTouch()
声登; - 因?yàn)?code>onTouch()返回
true
狠鸳,所以事件被消費(fèi),不會(huì)繼續(xù)往下傳遞悯嗓,View.dispatchTouchEvent()
直接返回true
件舵; - 所以最終不會(huì)調(diào)用
View.onTouchEvent()
,也不會(huì)調(diào)用onClick()
脯厨。
三. 事件分發(fā)機(jī)制流程總結(jié)
這個(gè)三個(gè)方法均有一個(gè) boolean(布爾) 類型的返回值铅祸,通過返回 true 和 false 來控制事件傳遞的流程。
PS: 從上表可以看到 Activity
和 View
都是沒有事件攔截的合武,這是因?yàn)椋?/p>
- Activity 作為原始的事件分發(fā)者临梗,如果 Activity 攔截了事件會(huì)導(dǎo)致整個(gè)屏幕都無法響應(yīng)事件,這肯定不是我們想要的效果稼跳。
- View最為事件傳遞的最末端盟庞,要么消費(fèi)掉事件,要么不處理進(jìn)行回傳岂贩,根本沒必要進(jìn)行事件攔截茫经。
View相關(guān)
Question: 為什么 View 會(huì)有 dispatchTouchEvent ?
A:View
可以注冊(cè)很多事件監(jiān)聽器,例如:?jiǎn)螕羰录?onClick
)萎津、長(zhǎng)按事件(onLongClick
)卸伞、觸摸事件(onTouch
),并且View自身也有onTouchEvent
方法锉屈,那么問題來了荤傲,這么多與事件相關(guān)的方法應(yīng)該由誰管理?毋庸置疑就是 dispatchTouchEvent
颈渊,所以 View 也會(huì)有事件分發(fā)遂黍。
Question: 與 View 事件相關(guān)的各個(gè)方法調(diào)用順序是怎樣的?
A:如果不去看源碼俊嗽,想一下讓自己設(shè)計(jì)會(huì)怎樣雾家?
- 單擊事件(
onClickListener
) 需要兩個(gè)事件(ACTION_DOWN
和ACTION_UP
)才能觸發(fā),如果先分配給onClick
判斷绍豁,等它判斷完芯咧,用戶手指已經(jīng)離開屏幕,黃花菜都涼了,定然造成 View 無法響應(yīng)其他事件敬飒,應(yīng)該最后調(diào)用邪铲。(最后) - 長(zhǎng)按事件(
onLongClickListener
) 同理,也是需要長(zhǎng)時(shí)間等待才能出結(jié)果无拗,肯定不能排到前面带到,但因?yàn)椴恍枰?code>ACTION_UP,應(yīng)該排在onClick
前面英染。(onLongClickListener
>onClickListener
) - 觸摸事件(
onTouchListener
) 揽惹, 如果用戶注冊(cè)了觸摸事件,說明用戶要自己處理觸摸事件税迷,這個(gè)應(yīng)該排在最前面永丝。(最前)、 - View自身處理(
onTouchEvent
) 提供了一種默認(rèn)的處理方式箭养,如果用戶已經(jīng)處理好了慕嚷,也就不需要了,所以應(yīng)該排在onTouchListener
后面毕泌。(onTouchListener
>onTouchEvent
)
所以事件的調(diào)度順序應(yīng)該是 onTouchListener
> onTouchEvent
> onLongClickListener
> onClickListener
喝检。
ViewGroup相關(guān)
ViewGroup
(通常是各種Layout
) 的事件分發(fā)相對(duì)來說就要麻煩一些,因?yàn)?ViewGroup
不僅要考慮自身撼泛,還要考慮各種ChildView
挠说,一旦處理不好就容易引起各種事件沖突,正所謂養(yǎng)兒方知父母難啊愿题。
VIewGroup
的事件分發(fā)流程又是如何的呢损俭?
我們了解到事件是通過ViewGroup
一層一層傳遞的,最終傳遞給View
潘酗,ViewGroup
要比它的 ChildView
先拿到事件杆兵,并且有權(quán)決定是否告訴要告訴ChildView
。在默認(rèn)的情況下 ViewGroup
事件分發(fā)流程是這樣的仔夺。
- 判斷自身是否需要(詢問
onInterceptTouchEvent
是否攔截)琐脏,如果需要,調(diào)用自己的onTouchEvent
缸兔。 - 自身不需要或者不確定日裙,則詢問
ChildView
,一般來說是調(diào)用手指觸摸位置的ChildView
惰蜜。 - 如果子
ChildView
不需要?jiǎng)t調(diào)用自身的onTouchEvent
昂拂。
用偽代碼應(yīng)該是這樣子的:
// 點(diǎn)擊事件產(chǎn)生后
// 步驟1:調(diào)用dispatchTouchEvent()
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false; //代表 是否會(huì)消費(fèi)事件
// 步驟2:判斷是否攔截事件
if (onInterceptTouchEvent(ev)) {
// a. 若攔截,則將該事件交給當(dāng)前View進(jìn)行處理
// 即調(diào)用onTouchEvent()去處理點(diǎn)擊事件
consume = onTouchEvent (ev) ;
} else {
// b. 若不攔截抛猖,則將該事件傳遞到下層
// 即 下層元素的dispatchTouchEvent()就會(huì)被調(diào)用政钟,重復(fù)上述過程
// 直到點(diǎn)擊事件被最終處理為止
consume = child.dispatchTouchEvent (ev) ;
}
// 步驟3:最終返回通知 該事件是否被消費(fèi)(接收 & 處理)
return consume;
}
}
安卓為了保證所有的事件都是被一個(gè) View 消費(fèi)的路克,對(duì)第一次的事件( ACTION_DOWN )進(jìn)行了特殊判斷樟结,View 只有消費(fèi)了 ACTION_DOWN 事件养交,才能接收到后續(xù)的事件(可點(diǎn)擊控件會(huì)默認(rèn)消費(fèi)所有事件),并且會(huì)將后續(xù)所有事件傳遞過來瓢宦,不會(huì)再傳遞給其他 View碎连,除非上層 View 進(jìn)行了攔截。如果上層 View 攔截了當(dāng)前正在處理的事件驮履,會(huì)收到一個(gè) ACTION_CANCEL
鱼辙,表示當(dāng)前事件已經(jīng)結(jié)束,后續(xù)事件不會(huì)再傳遞過來玫镐。
核心要點(diǎn)
- 事件分發(fā)原理: 責(zé)任鏈模式倒戏,事件層層傳遞,直到被消費(fèi)恐似。
- View 的
dispatchTouchEvent
主要用于調(diào)度自身的監(jiān)聽器和onTouchEvent
杜跷。- View的事件的調(diào)度順序是
onTouchListener
>onTouchEvent
>onLongClickListener
>onClickListener
。- 不論
View
自身是否注冊(cè)點(diǎn)擊事件矫夷,只要View
是可點(diǎn)擊的就會(huì)消費(fèi)事件葛闷。- 事件是否被消費(fèi)由返回值決定,
true
表示消費(fèi)双藕,false
表示不消費(fèi)淑趾,與是否使用了事件無關(guān)。- ViewGroup 中可能有多個(gè)
ChildView
時(shí)忧陪,將事件分配給包含點(diǎn)擊位置的 ChildView扣泊。- ViewGroup 和 ChildView 同時(shí)注冊(cè)了事件監(jiān)聽器(
onClick
等),由 ChildView 消費(fèi)嘶摊。- 一次觸摸流程中產(chǎn)生事件應(yīng)被同一
View
消費(fèi)延蟹,全部接收或者全部拒絕。- 只要接受
ACTION_DOWN
就意味著接受所有的事件更卒,拒絕ACTION_DOWN
則不會(huì)收到后續(xù)內(nèi)容等孵。- 如果當(dāng)前正在處理的事件被上層 View 攔截,會(huì)收到一個(gè)
ACTION_CANCEL
蹂空,后續(xù)事件不會(huì)再傳遞過來俯萌。
原文鏈接:https://www.haomeiwen.com/subject/atfkartx.html、GoodSong