1. WindowManagerService
WindowManagerService是一个独立的进程,拥有自己的main方法。它的内部有多个List用于存放各种各样状态的Activity。
WindowManagerService接收到屏幕的点击事件后,就会分发给其内部正在显示的activity,这个就是activity的点击事件被分发的基本原理。
总之Acitivity的dispatchTouchEvent就是这样被调用的,而MotionEvent也是WindowManagerService生成的。
2. Activity和Window
事件分发机制的入口是Acitivity类的dispatchTouchEvent,他会调用内部的mWindow来继续分发事件。
//Acitivity的
public class Activity{
@UnsupportedAppUsage
private Window mWindow;
public boolean dispatchTouchEvent(MotionEvent ev) {
// 不重要,忽略
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
// 不要关注别的,主要关注这行代码
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
public Window getWindow() {
return mWindow;
}
}
那这个Window又是什么呢?简单介绍一下。Window本身是一个抽象类,他的实例是phoneWindow对象,说白了就是每个Activity都会持有一个phoneWindow对象。
public abstract class Window {}
public class PhoneWindow extends Window implements MenuBuilder.Callback {}
注意,Window是一个抽象类,而PhoneWindow继承了Window,所以其实Window类并没有多少View相关的方法(因为没有继承View类)。
但是PhoneWindow内部有一个DecorView。DecorView中有两个View,TitleView和ContentView。TtitleView就是我们平时一直隐藏的actionBar,而ContentView就是我们平时写的xml文件,还记不记得我们每个Activity都一定会写的setContentView,实际上设置的就是DecorView的contentView。
最后再看一下事件从Activity分发到PhoneWindow后又做了些什么
public class PhoneWindow extends Window implements MenuBuilder.Callback {
private DecorView mDecor;
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
}
传到了DecorView,再看看DecorView中做了什么
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
final Window.Callback cb = mWindow.getCallback();
// 源码中是一行长代码,为了方便阅读我把它分开了
// 这部分是有关Window的回调,暂时不关注
if(cb != null && !mWindow.isDestroyed() && mFeatureId < 0)
return cb.dispatchTouchEvent(ev);
// 重点关注这行
return super.dispatchTouEvent(ev);
}
}
最终是调用了父类的dispatchTouchEvent,我们都知道FrameLayout本身就是ViewGroup的一种,接下来就要看View和ViewGroup的事件是如何分发的。
总结
- Activity内部持有一个PhoneWindow,而PhoneWindow又持有一个DecorView,DevorView其实就是ActionBar加上我们的ContentView。
- 事件分发流程是Activity-> PhoneWindow-> DecorView。
- DecorView本质是FrameLayout,即ViewGroup。
3. ViewGroup
刚才也说了,DecorView的本质是一个ViewGroup,这意味着事件分发本质上只有三个类参与,Activity,ViewGroup和View。
事件分发,只有存在可以分发的对象,才能把事件分发出去,View本身就是一个控件,已经是一个个体了,所以无法执行分发的这个操作。
ViewGroup是控件组,他的内部有子控件,存在分发事件的对象,所以ViewGroup可以执行分发的这个行为。
分发主要是通过dispatchTouchEvent这个方法,先看一下:
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false; // 这个变量表示事件是否被处理
final int actionMasked = action & MotionEvent.ACTION_MASK; // 表示事件的类型
......
return handled;
}
顺便介绍一下事件的不同类型:
MotionEvent.ACTION_DOWN | 按下View(所有事件的开始) |
MotionEvent.ACTION_UP | 抬起View(与DOWN对应) |
MotionEvent.ACTION_MOVE | 滑动View |
MotionEvent.ACTION_CANCEL | 结束事件(非人为原因) |
每当我们开始分发事件的时候,先要判断事件是否被拦截,是否拦截主要根据onInterceptTouchEvent方法和disallowIntercept共同决定。如果不拦截事件,就开始分发事件内容。
public boolean dispatchTouchEvent(MotionEvent ev) {
// 省略关联性不高的代码,只看最关键的部分
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; // 这个变量表示是否禁用了拦截功能
if (!disallowIntercept) { // 如果没有启用禁用拦截功能
intercepted = onInterceptTouchEvent(ev); // 通过该方法判断是否拦截事件
ev.setAction(action);
} else { // 如果启用了禁用拦截功能,就不会拦截该点击事件
intercepted = false;
}
} else {
intercepted = true;
}
if (!canceled && !intercepted) {
.....
分发点击事件
......
}
......其他代码......
}
/**
* 这个方法,返回true的条件太多,一般可以看成返回false
* 也就是说,一般情况下,ViewGroup不拦截事件
* 如果用户有特别需求,需要拦截事件,就重写该方法,然后返回true即可。
**/
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
&& ev.getAction() == MotionEvent.ACTION_DOWN
&& ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
&& isOnScrollbarThumb(ev.getX(), ev.getY())) {
return true;
}
return false;
}
事件分发的本质分为两种,一种是当前ViewGroup中,有可以被分发事件的子控件,事件就分发给它;另一种是事件无法被任何子控件接收,ViewGroup就自己处理事件。
public boolean dispatchTouchEvent(MotionEvent ev) {
......省略前面那一段判断intercepted的代码......
if (!canceled && !intercepted) {
......这里会省略一些我认为不重要的代码......
final int childrenCount = mChildrenCount;
if (childrenCount != 0) {
final View[] children = mChildren;
// 注意,这个地方是倒序获取子View
for (int i = childrenCount - 1; i >= 0; i--) {
// 下面这两行,不要去管他具体是怎么实现的,你就知道他是获取子View的下标和实例就行了
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
// 第一个是判断child是否能接收事件,第二个是判断点击事件是否在子View的范围内,两个条件不满足任意一个,就跳过该子View
// 这里不要去管这两个方法是怎么实现的,我们根据方法名能知道方法的作用就好了
if (!child.canReceivePointerEvents()|| !isTransformedTouchPointInView(x, y, child, null))
continue;
// 分发转换为点击事件
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// 此处和源码部分不符,我有意省略了TouchTarget相关的内容,我认为关联性不强,但是逻辑应该是对的
handled = true;
}
}
}
}
if (mFirstTouchTarget == null) {
// 分发转为点击事件,但是这里子View传的是空
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
return handled;
}
// 重点关注第三个参数,View child
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
// 当传入的child为空时,该View就会自己处理这个事件
handled = super.dispatchTouchEvent(event);
} else {
// 否则就让子View来处理这个事件
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
.......省略一大段.......
}
最后一段的内容是这样,当我们的事件没有被拦截时,就会遍历这个ViewGroup所持有的子View,判断到某个View如果在事件点击的范围内时,就调用dispatchTransformedTouchEvent这个方法,将该子View作为参数传进去。
这里要注意,对于遍历的方式,ViewGroup是倒着遍历他的子View集合的,原因是。每当我们往ViewGroup中添加View时,后添加的View总是会显示在屏幕的上方从而盖住先添加的view,而用户点击到这个地方时,他肯定是想点击他所看到的控件,也就是后添加的View,所以这里需要用倒着的顺序遍历。
如果遍历完所有的子View,还是没能处理这个点击事件(比如说这个事件点击到了该ViewGroup的空白地点,即上图中白色部分),还是调用dispatchTransformedTouchEvent这个方法,此时child传空
再来看看dispatchTransformedTouchEvent这个方法,当我们传入的child为空时,就会调用super.dispatchTouchEvent(event);
即ViewGroup的父类View的dispatchTouchEvent;而传入的child不为空时,就调用child的dispatchTouchEvent(event);
ViewGroup总结
最后总结一下ViewGroup的事件分发流程
- dispatchTouchEvent:判断该事件是否被拦截,如果被拦截,由该ViewGroup自己处理事件
- dispatchTouchEvent:没有被拦截,倒序遍历所有的子View,选择处于点击范围内的子View,让他处理该事件
- dispatchTouchEvent:如果所有子View都不能处理该事件,由该ViewGroup自己处理事件
- dispatchTransformedTouchEvent:处理事件时调用的方法,如果child变量传null,则调用该ViewGroup父类View的dispatchTouchEvent(即自己处理点击事件);child不为空,调用child的dispatchTouchEvent(即子View处理点击事件)。
- onInterceptTouchEvent:判断是否拦截事件,一般返回false。如果有特殊需要重写该方法。
4. View
最后看看View在事件分发过程中做了什么,按照道理来说,只有持有子控件的ViewGroup,才能将事件分发给别人。View本身是一个单独的控件,并不存在可以分发事件的对象。
从刚刚ViewGroup的源码中,我们知道了View的事件分发也是从dispatchTouchEvent开始。
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
// 这个条件你就默认他为true就行了
if (onFilterTouchEventForSecurity(event)) {
ListenerInfo li = mListenerInfo;
// 注意看li.mOnTouchListener.onTouch(this, event))这个条件,他会调用onTouch方法,这个方法是一个接口
// 我们如果有设置touch监听事件的话,就会调用这个方法
if (li != null
&& li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
// 如果没有TouchListener监听事件或者onTouch没有返回true时就执行onTouchEvent(event)
if (!result && onTouchEvent(event)) {
result = true;
}
}
return result;
}
dispatchTouchEvent主要做了两件事,他会先检测该View有没有设置TouchListener接口和onTouch方法,如果有就执行。
然后如果TouchListener.onTouch方法返回false,就执行onTouchEvent方法。
public boolean onTouchEvent(MotionEvent event) {
final int action = event.getAction();
// 看变量名就知道,是检测是否可以点击的变量
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
......
performClickInternal();
......
break;
case MotionEvent.ACTION_DOWN:
.......
break;
case MotionEvent.ACTION_CANCEL:
......
break;
case MotionEvent.ACTION_MOVE:
.......
break;
}
return true;
}
return false;
}
}
onTouchEvent就是处理点击事件,如果该事件可以点击就一定返回true,否则一定返回false。
然后就是根据不同的事件类型进行处理,最后单独列出performClickInternal()这个方法,这个方法最终会调用到OnClickLisener.onClick这个方法,这就表示。我们平时的单击控件,实际上是在手指抬起来之后,才处理的。
参考材料
Android事件分发机制详解:史上最全面、最易懂 - 简书
https://www.jianshu.com/p/38015afcdb58
Activity 与 Window、PhoneWindow、DecorView 之间的关系详解_Chin_Style的博客-CSDN博客_phonewindow
https://blog.csdn.net/weixin_41101173/article/details/79685305
码牛学院VIP课程 2020-7-12 事件分发机制