事件序列:其实就是从手指触摸屏幕到离开屏幕所发生的一系列事件
2. 什么是事件分发
我们要讲的事件分发其实就是将点击事件传递到某个具体的View
,这个传递的过程就叫做事件分发
3. 事件在哪些对象间进行传递、顺序是什么
Activity
的UI
界面由Activity
、ViewGroup
、View
及其派生类组成
事件分发在这三个对象之间进行传递。
当点击事件发生后,事件先传到Activity
,再传到ViewGroup
,最终传到View
4. 事件分发有啥用?
默认情况下事件分发会按照由Activity
到ViewGroup
再到View
的顺序进行分发,当我们不想View
进行处理,让ViewGroup
处理,那就可以进行拦截,这些知识可以用于解决滑动冲突。
例如:外部滑动和内部滑动方向不一致,当ScrollView
嵌套Fragment
,且Fragemnt
内部有个竖向的ListView
,当用户左右滑动时,要让外部的View
拦截单击事件,当用户上下滑动时,要让内部的View
拦截点击事件。怎么拦截,在哪里拦截,就用到了我们这篇文章所讲的内容了。
5. 事件分发涉及到的函数及相应的作用
方法 | 作用 |
---|---|
dispatchTouchEvent | 进行事件分发 |
onInterceptTouchEvent | 事件拦截 |
onTouchEvent | 事件消耗(就是交给当前View处理) |
- dispatchTouchEvent: 用来进行事件分发,若事件能够传递到当前
View
,则此方法一定会被调用。 - onInterceptTouchEvent: 在
dispatchTouchEvent
方法内被调用,用来判断是否拦截某个事件。若当前View
拦截了某个事件,则该方法不会再被调用,返回结果表示是否拦截当前事件,该方法只在ViewGroup
中存在。 - onTouchEvent: 用来处理点击事件,返回结果表示是否消耗当前事件,若不消耗,则在同一事件序列中,当前
View
无法再次接收到事件。
这三个方法可用以下伪代码表示
public boolean dispatchTouchEvent(MotionEvent ev){
boolean consume = false;
if(onInterceptTouchEvent(ev)){
consume = onTouchEvent(ev);
}else{
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
对应的根ViewGroup
,当一个点击事件产生时,Activity
会传递给它,这时它的dispatchTouchEvent
就会被调用,若该ViewGroup
的onInterceptTouchEvent
返回true
,代表拦截该事件,但是否消耗该事件,还要看它的onTouchEvent
的返回值,如果不拦截,则代表将事件分发下去给子View
,接着子View
的dispatchTouchEvent
方法会被调用,如此反复直到事件被最终处理。
6. Activity的事件分发
当一个点击事件发生时,事件最先传到Activity
的dispatchTouchEvent()
进行事件分发。这里主要要弄明白Activity
是怎么将事件分发到ViewGroup
中的
6.1 Demo演示
我们先看一个案例
(1) 自定义一个MyViewGroup
,继承自ViewGroup
,重写dispatchTouchEvent()
方法
public class MyViewGroup extends ViewGroup{
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.i(TAG, "dispatchTouchEvent: ");
//这里我们暂时先返回false
return false;
}
}
(2) 在Activity
的布局中,使用该布局作为最外层布局
<com.ld.eventdispatchdemo.activitydispatch.MyViewGroup xmlns:android=“http://schemas.android.com/apk/res/android”
xmlns:app=“http://schemas.android.com/apk/res-auto”
xmlns:tools=“http://schemas.android.com/tools”
android:layout_width=“match_parent”
android:layout_height=“match_parent”
android:id=“@+id/myViewGroup”
android:orientation=“vertical”
tools:context=“.activitydispatch.ActivityDispatchActivity”>
</com.ld.eventdispatchdemo.activitydispatch.MyViewGroup>
(3) 重写该Activity
的dispatchTouchEvent()
和onTouchEvent()
方法,打印log日志
public class Activity extends AppCompatActivity{
…
private static final String TAG = “Activit_activitydispatch”;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if(ev.getAction()==MotionEvent.ACTION_DOWN){
Log.i(TAG, "dispatchTouchEvent: ");
}
//这里是仿照源码的格式写的
if(getWindow().superDispatchTouchEvent(ev)){
Log.i(TAG, “dispatchTouchEvent: 这里被调用”);
return true;
}
return onTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i(TAG, "onTouchEvent: ");
return super.onTouchEvent(event);
}
}
当MyViewGroup
的dispatchTouchEvent
返回false
时,打印的log日志为:
Activit_activitydispatch: dispatchTouchEvent:
MyViewGroup_activitydispatch: dispatchTouchEvent:
Activit_activitydispatch: onTouchEvent:
Activit_activitydispatch: onTouchEvent:
Activit_activitydispatch: onTouchEvent:
Activit_activitydispatch: onTouchEvent:
Activit_activitydispatch: onTouchEvent:
当MyViewGroup
的dispatchTouchEvent
返回true
时,打印的log日志为:
Activit_activitydispatch: dispatchTouchEvent:
MyViewGroup_activitydispatch: dispatchTouchEvent:
Activit_activitydispatch: dispatchTouchEvent: 这里被调用
MyViewGroup_activitydispatch: dispatchTouchEvent:
Activit_activitydispatch: dispatchTouchEvent: 这里被调用
MyViewGroup_activitydispatch: dispatchTouchEvent:
Activit_activitydispatch: dispatchTouchEvent: 这里被调用
MyViewGroup_activitydispatch: dispatchTouchEvent:
Activit_activitydispatch: dispatchTouchEvent: 这里被调用
仔细观察可以看到,当MyViewGroup的dispatchTouchEvent返回false时,Activity的onTouchEvent会被调用,返回true时,不会被调用,这是什么原因呢?
你可能会有疑问,我的Activity
的dispatchTouchEvent()
方法内为何要这样写呢?别急,看完下面的源码你就知道了。
6.2 源码解析
目的:
1、研究MyViewGroup
的dispatchTouchEvent
返回false
时,Activity
的onTouchEvent
会被调用,返回true
时,不会被调用的原因
Activity
中的dispatchTouchEvent()
源码如下:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
//该方法为空方法,不用管它
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
有没有很熟悉,为了方便观察打日志所以上面我们重写了Activity
的dispatchTouchEvent
方法,但内容和源码基本一致。
可以从源码看到,当getWindow().superDispatchTouchEvent(ev)==true
时,那么此时return ture
,自然就不会调用底下的onTouchEvent()
方法,即Activity
的onTouchEvent()
。
getWindow()
返回Window
对象,Window
是抽象类,而PhoneWindow
是Window
的唯一实现类,所以getWindow().superDispatchTouchEvent(ev)
其实就是调用的PhoneWindow
内的superDispatchTouchEvent(ev)
方法。
看看PhoneWindow
类源码:
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
PhoneWindow
将事件直接传递给了DecorView
,接下来看看DecorView
是什么
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
}
mDecor
是getWindow().getDecorView()
返回的View
,通过setContentView
设置的View
是该View
的子View
。
DecorView
继承自FrameLayout(ViewGroup)
,所以mDecor.superDispatchTouchEvent(event)
其实调用的就是ViewGroup
的dispatchTouchEvent()
方法,所以到这里你就懂了吧,
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
其实就相当于下面这个
if(viewgroup.DispatchTouchEvent(ev)){
return true;
}
所以说当我们的MyLayout
的DispatchTouchEvent()
返回true
时,Activity
的onTouchEvent
就不会被调用。
6.3 事件时怎么从Activity分发到ViewGroup中的
从上面Activity
的dispatchTouchEvent
源码可知道,默认状态下,它内部一定会调用该方法,而if()
条件中的内容其实就是调用ViewGroup
的dispatchTouchEvent()
方法,也就是在这里完成了Activity
到ViewGroup
的事件分发。
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
6.4 小结:Activity分发的流程图
7. ViewGroup的事件分发
上面讲了Activity
在dispatchTouchEvent
内将事件传递到了ViewGroup
的dispatchTouchEvent()
方法中,那么ViewGroup
又是如何将事件进一步向下分发的呢?
7.1 Demo演示
(1) 自定义MyLayout
,继承自LinearLayout
,重写onInterceptTouchEvent()
方法,并返回true
,重写dispatchTouchEvent()
、onTouchEvent()
方法,打印log日志
public class MyLayout extends LinearLayout {
private static final String TAG = “MyLayout_ViewGroupDispatch”;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.i(TAG, "dispatchTouchEvent: ");
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.i(TAG, "onInterceptTouchEvent: ");
//此处暂时返回true观察现象
return true;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i(TAG, "onTouchEvent: ");
return super.onTouchEvent(event);
}
}
(2) 在Activity
的布局中,使用该布局作为最外层布局,并在该布局内添加一个按钮
<com.ld.eventdispatchdemo.viewgroupdispatch.MyLayout xmlns:android=“http://schemas.android.com/apk/res/android”
xmlns:app=“http://schemas.android.com/apk/res-auto”
xmlns:tools=“http://schemas.android.com/tools”
android:layout_width=“match_parent”
android:layout_height=“match_parent”
android:orientation=“vertical”
android:id=“@+id/myLayout”
tools:context=“.viewgroupdispatch.ViewGroupActivity”>
</com.ld.eventdispatchdemo.viewgroupdispatch.MyLayout>
(3) 在Activity
内为按钮添加点击事件
public class ViewGroupActivity extends AppCompatActivity {
private Button btn1;
private static final String TAG = “Activity_ViewGroupDispatch”;
@Override
protected void onCreate(Bundle savedInstanceState) {
btn1 = findViewById(R.id.btn1);
btn1.setOnClickListener(new View.OnClickListener() {
@SuppressLint(“LongLogTag”)
@Override
public void onClick(View v) {
Log.i(TAG, “onClick: 点击了按钮1”);
}
});
}
}
当MyLayout
的onInterceptTouchEvent()
返回true
时,分别点击空白处、点击按钮,log日志如下:
//点击空白处
MyLayout_ViewGroupDispatch: dispatchTouchEvent:
MyLayout_ViewGroupDispatch: onInterceptTouchEvent:
MyLayout_ViewGroupDispatch: onTouchEvent:
//点击按钮
MyLayout_ViewGroupDispatch: dispatchTouchEvent:
MyLayout_ViewGroupDispatch: onInterceptTouchEvent:
MyLayout_ViewGroupDispatch: onTouchEvent:
当MyLayout
的onInterceptTouchEvent()
返回false
时,分别点击空白处、点击按钮,log日志如下:
//点击空白处
MyLayout_ViewGroupDispatch: dispatchTouchEvent:
MyLayout_ViewGroupDispatch: onInterceptTouchEvent:
MyLayout_ViewGroupDispatch: onTouchEvent:
//点击按钮
MyLayout_ViewGroupDispatch: onInterceptTouchEvent:
MyLayout_ViewGroupDispatch: dispatchTouchEvent:
MyLayout_ViewGroupDispatch: onInterceptTouchEvent:
MyLayout_ViewGroupDispatch: dispatchTouchEvent:
MyLayout_ViewGroupDispatch: onInterceptTouchEvent:
MyLayout_ViewGroupDispatch: dispatchTouchEvent:
MyLayout_ViewGroupDispatch: onInterceptTouchEvent:
MyLayout_ViewGroupDispatch: dispatchTouchEvent:
MyLayout_ViewGroupDispatch: onInterceptTouchEvent:
Activity_ViewGroupDispatch: onClick: 点击了按钮1
可以看到当ViewGroup(MyLayout)
的onInterceptTouchEvent()
返回true
时,并没有触发按钮的点击事件,并且自身的onTouchEvent()
方法被调用,当返回false
时,按钮的点击事件触发,但自身的onTouchEvent()
方法未被调用。
而且在默认状态下,onInterceptTouchEvent()
一定会被调用。
以上现象是什么原因呢?接下来我们看看ViewGroup的dispatchTouchEvent()
方法的源码
7.2 源码解析
目的:
1、研究当ViewGroup(MyLayout)
的onInterceptTouchEvent()
返回true
时,并没有触发按钮的点击事件,并且自身的onTouchEvent()
方法被调用,当返回false
时,按钮的点击事件触发,但自身的onTouchEvent()
方法未被调用的原因
2、分发事件是怎么从ViewGroup
分发到View
中的
ViewGroup
的dispatchTouchEvent()
源码:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
…
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
…
//一大堆代码
}
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
}
我们想要了解事件是如何分发,其实是主要看ViewGroup
的dispatchTouchEvent()
方法什么时候返回true
,什么时候返回false
。看源码可以知道,ViewGroup
的dispatchTouchEvent()
方法返回的是handle
的值,所以我们只需要观察该方法内改变handle
值的语句。
首先初始化了handle
的值,默认为false
然后你可以看到dispatchTouchEvent()
的大部分内容都在if (onFilterTouchEventForSecurity(ev)) {}
这个条件判断内,也就是说如果onFilterTouchEventForSecurity(ev)
方法返回true
的话,那么就进入该if
判断内。若返回false
,则dispatchTouchEvent()
返回初始值为false
的handled
,表示不分发事件。
查看下onFilterTouchEventForSecurity(ev)
方法
public boolean onFilterTouchEventForSecurity(MotionEvent event) {
//noinspection RedundantIfStatement
if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0
&& (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
// Window is obscured, drop this touch.
return false;
}
return true;
}
FILTER_TOUCHES_WHEN_OBSCURED
是android:filterTouchesWhenObscured
属性所对应的。android:filterTouchesWhenObscured
是true
的话,则表示其他视图在该视图之上,导致该视图被隐藏时,该视图就不再响应触摸事件。MotionEvent.FLAG_WINDOW_IS_OBSCURED
为true
的话,则表示该视图的窗口是被隐藏的
而我们并没有在XML
中为控件设置android:filterTouchesWhenObscured
属性,所以它==0,没有进入if()
方法,所以onFilterTouchEventForSecurity()
方法返回true
,那么 if (onFilterTouchEventForSecurity(ev))
判断必定会进入如下的判断中。
接下来我们看看if(onFilterTouchEventForSecurity(ev))
判断下的内容
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
…
}
private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级安卓工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新Android移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
最后
愿你有一天,真爱自己,善待自己。
本文在开源项目:Android开发不会这些?如何面试拿高薪 中已收录,里面包含不同方向的自学编程路线、面试题集合/面经、及系列技术文章等,资源持续更新中…
-1710508090609)]
[外链图片转存中…(img-GeTWlWYW-1710508090610)]
[外链图片转存中…(img-oJF6r4A5-1710508090610)]
[外链图片转存中…(img-AyrL5WGF-1710508090611)]
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
[外链图片转存中…(img-aBAEVDbo-1710508090611)]
最后
愿你有一天,真爱自己,善待自己。
本文在开源项目:Android开发不会这些?如何面试拿高薪 中已收录,里面包含不同方向的自学编程路线、面试题集合/面经、及系列技术文章等,资源持续更新中…