纸上得来终觉浅,看了很多别人写的有关View的事件分发机制的博客,但别人的终究
是别人的,把自己的理解写下来,才是自己的,但万变不离其宗。本篇将从另外一个
角度带你理解View的事件分发机制。
序言
关于 View 和 ViewGroup 的事件分发机制 我打算用两篇博客来写,
本篇主要讲述 View 的事件分发机制,下一篇讲述ViewGroup的事件分发机制
View 的事件分发和领导派发任务是很相似的,所以我们先通过这个生活中常见的场景引入,以便大家更好的理解View的事件分发
场景描述:
当老板有一个问题需要解决的时候,他可以自己直接解决,也可以将问题交给经理去解决,当然老板不可能所有问题都
要自己解决,那样还要经理和员工干什么,所以老板一般会将任务派发给经理,一般经理也不会自己解决,他会将问题
派发给员工去解决,员工嘛就是干活的,否则就得走人了,当然不排除有些问题员工没有能力解决那只能将问题返回给
经理,经理有能力解决就解决了,如果经理也解决不了,那就把这个问题扔回给老板,最后只能由老板自己解决了。
类似地:
我们的View的事件分发也是这样的,当我们手接触到手机屏幕的时候,屏幕接收到一个Touch事件,系统会将这Touch
事件封装成一个对象MotionEvent,之后所有的事件处理都将与这个MotionEvent相关,和领导派发任务一样,我们的
Touch事件会首先到达最外层的ViewGroup(Activity),然后再一层一层地向子View派发,最终会到达最内层的View,
最内层的View可以处理这个Touch事件,也可以将这个Touch事件扔回给自己的父View
对于View和ViewGroup我们需要关注下面的个方法:
View 两个方法:
-
dispatchTouchEvent (
MotionEvent event
)//负责事件分发 -
onTouchEvent(
MotionEvent event
)//当前View自己处理当前事件
ViewGroup 三个方法:
-
dispatchTouchEvent (
MotionEvent event
)//负责事件分发 -
onInterCeptTouchEvent(
MotionEvent event
)//处理是否拦截当前事件 -
onTouchEvent(
MotionEvent event
)//当前View自己处理当前事件
我们注意到上面的方法都参数都是一个 MotionEvent 对象,View
比ViewGroup
少了一个onInterCeptTouchEvent()
方法,这时因为对于View
来讲,它是不能有子View
的,所以不需要拦截事件的方法
正文:
我们先从Android中两种不同类型的控件开始介绍
- 可被点击的:
Button、ImageButton...
//即本身CLICKABLE为true
的 - 不可被点击的:
ImageView、TextView...
//即本身CLICKABLE为false
的
注意:上面两点很重要,大家一定要记在心里。也可能你会有疑问我们平常用的ImageView
和TextView
都能点击呀,请注意我说的是本身可被点击的,ImageView
和TextView
能被点击是因为我们给他们设置了setOnClickListener()
,这样就把这两种View
的CLICKABLE
置为true
了,关于这一点大家可以通过调用这个方法前后添加log日志去验证。
我们先新建一个project: ViewDemo
ViewDemo 很简单:在布局文件中添加一个 ImageView 和一个 Button 控件
在MainActivity
中初始化并给两个控件都设置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
并设置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;
}
});
}
运行程序依次点击 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
我们可以看到点击ImageView
的时候日志打印了一次,点击Button
的时候日志打印了三次
如果我们把两个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;
}
});
}
分别点击 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
此时可以看到两个控件的日志打印都是三次,DOWN
、MOVE
、UP
一分钟时间思考一下这是为什么?
下面让我带大家从源码角度分析一下为什么会这样:
前面我们提到过对于View来讲我们关注两个方法:
-
dispatchTouchEvent (
MotionEvent event
)//负责事件分发 -
onTouchEvent(
MotionEvent event
)//当前View自己处理当前事件
我们需要知道,当一个View
接收到Touch
事件的时候,首先被调用的是当前View
的dispatchTouchEvent()
方法:
下面我们看一下View源码中的这个方法: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;
}
从方法的注释我们可以知道:这个方法是处理事件分发的,如果return true
说明当前事件被消费了,便不再继续分发,否则继续分发
我们主要看方法中的两个if判断:先看第一个
li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)
因为我们给控件设置了onTouchLishener
所以mListenerInfo
不为空,li != null
为true
,li.mOnTouchListener != null
也为true
,这个mOnTouchListener
就是我们mImageView.setOnTouchListener(new View.OnTouchListener())
的时候new
出来的,第三个条件主要取决于mViewFlags
,在Android系统中所有控件默认都是enable
的,除非我们对一个控件设置view.setEnable(false)
,所以(mViewFlags & ENABLED_MASK) == ENABLED
也为true
,那么能不能进入到if()
内部关键看li.mOnTouchListener.onTouch(this, event)
,这个方法的返回值就是我们最开始setOnTouchListener
中重写的onTouch()
方法的返回值,默认为false
,后来被我们改成了true
。
在onTouch()
方法中默认false
,也就是说此处的第一个if()
条件不成立,那么将到第二个if()
判断:!result
为true
,那么我们主要看onTouchEvent(event)
的返回值,那么这个onTouchEvent(event)
有时什么东东呢?我们进入到这个方法中(干扰代码已省略):
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;
}
从方法中我们可以看到这个方法的返回值也是 boolean
类型的,看到这里不知道大家能不能想起我在开头说的Android中两种不同类型,一种是默认可以点击的,一种是默认不可以点击,我们的 ImageView
和 Button
的可点击与否的作用就在这个方法中的到了体现,让我们来看一下这个 if()
条件判断吧
先说默认条件下两个Touch方法都返回false
- ImageView
如果是 ImageView
不用想,默认就是不可点击的,也就是说CLICKABLE
和LONG_CLICKABLE
都为false
,那么好,这个条件我们是进不去的,再看最后return
值是false
,也就是说只要if()
判断进不去,全都会返回false
,回到我们之前的dispatchTouchEvent()
中,既然onTouchEvent()
也返回了false
,那么最终dispatchTouchEvent()
的返回值也是false
,说明这个Touch
的DOWN
事件没有被消费,既然DOWN
事件没有被消费,也就没有后面的MOVE
和UP
,所以最初的ImageView
值打印了一行log,
- Button
如果是Button
默认就是可点击的CLICKABLE
和LONG_CLICKABLE
都为true
,也就是说if()
判断能够进入,那么就说明,里面的DOWN
、MOVE
和UP
事件都会响应,另外我们再看,进入if()
后最后一句: return true;
这句返回了true
,也就是说,只要进入了这个if()
判断,就回返回true
,也就是我们的dispatchTouchEvent()
中的第二个if()
判断会返回true
,那么我们的dispatchTouchEvent()
最终也就回返回true
,也就是说我们的Touch
事件被消费了。当然DOWN
、MOVE
、UP
事件都会响应,所以我们的Button
打印了三行log
。
再说之后我们把两个Touch方法都返回true
当这两个onTouch()
方法都返回true的时候,就更简单了,还是看我们的dispatchTouchEvent()
,即第一个if()
判断中mOnTouchListener.onTouch(this, event)
为true
,前面我们已经判断了,if()
中其他的都为true
,所以第一个if()
判断可以进入,最终返回了true
,dispatchTouchEvent()
的返回值为true
,根本就走不到第二个if()
条件判断,所以跟后面的onTouchEvent()
就没什么关系了。所以最终这个Touch
事件在dispatchTouchEvent()
中的回调的onTouch()
方法中得消费掉了。
注意:不知道有没有同学发现我上面onTouchEvent()
方法中除了省略的代码之外,还留下了一行这个方法就与我们的Click事件又关了,我们先看一下这个方法: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;
}
不过要想执行到 performClick() 这个方法要想执行到,首先得能进入到上面 onTouchEvent() 方法中的 if() 条件判断
下面我们在MainActivity的onCreate方法中给ImageView
和Button
设置两个click
监听事件,把onTouch事件还原到默认的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---->");
}
});
先点击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---->
再点击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
打印的结果我们可以理解,但是我们只给ImageView
和Button
添加了Click
监听事件,其他都没变,两个onTouch()
方法返回的竟然也都一样,按照我们最初只添加两个setOnTouchListener
方法,ImageView
应该只打印一次才对呀,怎么添加了一个setOnClickListener
,就打印三次了呢?而切如果按照我们最开始的推论,既然ImageView
是默认不可点击的,那么在onTouchEvent
方法中的if()
判断条件不可能进入呀,更不会执行到performClick()
方法,怎么会也打印出除了最后一行onClick
的log
呢?
猜想:上面log
既然显示执行了performClick()
,也就是说onTouchEvent()
中的if()
判断条件进入了,而我们只设置了setOnClickListener
方法,莫非这个方法做了什么手脚?是不是把ImageView
的CLICKABLE
的值改变了呢?
我们进入到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);
看到了吧?我们的猜想是对的,一旦我们设置了setOnClickListener
当前View
的CLICKABLE
的值就会被置为true
,这就不难理解为什么ImageView
也能进入到if()
判断中,而且执行了performClick()
方法。
通过上面例子我们也的出来一个结论:View
的onTouch()
方法是先于onClick()
方法执行的,如果onTouch()
方法返回true
即被消费了,就不会执行onTouchEvent()
方法,也就执行不到onClick()
方法,
这点我们也可以验证一下,就是把上面两个setOnTouchListener中的onTouch方法都置为true然后分别点击ImageView
和Button
然后看log
:
点击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
点击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
也都设置了setOnClickListener
,但是在onTouch()
方法中返回了true
,所以onclick
都没有执行
总结
-
Android中分为两种类型的View,即默认可点击的和默认不可点击的(重点)
-
当一个Touch事件发出的时候,View中最先执行的是dispatchTouchEvent()
-
在dispatchTouchEvent()方法内部先判断onTouch()的返回值,根据返回值确定是否执行onTouchEvent()方法
-
一个View的onTouch事件是优先于onClick执行的
-
onTouch()和onTouchEvent()的共同点是,都在dispatchTouchEvent中执行判断;区别是,onTouch()优先于onTouchEvent()执行,而且其返回值决定了onTouchEvent()能不能够得到执行
好了以上就是我对View的事件分发机制的全部理解,不足之处或者又不理解的地方请在评论区留下您的评论,大家共同进步。
最后附上ViewDemo源码:github下载
下一篇:Android事件分发机制( ViewGroup篇)
网友评论