紙上得來終覺淺蠢护,看了很多別人寫的有關(guān)View的事件分發(fā)機(jī)制的博客,但別人的終究
是別人的箍土,把自己的理解寫下來逢享,才是自己的,但萬變不離其宗涮帘。本篇將從另外一個(gè)
角度帶你理解View的事件分發(fā)機(jī)制拼苍。
序言
關(guān)于 View 和 ViewGroup 的事件分發(fā)機(jī)制 我打算用兩篇博客來寫,
本篇主要講述 View 的事件分發(fā)機(jī)制调缨,下一篇講述ViewGroup的事件分發(fā)機(jī)制
View 的事件分發(fā)和領(lǐng)導(dǎo)派發(fā)任務(wù)是很相似的疮鲫,所以我們先通過這個(gè)生活中常見的場景引入,以便大家更好的理解View的事件分發(fā)
場景描述:
當(dāng)老板有一個(gè)問題需要解決的時(shí)候弦叶,他可以自己直接解決俊犯,也可以將問題交給經(jīng)理去解決,當(dāng)然老板不可能所有問題都
要自己解決伤哺,那樣還要經(jīng)理和員工干什么燕侠,所以老板一般會(huì)將任務(wù)派發(fā)給經(jīng)理,一般經(jīng)理也不會(huì)自己解決立莉,他會(huì)將問題
派發(fā)給員工去解決绢彤,員工嘛就是干活的,否則就得走人了蜓耻,當(dāng)然不排除有些問題員工沒有能力解決那只能將問題返回給
經(jīng)理茫舶,經(jīng)理有能力解決就解決了,如果經(jīng)理也解決不了刹淌,那就把這個(gè)問題扔回給老板饶氏,最后只能由老板自己解決了。
類似地:
我們的View的事件分發(fā)也是這樣的有勾,當(dāng)我們手接觸到手機(jī)屏幕的時(shí)候疹启,屏幕接收到一個(gè)Touch事件,系統(tǒng)會(huì)將這Touch
事件封裝成一個(gè)對象MotionEvent蔼卡,之后所有的事件處理都將與這個(gè)MotionEvent相關(guān)喊崖,和領(lǐng)導(dǎo)派發(fā)任務(wù)一樣,我們的
Touch事件會(huì)首先到達(dá)最外層的ViewGroup(Activity)雇逞,然后再一層一層地向子View派發(fā)荤懂,最終會(huì)到達(dá)最內(nèi)層的View,
最內(nèi)層的View可以處理這個(gè)Touch事件,也可以將這個(gè)Touch事件扔回給自己的父View
對于View和ViewGroup我們需要關(guān)注下面的個(gè)方法:
View 兩個(gè)方法:
-
dispatchTouchEvent (
MotionEvent event
)//負(fù)責(zé)事件分發(fā) -
onTouchEvent(
MotionEvent event
)//當(dāng)前View自己處理當(dāng)前事件
ViewGroup 三個(gè)方法:
-
dispatchTouchEvent (
MotionEvent event
)//負(fù)責(zé)事件分發(fā) -
onInterCeptTouchEvent(
MotionEvent event
)//處理是否攔截當(dāng)前事件 -
onTouchEvent(
MotionEvent event
)//當(dāng)前View自己處理當(dāng)前事件
我們注意到上面的方法都參數(shù)都是一個(gè) MotionEvent 對象喝峦,View
比ViewGroup
少了一個(gè)onInterCeptTouchEvent()
方法势誊,這時(shí)因?yàn)閷τ?code>View來講,它是不能有子View
的谣蠢,所以不需要攔截事件的方法
正文:
我們先從Android中兩種不同類型的控件開始介紹
- 可被點(diǎn)擊的:
Button粟耻、ImageButton...
//即本身CLICKABLE為true
的 - 不可被點(diǎn)擊的:
ImageView、TextView...
//即本身CLICKABLE為false
的
注意:上面兩點(diǎn)很重要眉踱,大家一定要記在心里挤忙。也可能你會(huì)有疑問我們平常用的ImageView
和TextView
都能點(diǎn)擊呀,請注意我說的是本身可被點(diǎn)擊的谈喳,ImageView
和TextView
能被點(diǎn)擊是因?yàn)槲覀兘o他們設(shè)置了setOnClickListener()
,這樣就把這兩種View
的CLICKABLE
置為true
了级历,關(guān)于這一點(diǎn)大家可以通過調(diào)用這個(gè)方法前后添加log日志去驗(yàn)證初狰。
我們先新建一個(gè)project: ViewDemo
ViewDemo 很簡單:在布局文件中添加一個(gè) ImageView 和一個(gè) Button 控件
在MainActivity
中初始化并給兩個(gè)控件都設(shè)置setOnTouchListener
先看布局文件
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="horizontal">
<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/ic_launcher"
/>
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="按鈕"/>
</LinearLayout>
MainActivity中初始化ImageView
和Button
并設(shè)置setOnTouchListener
在onTouch()
方法中添加log日志
MainActivity主要代碼
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mImageView = findViewById(R.id.imageView);
mButton = findViewById(R.id.button);
mImageView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
Log.e(TAG,"----mImageView-----onTouch---->"+motionEvent.getAction());
return false;
}
});
mButton.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
Log.e(TAG,"----mButton--------onTouch---->"+motionEvent.getAction());
return false;
}
});
}
運(yùn)行程序依次點(diǎn)擊 ImageView
和Button
其中 motionEvent.getAction() 的值代表的含義:
0:ACTION_DOWN
2:ACTION_MOVE
1:ACTION_UP
打印log如下:
10-30 11:50:06.439 10517-10517/com.marco.viewdemo E/MainActivity: ----mImageView-----onTouch---->0
10-30 11:50:07.276 10517-10517/com.marco.viewdemo E/MainActivity: ----mButton--------onTouch---->0
10-30 11:50:07.285 10517-10517/com.marco.viewdemo E/MainActivity: ----mButton--------onTouch---->2
10-30 11:50:07.313 10517-10517/com.marco.viewdemo E/MainActivity: ----mButton--------onTouch---->1
我們可以看到點(diǎn)擊ImageView
的時(shí)候日志打印了一次枣购,點(diǎn)擊Button
的時(shí)候日志打印了三次
如果我們把兩個(gè)onTouch()
方法的返回值都返回true
即:
mImageView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
Log.e(TAG,"----mImageView-----onTouch---->"+motionEvent.getAction());
return true;
}
});
mButton.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
Log.e(TAG,"----mButton--------onTouch---->"+motionEvent.getAction());
return true;
}
});
}
分別點(diǎn)擊 ImageView 和 Button 再來看日志:
10-30 12:55:16.427 20779-20779/com.marco.viewdemo E/MainActivity: ----mImageView-----onTouch---->0
10-30 12:55:16.442 20779-20779/com.marco.viewdemo E/MainActivity: ----mImageView-----onTouch---->2
10-30 12:55:16.487 20779-20779/com.marco.viewdemo E/MainActivity: ----mImageView-----onTouch---->1
10-30 12:55:19.514 20779-20779/com.marco.viewdemo E/MainActivity: ----mButton--------onTouch---->0
10-30 12:55:19.533 20779-20779/com.marco.viewdemo E/MainActivity: ----mButton--------onTouch---->2
10-30 12:55:19.574 20779-20779/com.marco.viewdemo E/MainActivity: ----mButton--------onTouch---->1
此時(shí)可以看到兩個(gè)控件的日志打印都是三次爹耗,DOWN
、MOVE
岗仑、UP
一分鐘時(shí)間思考一下這是為什么?
下面讓我?guī)Т蠹覐脑创a角度分析一下為什么會(huì)這樣:
前面我們提到過對于View來講我們關(guān)注兩個(gè)方法:
-
dispatchTouchEvent (
MotionEvent event
)//負(fù)責(zé)事件分發(fā) -
onTouchEvent(
MotionEvent event
)//當(dāng)前View自己處理當(dāng)前事件
我們需要知道,當(dāng)一個(gè)View
接收到Touch
事件的時(shí)候挽绩,首先被調(diào)用的是當(dāng)前View
的dispatchTouchEvent()
方法:
下面我們看一下View源碼中的這個(gè)方法:dispatchTouchEvent()
為了便于理解,我省略了部分干擾代碼驾中,只保留了有效的部分
dispatchTouchEvent(MotionEvent event
)
/**
* Pass the touch screen motion event down to the target view, or this
* view if it is the target.
*
* @param event The motion event to be dispatched.
* @return True if the event was handled by the view, false otherwise.
*/
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
......
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;
}
從方法的注釋我們可以知道:這個(gè)方法是處理事件分發(fā)的唉堪,如果return true
說明當(dāng)前事件被消費(fèi)了,便不再繼續(xù)分發(fā)肩民,否則繼續(xù)分發(fā)
我們主要看方法中的兩個(gè)if判斷:先看第一個(gè)
li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)
因?yàn)槲覀兘o控件設(shè)置了onTouchLishener
所以mListenerInfo
不為空唠亚,li != null
為true
,li.mOnTouchListener != null
也為true
,這個(gè)mOnTouchListener
就是我們mImageView.setOnTouchListener(new View.OnTouchListener())
的時(shí)候new
出來的持痰,第三個(gè)條件主要取決于mViewFlags
灶搜,在Android系統(tǒng)中所有控件默認(rèn)都是enable
的,除非我們對一個(gè)控件設(shè)置view.setEnable(false)
,所以(mViewFlags & ENABLED_MASK) == ENABLED
也為true
共啃,那么能不能進(jìn)入到if()
內(nèi)部關(guān)鍵看li.mOnTouchListener.onTouch(this, event)
占调,這個(gè)方法的返回值就是我們最開始setOnTouchListener
中重寫的onTouch()
方法的返回值,默認(rèn)為false
移剪,后來被我們改成了true
究珊。
在onTouch()
方法中默認(rèn)false
,也就是說此處的第一個(gè)if()
條件不成立纵苛,那么將到第二個(gè)if()
判斷:剿涮!result
為true
,那么我們主要看onTouchEvent(event)
的返回值攻人,那么這個(gè)onTouchEvent(event)
有時(shí)什么東東呢取试?我們進(jìn)入到這個(gè)方法中(干擾代碼已省略):
onTouchEvent(MotionEvent event
)
/*
* @param event The motion event.
* @return True if the event was handled, false otherwise.
*/
public boolean onTouchEvent(MotionEvent event) {
......
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
......
if (!post(mPerformClick)) {
performClick();
}
......
break;
case MotionEvent.ACTION_DOWN:
......
break;
case MotionEvent.ACTION_CANCEL:
......
break;
case MotionEvent.ACTION_MOVE:
......
break;
}
return true;
}
return false;
}
從方法中我們可以看到這個(gè)方法的返回值也是 boolean
類型的,看到這里不知道大家能不能想起我在開頭說的Android中兩種不同類型怀吻,一種是默認(rèn)可以點(diǎn)擊的瞬浓,一種是默認(rèn)不可以點(diǎn)擊,我們的 ImageView
和 Button
的可點(diǎn)擊與否的作用就在這個(gè)方法中的到了體現(xiàn)蓬坡,讓我們來看一下這個(gè) if()
條件判斷吧
先說默認(rèn)條件下兩個(gè)Touch方法都返回false
- ImageView
如果是 ImageView
不用想猿棉,默認(rèn)就是不可點(diǎn)擊的,也就是說CLICKABLE
和LONG_CLICKABLE
都為false
屑咳,那么好萨赁,這個(gè)條件我們是進(jìn)不去的,再看最后return
值是false
兆龙,也就是說只要if()
判斷進(jìn)不去杖爽,全都會(huì)返回false
,回到我們之前的dispatchTouchEvent()
中,既然onTouchEvent()
也返回了false
慰安,那么最終dispatchTouchEvent()
的返回值也是false
腋寨,說明這個(gè)Touch
的DOWN
事件沒有被消費(fèi),既然DOWN
事件沒有被消費(fèi)泻帮,也就沒有后面的MOVE
和UP
精置,所以最初的ImageView
值打印了一行l(wèi)og计寇,
- Button
如果是Button
默認(rèn)就是可點(diǎn)擊的CLICKABLE
和LONG_CLICKABLE
都為true
锣杂,也就是說if()
判斷能夠進(jìn)入,那么就說明番宁,里面的DOWN
元莫、MOVE
和UP
事件都會(huì)響應(yīng),另外我們再看蝶押,進(jìn)入if()
后最后一句: return true;
這句返回了true
踱蠢,也就是說,只要進(jìn)入了這個(gè)if()
判斷棋电,就回返回true
茎截,也就是我們的dispatchTouchEvent()
中的第二個(gè)if()
判斷會(huì)返回true
,那么我們的dispatchTouchEvent()
最終也就回返回true
赶盔,也就是說我們的Touch
事件被消費(fèi)了企锌。當(dāng)然DOWN
、MOVE
于未、UP
事件都會(huì)響應(yīng)撕攒,所以我們的Button
打印了三行log
。
再說之后我們把兩個(gè)Touch方法都返回true
當(dāng)這兩個(gè)onTouch()
方法都返回true的時(shí)候烘浦,就更簡單了抖坪,還是看我們的dispatchTouchEvent()
,即第一個(gè)if()
判斷中mOnTouchListener.onTouch(this, event)
為true
闷叉,前面我們已經(jīng)判斷了擦俐,if()
中其他的都為true
,所以第一個(gè)if()
判斷可以進(jìn)入握侧,最終返回了true
蚯瞧,dispatchTouchEvent()
的返回值為true
,根本就走不到第二個(gè)if()
條件判斷藕咏,所以跟后面的onTouchEvent()
就沒什么關(guān)系了状知。所以最終這個(gè)Touch
事件在dispatchTouchEvent()
中的回調(diào)的onTouch()
方法中得消費(fèi)掉了。
注意:不知道有沒有同學(xué)發(fā)現(xiàn)我上面onTouchEvent()
方法中除了省略的代碼之外孽查,還留下了一行這個(gè)方法就與我們的Click事件又關(guān)了饥悴,我們先看一下這個(gè)方法:performClick();
performClick()
/**
* Call this view's OnClickListener, if it is defined. Performs all normal
* actions associated with clicking: reporting accessibility event, playing
* a sound, etc.
*
* @return True there was an assigned OnClickListener that was called, false
* otherwise is returned.
*/
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
return result;
}
不過要想執(zhí)行到 performClick() 這個(gè)方法要想執(zhí)行到,首先得能進(jìn)入到上面 onTouchEvent() 方法中的 if() 條件判斷
下面我們在MainActivity的onCreate方法中給ImageView
和Button
設(shè)置兩個(gè)click
監(jiān)聽事件,把onTouch事件還原到默認(rèn)的false返回值
mImageView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
Log.e(TAG,"----mImageView-----onTouch---->"+motionEvent.getAction());
return false;
}
});
mButton.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
Log.e(TAG,"----mButton--------onTouch---->"+motionEvent.getAction());
return false;
}
});
mImageView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Log.e(TAG,"----mImageView--------onClick---->");
}
});
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Log.e(TAG,"----mButton--------onClick---->");
}
});
先點(diǎn)擊ImageView打印log:
10-30 14:17:01.255 11719-11719/com.marco.viewdemo E/MainActivity: ----mImageView-----onTouch---->0
10-30 14:17:01.272 11719-11719/com.marco.viewdemo E/MainActivity: ----mImageView-----onTouch---->2
10-30 14:17:01.305 11719-11719/com.marco.viewdemo E/MainActivity: ----mImageView-----onTouch---->1
10-30 14:17:01.309 11719-11719/com.marco.viewdemo E/MainActivity: ----mImageView-----onClick---->
再點(diǎn)擊Button打印log:
10-30 14:18:00.624 11719-11719/com.marco.viewdemo E/MainActivity: ----mButton--------onTouch---->0
10-30 14:18:00.641 11719-11719/com.marco.viewdemo E/MainActivity: ----mButton--------onTouch---->2
10-30 14:18:00.671 11719-11719/com.marco.viewdemo E/MainActivity: ----mButton--------onTouch---->1
10-30 14:18:00.674 11719-11719/com.marco.viewdemo E/MainActivity: ----mButton--------onClick---->
咦西设?竟然一樣瓣铣?你可能要問了,對于Button
打印的結(jié)果我們可以理解贷揽,但是我們只給ImageView
和Button
添加了Click
監(jiān)聽事件棠笑,其他都沒變,兩個(gè)onTouch()
方法返回的竟然也都一樣禽绪,按照我們最初只添加兩個(gè)setOnTouchListener
方法蓖救,ImageView
應(yīng)該只打印一次才對呀,怎么添加了一個(gè)setOnClickListener
印屁,就打印三次了呢循捺?而切如果按照我們最開始的推論,既然ImageView
是默認(rèn)不可點(diǎn)擊的雄人,那么在onTouchEvent
方法中的if()
判斷條件不可能進(jìn)入呀从橘,更不會(huì)執(zhí)行到performClick()
方法,怎么會(huì)也打印出除了最后一行onClick
的log
呢础钠?
猜想:上面log
既然顯示執(zhí)行了performClick()
恰力,也就是說onTouchEvent()
中的if()
判斷條件進(jìn)入了,而我們只設(shè)置了setOnClickListener
方法旗吁,莫非這個(gè)方法做了什么手腳踩萎?是不是把ImageView
的CLICKABLE
的值改變了呢?
我們進(jìn)入到setOnClickListener
中一探究竟:
/**
* Register a callback to be invoked when this view is clicked. If this view is not
* clickable, it becomes clickable.
*
* @param l The callback that will run
*
* @see #setClickable(boolean)
*/
public void setOnClickListener(OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
天吶阵漏,setClickable(true);
看到了吧驻民?我們的猜想是對的,一旦我們設(shè)置了setOnClickListener
當(dāng)前View
的CLICKABLE
的值就會(huì)被置為true
履怯,這就不難理解為什么ImageView
也能進(jìn)入到if()
判斷中回还,而且執(zhí)行了performClick()
方法。
通過上面例子我們也的出來一個(gè)結(jié)論:View
的onTouch()
方法是先于onClick()
方法執(zhí)行的叹洲,如果onTouch()
方法返回true
即被消費(fèi)了柠硕,就不會(huì)執(zhí)行onTouchEvent()
方法,也就執(zhí)行不到onClick()
方法运提,
這點(diǎn)我們也可以驗(yàn)證一下蝗柔,就是把上面兩個(gè)setOnTouchListener中的onTouch方法都置為true然后分別點(diǎn)擊ImageView
和Button
然后看log
:
點(diǎn)擊ImageViewhe
10-30 14:43:54.142 29165-29165/com.marco.viewdemo E/MainActivity: ----mImageView-----onTouch---->0
10-30 14:43:54.150 29165-29165/com.marco.viewdemo E/MainActivity: ----mImageView-----onTouch---->2
10-30 14:43:54.202 29165-29165/com.marco.viewdemo E/MainActivity: ----mImageView-----onTouch---->1
點(diǎn)擊Button
10-30 14:44:13.478 29165-29165/com.marco.viewdemo E/MainActivity: ----mButton--------onTouch---->0
10-30 14:44:13.494 29165-29165/com.marco.viewdemo E/MainActivity: ----mButton--------onTouch---->2
10-30 14:44:13.641 29165-29165/com.marco.viewdemo E/MainActivity: ----mButton--------onTouch---->1
看到了吧?即使我們給ImageView
和Button
也都設(shè)置了setOnClickListener
民泵,但是在onTouch()
方法中返回了true
癣丧,所以onclick
都沒有執(zhí)行
總結(jié)
Android中分為兩種類型的View,即默認(rèn)可點(diǎn)擊的和默認(rèn)不可點(diǎn)擊的(重點(diǎn))
當(dāng)一個(gè)Touch事件發(fā)出的時(shí)候,View中最先執(zhí)行的是dispatchTouchEvent()
在dispatchTouchEvent()方法內(nèi)部先判斷onTouch()的返回值栈妆,根據(jù)返回值確定是否執(zhí)行onTouchEvent()方法
一個(gè)View的onTouch事件是優(yōu)先于onClick執(zhí)行的
onTouch()和onTouchEvent()的共同點(diǎn)是胁编,都在dispatchTouchEvent中執(zhí)行判斷厢钧;區(qū)別是,onTouch()優(yōu)先于onTouchEvent()執(zhí)行嬉橙,而且其返回值決定了onTouchEvent()能不能夠得到執(zhí)行
好了以上就是我對View的事件分發(fā)機(jī)制的全部理解早直,不足之處或者又不理解的地方請?jiān)谠u論區(qū)留下您的評論,大家共同進(jìn)步市框。
最后附上ViewDemo源碼:github下載
下一篇:Android事件分發(fā)機(jī)制( ViewGroup篇)