1. 基礎認知
1.1 事件分發(fā)的對象是誰际邻?
答:點擊事件(Touch
事件)
- 定義
當用戶觸摸屏幕時(View
或ViewGroup
派生的控件),將產(chǎn)生點擊事件(Touch
事件)
Touch
事件的相關細節(jié)(發(fā)生觸摸的位置昂勉、時間等)被封裝成MotionEvent
對象
- 事件類型(4種)
事件類型 | 具體動作 |
---|---|
MotionEvent.ACTION_DOWN | 按下View(所有事件的開始) |
MotionEvent.ACTION_UP | 抬起View(與DOWN對應) |
MotionEvent.ACTION_MOVE | 滑動View |
MotionEvent.ACTION_CANCEL | 結束事件(非人為原因) |
- 特別說明:事件列
從手指接觸屏幕 至 手指離開屏幕躏敢,這個過程產(chǎn)生的一系列事件
注:一般情況下慎冤,事件列都是以
DOWN
事件開始、UP
事件結束虏缸,中間有無數(shù)的MOVE事件鲫懒,如下圖:
即當一個點擊事件(MotionEvent
)產(chǎn)生后,系統(tǒng)需把這個事件傳遞給一個具體的 View
去處理刽辙。
1.2 事件分發(fā)的本質
答:將點擊事件(MotionEvent)傳遞到某個具體的View
& 處理的整個過程
即 事件傳遞的過程 = 分發(fā)過程窥岩。
1.3 事件在哪些對象之間進行傳遞?
答:Activity宰缤、ViewGroup颂翼、View
-
Android
的UI
界面由Activity
、ViewGroup
慨灭、View
及其派生類組成
1.4 事件分發(fā)的順序
即 事件傳遞的順序:Activity
-> ViewGroup
-> View
即:1個點擊事件發(fā)生后朦乏,事件先傳到
Activity
、再傳到ViewGroup
氧骤、最終再傳到View
1.5 事件分發(fā)過程由哪些方法協(xié)作完成呻疹?
答:dispatchTouchEvent() 、onInterceptTouchEvent()和onTouchEvent()
下文會對這3個方法進行詳細介紹
1.6 總結
- 至此语淘,相信大家已經(jīng)對
Android
的事件分發(fā)有了感性的認知 - 下面诲宇,我將詳細介紹
Android
事件分發(fā)機制
2. 事件分發(fā)機制 源碼分析
- 請謹記:
Android
事件分發(fā)流程 = Activity -> ViewGroup -> View
即:1個點擊事件發(fā)生后,事件先傳到
Activity
惶翻、再傳到ViewGroup
姑蓝、最終再傳到View
- 從上可知,要想充分理解Android分發(fā)機制吕粗,本質上是要理解:
-
Activity
對點擊事件的分發(fā)機制 -
ViewGroup
對點擊事件的分發(fā)機制 -
View
對點擊事件的分發(fā)機制
-
- 下面纺荧,我將通過源碼,全面解析 事件分發(fā)機制
即按順序講解:
Activity
事件分發(fā)機制颅筋、ViewGroup
事件分發(fā)機制宙暇、View
事件分發(fā)機制
2.1 Activity的事件分發(fā)機制
當一個點擊事件發(fā)生時,事件最先傳到Activity
的dispatchTouchEvent()
進行事件分發(fā)
2.1.1 源碼分析
/**
* 源碼分析:Activity.dispatchTouchEvent()
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
// 一般事件列開始都是DOWN事件 = 按下事件议泵,故此處基本是true
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
// ->>分析1
}
// ->>分析2
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
// 若getWindow().superDispatchTouchEvent(ev)的返回true
// 則Activity.dispatchTouchEvent()就返回true占贫,則方法結束。即 :該點擊事件停止往下傳遞 & 事件傳遞過程結束
// 否則:繼續(xù)往下調(diào)用Activity.onTouchEvent
}
// ->>分析4
return onTouchEvent(ev);
}
/**
* 分析1:onUserInteraction()
* 作用:實現(xiàn)屏保功能
* 注:
* a. 該方法為空方法
* b. 當此activity在棧頂時先口,觸屏點擊按home型奥,back,menu鍵等都會觸發(fā)此方法
*/
public void onUserInteraction() {
}
// 回到最初的調(diào)用原處
/**
* 分析2:getWindow().superDispatchTouchEvent(ev)
* 說明:
* a. getWindow() = 獲取Window類的對象
* b. Window類是抽象類碉京,其唯一實現(xiàn)類 = PhoneWindow類厢汹;即此處的Window類對象 = PhoneWindow類對象
* c. Window類的superDispatchTouchEvent() = 1個抽象方法,由子類PhoneWindow類實現(xiàn)
*/
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
// mDecor = 頂層View(DecorView)的實例對象
// ->> 分析3
}
/**
* 分析3:mDecor.superDispatchTouchEvent(event)
* 定義:屬于頂層View(DecorView)
* 說明:
* a. DecorView類是PhoneWindow類的一個內(nèi)部類
* b. DecorView繼承自FrameLayout谐宙,是所有界面的父類
* c. FrameLayout是ViewGroup的子類烫葬,故DecorView的間接父類 = ViewGroup
*/
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
// 調(diào)用父類的方法 = ViewGroup的dispatchTouchEvent()
// 即 將事件傳遞到ViewGroup去處理,詳細請看ViewGroup的事件分發(fā)機制
}
// 回到最初的調(diào)用原處
/**
* 分析4:Activity.onTouchEvent()
* 定義:屬于頂層View(DecorView)
* 說明:
* a. DecorView類是PhoneWindow類的一個內(nèi)部類
* b. DecorView繼承自FrameLayout凡蜻,是所有界面的父類
* c. FrameLayout是ViewGroup的子類搭综,故DecorView的間接父類 = ViewGroup
*/
public boolean onTouchEvent(MotionEvent event) {
// 當一個點擊事件未被Activity下任何一個View接收 / 處理時
// 應用場景:處理發(fā)生在Window邊界外的觸摸事件
// ->> 分析5
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
// 即 只有在點擊事件在Window邊界外才會返回true,一般情況都返回false咽瓷,分析完畢
}
/**
* 分析5:mWindow.shouldCloseOnTouch(this, event)
*/
public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
// 主要是對于處理邊界外點擊事件的判斷:是否是DOWN事件设凹,event的坐標是否在邊界內(nèi)等
if (mCloseOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN
&& isOutOfBounds(context, event) && peekDecorView() != null) {
return true;
}
return false;
// 返回true:說明事件在邊界外刊苍,即 消費事件
// 返回false:未消費(默認)
}
// 回到分析4調(diào)用原處
2.1.2 總結
- 當一個點擊事件發(fā)生時搜立,從
Activity
的事件分發(fā)開始(Activity.dispatchTouchEvent()
)
- 方法總結
那么,ViewGroup
的dispatchTouchEvent()
什么時候返回true
/ false
捐川?請繼續(xù)往下看ViewGroup事件的分發(fā)機制
2.2 ViewGroup事件的分發(fā)機制
從上面Activity
事件分發(fā)機制可知钻洒,ViewGroup
事件分發(fā)機制從dispatchTouchEvent()
開始
2.2.1 源碼分析
Android 5.0
后奋姿,ViewGroup.dispatchTouchEvent()
的源碼發(fā)生了變化(更加復雜),但原理相同素标;- 本文為了讓讀者容易理解称诗,故采用
Android 5.0
前的版本
/**
* 源碼分析:ViewGroup.dispatchTouchEvent()
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
... // 僅貼出關鍵代碼
// 重點分析1:ViewGroup每次事件分發(fā)時,都需調(diào)用onInterceptTouchEvent()詢問是否攔截事件
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
// 判斷值1:disallowIntercept = 是否禁用事件攔截的功能(默認是false)头遭,可通過調(diào)用requestDisallowInterceptTouchEvent()修改
// 判斷值2: !onInterceptTouchEvent(ev) = 對onInterceptTouchEvent()返回值取反
// a. 若在onInterceptTouchEvent()中返回false(即不攔截事件)寓免,就會讓第二個值為true癣诱,從而進入到條件判斷的內(nèi)部
// b. 若在onInterceptTouchEvent()中返回true(即攔截事件),就會讓第二個值為false袜香,從而跳出了這個條件判斷
// c. 關于onInterceptTouchEvent() ->>分析1
ev.setAction(MotionEvent.ACTION_DOWN);
final int scrolledXInt = (int) scrolledXFloat;
final int scrolledYInt = (int) scrolledYFloat;
final View[] children = mChildren;
final int count = mChildrenCount;
// 重點分析2
// 通過for循環(huán)撕予,遍歷了當前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);
// 判斷當前遍歷的View是不是正在點擊的View,從而找到當前被點擊的View
// 若是蜈首,則進入條件判斷內(nèi)部
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;
// 條件判斷的內(nèi)部調(diào)用了該View的dispatchTouchEvent()
// 即 實現(xiàn)了點擊事件從ViewGroup到子View的傳遞(具體請看下面的View事件分發(fā)機制)
if (child.dispatchTouchEvent(ev)) {
mMotionTarget = child;
return true;
// 調(diào)用子View的dispatchTouchEvent后是有返回值的
// 若該控件可點擊实抡,那么點擊時,dispatchTouchEvent的返回值必定是true欢策,因此會導致條件判斷成立
// 于是給ViewGroup的dispatchTouchEvent()直接返回了true吆寨,即直接跳出
// 即把ViewGroup的點擊事件攔截掉
}
}
}
}
}
}
boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
(action == MotionEvent.ACTION_CANCEL);
if (isUpOrCancel) {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
final View target = mMotionTarget;
// 重點分析3
// 若點擊的是空白處(即無任何View接收事件) / 攔截事件(手動復寫onInterceptTouchEvent(),從而讓其返回true)
if (target == null) {
ev.setLocation(xf, yf);
if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
}
return super.dispatchTouchEvent(ev);
// 調(diào)用ViewGroup父類的dispatchTouchEvent()踩寇,即View.dispatchTouchEvent()
// 因此會執(zhí)行ViewGroup的onTouch() ->> onTouchEvent() ->> performClick() ->> onClick()啄清,即自己處理該事件,事件不會往下傳遞(具體請參考View事件的分發(fā)機制中的View.dispatchTouchEvent())
// 此處需與上面區(qū)別:子View的dispatchTouchEvent()
}
...
}
/**
* 分析1:ViewGroup.onInterceptTouchEvent()
* 作用:是否攔截事件
* 說明:
* a. 返回true = 攔截姑荷,即事件停止往下傳遞(需手動設置盒延,即復寫onInterceptTouchEvent(),從而讓其返回true)
* b. 返回false = 不攔截(默認)
*/
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
// 回到調(diào)用原處
2.2.2 總結
- 結論:
Android
事件分發(fā)總是先傳遞到ViewGroup
鼠冕、再傳遞到View
- 過程:當點擊了某個控件時
- 核心方法總結
2.2.3 Demo講解
-
布局如下
測試代碼
布局文件: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
/**
* ViewGroup布局(myLayout)中有2個子View = 2個按鈕
*/
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布局設置監(jiān)聽事件
myLayout.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d("TAG", "點擊了ViewGroup");
}
});
// 2\. 為按鈕1設置監(jiān)聽事件
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d("TAG", "點擊了button1");
}
});
// 3\. 為按鈕2設置監(jiān)聽事件
button2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d("TAG", "點擊了button2");
}
});
}
}
-
結果測試
從上面的測試結果發(fā)現(xiàn):
- 點擊
Button
時添寺,執(zhí)行Button.onClick()
,但ViewGroupLayout
注冊的onTouch()
不會執(zhí)行 - 只有點擊空白區(qū)域時懈费,才會執(zhí)行
ViewGroupLayout
的onTouch()
- 結論:
Button
的onClick()
將事件消費掉了计露,因此事件不會再繼續(xù)向下傳遞。
2.3 View事件的分發(fā)機制
從上面ViewGroup
事件分發(fā)機制知道憎乙,View
事件分發(fā)機制從dispatchTouchEvent()
開始
2.3.1 源碼分析
/**
* 源碼分析:View.dispatchTouchEvent()
*/
public boolean dispatchTouchEvent(MotionEvent event) {
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}
// 說明:只有以下3個條件都為真票罐,dispatchTouchEvent()才返回true;否則執(zhí)行onTouchEvent()
// 1\. mOnTouchListener != null
// 2\. (mViewFlags & ENABLED_MASK) == ENABLED
// 3\. mOnTouchListener.onTouch(this, event)
// 下面對這3個條件逐個分析
/**
* 條件1:mOnTouchListener != null
* 說明:mOnTouchListener變量在View.setOnTouchListener()方法里賦值
*/
public void setOnTouchListener(OnTouchListener l) {
mOnTouchListener = l;
// 即只要我們給控件注冊了Touch事件泞边,mOnTouchListener就一定被賦值(不為空)
}
/**
* 條件2:(mViewFlags & ENABLED_MASK) == ENABLED
* 說明:
* a. 該條件是判斷當前點擊的控件是否enable
* b. 由于很多View默認enable该押,故該條件恒定為true
*/
/**
* 條件3:mOnTouchListener.onTouch(this, event)
* 說明:即 回調(diào)控件注冊Touch事件時的onTouch();需手動復寫設置阵谚,具體如下(以按鈕Button為例)
*/
button.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return false;
}
});
// 若在onTouch()返回true蚕礼,就會讓上述三個條件全部成立,從而使得View.dispatchTouchEvent()直接返回true梢什,事件分發(fā)結束
// 若在onTouch()返回false奠蹬,就會使得上述三個條件不全部成立,從而使得View.dispatchTouchEvent()中跳出If嗡午,執(zhí)行onTouchEvent(event)
接下來囤躁,我們繼續(xù)看:onTouchEvent(event)的源碼分析
- 詳情請看注釋
Android 5.0
后View.onTouchEvent()
源碼發(fā)生了變化(更加復雜),但原理相同;- 本文為了讓讀者更好理解狸演,所以采用
Android 5.0
前的版本
/**
* 源碼分析:View.onTouchEvent()
*/
public boolean onTouchEvent(MotionEvent event) {
final int viewFlags = mViewFlags;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
return (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
// 若該控件可點擊言蛇,則進入switch判斷中
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
// a. 若當前的事件 = 抬起View(主要分析)
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;
...// 經(jīng)過種種判斷,此處省略
// 執(zhí)行performClick() ->>分析1
performClick();
break;
// b. 若當前的事件 = 按下View
case MotionEvent.ACTION_DOWN:
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPrivateFlags |= PREPRESSED;
mHasPerformedLongPress = false;
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
break;
// c. 若當前的事件 = 結束事件(非人為原因)
case MotionEvent.ACTION_CANCEL:
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
removeTapCallback();
break;
// d. 若當前的事件 = 滑動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)) {
// Outside button
removeTapCallback();
if ((mPrivateFlags & PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();
// Need to switch from pressed to not pressed
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
}
}
break;
}
// 若該控件可點擊宵距,就一定返回true
return true;
}
// 若該控件不可點擊猜极,就一定返回false
return false;
}
/**
* 分析1:performClick()
*/
public boolean performClick() {
if (mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
mOnClickListener.onClick(this);
return true;
// 只要我們通過setOnClickListener()為控件View注冊1個點擊事件
// 那么就會給mOnClickListener變量賦值(即不為空)
// 則會往下回調(diào)onClick() & performClick()返回true
}
return false;
}
2.3.2 總結
- 每當控件被點擊時:
注:
onTouch()
的執(zhí)行 先于onClick()
- 核心方法總結
2.3.3 Demo講解
下面我將用Demo
驗證上述的結論
/**
* 結論驗證1:在回調(diào)onTouch()里返回false
*/
// 1\. 通過OnTouchListener()復寫onTouch(),從而手動設置返回false
button.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
System.out.println("執(zhí)行了onTouch(), 動作是:" + event.getAction());
return false;
}
});
// 2\. 通過 OnClickListener()為控件設置點擊事件消玄,為mOnClickListener變量賦值(即不為空),從而往下回調(diào)onClick()
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
System.out.println("執(zhí)行了onClick()");
}
});
/**
* 結論驗證2:在回調(diào)onTouch()里返回true
*/
// 1\. 通過OnTouchListener()復寫onTouch()丢胚,從而手動設置返回true
button.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
System.out.println("執(zhí)行了onTouch(), 動作是:" + event.getAction());
return true;
}
});
// 2\. 通過 OnClickListener()為控件設置點擊事件翩瓜,為mOnClickListener變量賦值(即不為空)
// 但由于dispatchTouchEvent()返回true,即事件不再向下傳遞携龟,故不調(diào)用onClick())
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
System.out.println("執(zhí)行了onClick()");
}
});
-
測試結果
2.4 總結
若您已經(jīng)看到此處兔跌,那么恭喜你,你已經(jīng)能非常熟悉掌握Android的事件分發(fā)機制了
即:
Activity
峡蟋、ViewGroup
坟桅、View
的事件分發(fā)機制
3. 工作流程 總結
- 在本節(jié)中,我將結合源碼蕊蝗,梳理出1個事件分發(fā)的工作流程總結仅乓,具體如下:
左側虛線:具備相關性 & 逐層返回
- 以角色為核心的圖解說明
- 以方法為核心的圖解說明
4. 核心方法總結
-
已知事件分發(fā)過程的核心方法為:
dispatchTouchEvent()
、onInterceptTouchEvent()
和onTouchEvent()
下面蓬戚,我將結合總結的工作流程夸楣,再次詳細講解該3個方法
4.1 dispatchTouchEvent()
- 簡介
- 返回情況說明
情況1:默認
情況2:返回true
情況3:返回false
4.2 onInterceptTouchEvent()
- 簡介
注:
Activity
、View
都無該方法
- 返回情況說明
情況1:true
情況2:false(默認)
4.3 onTouchEvent()
- 簡介
- 返回情況說明
情況1:返回true
情況2:返回false(default)
4.4 三者關系
下面子漩,我用一段偽代碼來闡述上述3個方法的關系 & 事件傳遞規(guī)則
/**
* 點擊事件產(chǎn)生后
*/
// 步驟1:調(diào)用dispatchTouchEvent()
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false; //代表 是否會消費事件
// 步驟2:判斷是否攔截事件
if (onInterceptTouchEvent(ev)) {
// a. 若攔截豫喧,則將該事件交給當前View進行處理
// 即調(diào)用onTouchEvent ()方法去處理點擊事件
consume = onTouchEvent (ev) ;
} else {
// b. 若不攔截,則將該事件傳遞到下層
// 即 下層元素的dispatchTouchEvent()就會被調(diào)用幢泼,重復上述過程
// 直到點擊事件被最終處理為止
consume = child.dispatchTouchEvent (ev) ;
}
// 步驟3:最終返回通知 該事件是否被消費(接收 & 處理)
return consume;
}
5. 常見的事件分發(fā)場景
下面紧显,我將通過實例說明常見的事件傳遞情況 & 流程
5.1 背景描述
- 討論的布局如下:
-
情景
- 用戶先觸摸到屏幕上
View C
上的某個點(圖中黃區(qū))
Action_DOWN
事件在此處產(chǎn)生- 用戶移動手指
- 最后離開屏幕
- 用戶先觸摸到屏幕上
5.2 一般的事件傳遞情況
一般的事件傳遞場景有:
- 默認情況
- 處理事件
- 攔截
DOWN
事件 - 攔截后續(xù)事件(
MOVE
、UP
)
場景1:默認
即不對控件里的方法(
dispatchTouchEvent()
缕棵、onTouchEvent()
孵班、onInterceptTouchEvent()
)進行重寫 或 更改返回值那么調(diào)用的是這3個方法的默認實現(xiàn):調(diào)用下層的方法 & 逐層返回
-
事件傳遞情況:(呈
U
型)- 從上往下調(diào)用dispatchTouchEvent()
Activity A ->> ViewGroup B ->> View C
- 從下往上調(diào)用onTouchEvent()
View C ->> ViewGroup B ->> Activity A
注:雖然
ViewGroup B
的onInterceptTouchEvent
()對DOWN
事件返回了false
,但后續(xù)的事件(MOVE挥吵、UP)
依然會傳遞給它的onInterceptTouchEvent()
這一點與onTouchEvent()
的行為是不一樣的:不再傳遞 & 接收該事件列的其他事件
場景2:處理事件
設View C
希望處理該點擊事件重父,即:設置View C
為可點擊的(Clickable)
或 復寫其onTouchEvent()
返回true
最常見的:設置
Button
按鈕來響應點擊事件
事件傳遞情況:(如下圖)
-
DOWN
事件被傳遞給C的onTouchEvent
方法,該方法返回true
忽匈,表示處理該事件 - 因為
View C
正在處理該事件房午,那么DOWN
事件將不再往上傳遞給ViewGroup B 和Activity A
的onTouchEvent()
; - 該事件列的其他事件
(Move丹允、Up)
也將傳遞給View C
的onTouchEvent()
會逐層往
dispatchTouchEvent()
返回郭厌,最終事件分發(fā)結束
場景3:攔截DOWN事件
假設ViewGroup B
希望處理該點擊事件袋倔,即ViewGroup B
復寫了onInterceptTouchEvent()
返回true
、onTouchEvent()
返回true
事件傳遞情況:(如下圖)
DOWN
事件被傳遞給ViewGroup B
的onInterceptTouchEvent()
折柠,該方法返回true
宾娜,表示攔截該事件,即自己處理該事件(事件不再往下傳遞)調(diào)用自身的
onTouchEvent()
處理事件(DOWN
事件將不再往上傳遞給Activity A
的onTouchEvent()
)該事件列的其他事件
(Move扇售、Up)
將直接傳遞給ViewGroup B
的onTouchEvent()
注:
- 該事件列的其他事件
(Move前塔、Up)
將不會再傳遞給ViewGroup B
的onInterceptTouchEvent
();因:該方法一旦返回一次true
承冰,就再也不會被調(diào)用- 逐層往
dispatchTouchEvent()
返回华弓,最終事件分發(fā)結束
場景4:攔截DOWN的后續(xù)事件
結論
- 若
ViewGroup
攔截了一個半路的事件(如MOVE
),該事件將會被系統(tǒng)變成一個CANCEL
事件 & 傳遞給之前處理該事件的子View
困乒; - 該事件不會再傳遞給
ViewGroup
的onTouchEvent()
- 只有再到來的事件才會傳遞到
ViewGroup
的onTouchEvent()
場景描述
ViewGroup B
無攔截DOWN
事件(還是View C
來處理DOWN
事件)寂屏,但它攔截了接下來的MOVE
事件
即
DOWN
事件傳遞到View C
的onTouchEvent()
,返回了true
實例講解
- 在后續(xù)到來的MOVE事件娜搂,
ViewGroup B
的onInterceptTouchEvent()
返回true
攔截該MOVE
事件迁霎,但該事件并沒有傳遞給ViewGroup B
;這個MOVE
事件將會被系統(tǒng)變成一個CANCEL
事件傳遞給View C
的onTouchEvent()
- 后續(xù)又來了一個
MOVE
事件百宇,該MOVE
事件才會直接傳遞給ViewGroup B
的onTouchEvent()
后續(xù)事件將直接傳遞給
ViewGroup B
的onTouchEvent()
處理考廉,而不會再傳遞給ViewGroup B
的onInterceptTouchEvent()
,因該方法一旦返回一次true携御,就再也不會被調(diào)用了芝此。
-
View C
再也不會收到該事件列產(chǎn)生的后續(xù)事件
至此,關于Android
常見的事件傳遞情況 & 流程已經(jīng)講解完畢因痛。
6. 額外知識
6.1 Touch事件的后續(xù)事件(MOVE婚苹、UP)層級傳遞
- 若給控件注冊了
Touch
事件,每次點擊都會觸發(fā)一系列action
事件(ACTION_DOWN鸵膏,ACTION_MOVE膊升,ACTION_UP等) - 當
dispatchTouchEvent()
事件分發(fā)時,只有前一個事件(如ACTION_DOWN)返回true谭企,才會收到后一個事件(ACTION_MOVE和ACTION_UP)
即如果在執(zhí)行ACTION_DOWN時返回false廓译,后面一系列的ACTION_MOVE、ACTION_UP事件都不會執(zhí)行
從上面對事件分發(fā)機制分析知:
- dispatchTouchEvent()债查、 onTouchEvent() 消費事件非区、終結事件傳遞(返回true)
- 而onInterceptTouchEvent 并不能消費事件,它相當于是一個分叉口起到分流導流的作用盹廷,對后續(xù)的ACTION_MOVE和ACTION_UP事件接收起到非常大的作用
請記渍鞒瘛:接收了ACTION_DOWN事件的函數(shù)不一定能收到后續(xù)事件(ACTION_MOVE、ACTION_UP)
這里給出ACTION_MOVE和ACTION_UP事件的傳遞結論:
- 結論1
若對象(Activity、ViewGroup管怠、View)的dispatchTouchEvent()分發(fā)事件后消費了事件(返回true)淆衷,那么收到ACTION_DOWN的函數(shù)也能收到ACTION_MOVE和ACTION_UP
黑線:ACTION_DOWN事件傳遞方向
紅線:ACTION_MOVE 、 ACTION_UP事件傳遞方向
- 結論2
若對象(Activity渤弛、ViewGroup祝拯、View)的onTouchEvent()處理了事件(返回true),那么ACTION_MOVE她肯、ACTION_UP的事件從上往下傳到該View
后就不再往下傳遞佳头,而是直接傳給自己的onTouchEvent()
& 結束本次事件傳遞過程。
黑線:ACTION_DOWN事件傳遞方向
紅線:ACTION_MOVE晴氨、ACTION_UP事件傳遞方向
6.2 onTouch()和onTouchEvent()的區(qū)別
- 該2個方法都是在
View.dispatchTouchEvent()
中調(diào)用 - 但
onTouch()
優(yōu)先于onTouchEvent
執(zhí)行畜晰;若手動復寫在onTouch()
中返回true
(即 將事件消費掉),將不會再執(zhí)行onTouchEvent()
注:若1個控件不可點擊(即非
enable
)瑞筐,那么給它注冊onTouch
事件將永遠得不到執(zhí)行,具體原因看如下代碼
// &&為短路與腊瑟,即如果前面條件為false聚假,將不再往下執(zhí)行
// 故:onTouch()能夠得到執(zhí)行需2個前提條件:
// 1\. mOnTouchListener的值不能為空
// 2\. 當前點擊的控件必須是enable的
mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)
// 對于該類控件,若需監(jiān)聽它的touch事件闰非,就必須通過在該控件中重寫onTouchEvent()來實現(xiàn)
7. 總結
通過閱讀本文膘格,相信您已經(jīng)可以全面了解
Android
事件分發(fā)機制與
Android
事件分發(fā)最相關的知識:自定義View系列文章
(1)自定義View基礎 - 最易懂的自定義View原理系列
(2)自定義View Measure過程 - 最易懂的自定義View原理系列
(3)自定義View Layout過程 - 最易懂的自定義View原理系列
(4)自定義View Draw過程- 最易懂的自定義View原理系列