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;
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
private void clearTouchTargets() {
TouchTarget target = mFirstTouchTarget;
if (target != null) {
do {
TouchTarget next = target.next;
target.recycle();
target = next;
} while (target != null);
mFirstTouchTarget = null;
}
}
可以看到,若为ACTION_DOWN
事件,就会触发 cancelAndClearTouchTargets(ev)
和resetTouchState()
方法,在resetTouchState()
方法中,有一个 clearTouchTargets()
方法,而在 clearTouchTargets()
方法内会将mFirstTouchTarget
设置为null
。我们暂时先记住这个mFristTouchTarget
已经置为null
了。
我们再看if (onFilterTouchEventForSecurity(ev)){}
该判断内的其他代码
if (onFilterTouchEventForSecurity(ev)) {
…
//记录是否拦截
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//判断是否设置了FLAG_DISALLOW_INTERCEPT这个标记位,默认为false
//disallowIntercept代表禁止拦截判断
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
//关键看这里
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
…
}
前面我们知道了mFirstTouchTarget
为null
,所以说只要是ACTION_DOWN
事件,就会进入到 if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {}
该方法块内,因为我们没有设置FLAG_DISALLOW_INTERCEPT
属性,所以它为默认为false
,所以进入到了 if (!disallowIntercept) {}
方法块内,调用了onInterceptTouchEvent()
方法。这里就解释了为何默认情况下dispatchTouchEvent()
后会调用onInterceptTouchEvent()
方法。
接下来我们看下if (onFilterTouchEventForSecurity(ev)) {}
方法块其余部分源码
if (onFilterTouchEventForSecurity(ev)) {
…
if (!canceled && !intercepted) {
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i–) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
//判断子元素是否能够接受点击事件
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
//调用子元素的dispatchTouchEvent方法
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
在上一个方法块中,intercepted=onInterceptTouchEvent()
的返回值,当不拦截的时候,intercepted==false
,进入到if (!canceled && !intercepted) {}
方法块内。可以看到我们通过for
循环遍历了所有的子元素,然后判断子元素是否能够接收到点击事件。判断子元素是否能够接收点击事件由两点决定:1、canViewReceivePointerEvents(child)
判断点击事件坐标是否落在子元素的区域内,2、isTransformedTouchPointInView(x, y, child, null)
判断子元素是否在播放动画。
所以说当onInterceptTouchEvent()
返回false
时,触发了点击事件,返回true
时没有触发。
若满足这两个条件,则事件传递给它处理。有一个为ture
则不会进入到该if (!canceled && !intercepted) {}
方法块内,而是执行下面的代码。
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {}
这个方法块,dispatchTransformedTouchEvent()
其实是调用子元素的dispatchTouchEvent()
方法,dispatchTransformedTouchEvent()
源码如下:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,View child, int desiredPointerIdBits) {
…
if (child == null) {
//child为null,调用父类的dispatchTouchEvent方法,ViewGroup父类为View,所以是调用View的dispatchTouchEvent方法。
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
//child不为null
handled = child.dispatchTouchEvent(transformedEvent);
}
…
}
可以看到当child
不为null
时,调用child.dispatchTouchEvent(transformedEvent)
,完成了从ViewGroup
到View
的事件分发。
7.3 事件怎么从ViewGroup分发到View中的
在上面的源码中,ViewGroup
的dispatchTouchEvent
方法内,当onInterceptTouchEvent
返回false
时,会调用dispatchTransformedTouchEvent()
方法,而该方法内会调用View
的dispatchTouchEvent
,在这里实现了事件从ViewGroup
到View
的事件分发。
7.4 小结:ViewGroup分发的流程图
8. View的事件分发
8.1 Demo演示
(1) 自定义一个MyButton
,继承自Button
,重写dispatchTouchEvent()
和onTouchEvent()
方法并打印日志
public class MyButton extends AppCompatButton {
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.i(TAG, "dispatchTouchEvent: ");
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.i(TAG, “onTouchEvent: ACTION_DOWN”);
break;
default:
break;
}
return super.onTouchEvent(event);
}
}
(2) 在Activity
布局中,放入该控件
<com.ld.eventdispatchdemo.viewdiapatch.MyButton
android:id=“@+id/btn_click”
android:text=“view的点击事件分发”
android:layout_width=“match_parent”
android:layout_height=“wrap_content”
android:clickable=“true”/>
(3) 在Activity
内为按钮添加点击事件和touch
事件
public class ViewDispatchActivity extends AppCompatActivity {
private static final String TAG = “Activity_viewDispatch”;
private Button btnClick;
@Override
protected void onCreate(Bundle savedInstanceState) {
btnClick = findViewById(R.id.btn_click);
btnClick.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.i(TAG, "onClick: ");
}
});
btnClick.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.i(TAG, "onTouch: ");
break;
}
//这里暂时先返回false查看日志
return false; // 返回false,onTouchEvent会被调用
}
});
}
}
当按钮的onTouch()
方法的返回值为false
时,打印的log日志为:
MyButton_viewDispatch: dispatchTouchEvent:
Activity_viewDispatch: onTouch:
MyButton_viewDispatch: onTouchEvent: ACTION_DOWN
MyButton_viewDispatch: dispatchTouchEvent:
MyButton_viewDispatch: dispatchTouchEvent:
最后
小编这些年深知大多数初中级Android工程师,想要提升自己,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。
因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人
都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
资料⬅专栏获取
false查看日志
return false; // 返回false,onTouchEvent会被调用
}
});
}
}
当按钮的onTouch()
方法的返回值为false
时,打印的log日志为:
MyButton_viewDispatch: dispatchTouchEvent:
Activity_viewDispatch: onTouch:
MyButton_viewDispatch: onTouchEvent: ACTION_DOWN
MyButton_viewDispatch: dispatchTouchEvent:
MyButton_viewDispatch: dispatchTouchEvent:
最后
小编这些年深知大多数初中级Android工程师,想要提升自己,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。
因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
[外链图片转存中…(img-y7aqUEEl-1718985851652)]
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人
都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
资料⬅专栏获取