前言
触摸事件已经是老生常谈的事情了,但是可能大部分童鞋都只是知道在ViewTree下的机制,但是事件怎么到ViewTree的,可能处于一种比较空白的状态,这系列文章我希望从InputManagerService开始了解事件是怎么一步一步的传递到View中。
本系列分上下部,上部主要围绕应用层,针对Framework之后的事件分发的这段流程来讲,下部分就是围绕IMS,WMS到InputChannel来讲。
这系列文章会帮助到大家理解成个事件从产生到消费的完整流程:
- 了解InputManagerService,了解这个服务到底是做什么的(因篇幅问题并且涉及到Framework,本章先不讲这个,下一篇会上)
- 了解ViewRootImpl是怎么接收到InputManagerService发送的事件的
- 了解Activity是怎么接收到事件,如何从DecorView开始分发事件到ViewTree
- 回顾ViewTree的事件分发机制
但是整体介绍我会通过倒序的方式来介绍,越难的东西放在后面,让大家能够从浅到深去了解事件分发的过程,首先我们先回顾一下View的触摸事件机制吧~
1. 回顾一下View的触摸事件机制
看完流程图之后,肯定多多少少唤醒了你对View触摸事件机制的记忆,但是没有具体代码感觉还是有点空虚,接下来我们就重新对View和ViewGroup,以及分发、拦截、消费三个方法来一次回顾
1.1 三个方法,两个角色
三个方法:
- dispatchTouchEvent(事件分发)
- onInterceptTouchEvent(事件拦截)
- onTouchEvent(事件消费)
两个角色:
- View
- dispatchTouchEvent(事件分发)
- onTouchEvent(事件消费)
- ViewGroup
- dispatchTouchEvent(事件分发)
- onInterceptTouchEvent(事件拦截)
- onTouchEvent(事件消费)
我们可以看出来,两个角色的功能设计上还是多多少少有点区别的,View中不包含事件拦截,甚至都不关注事件分发,只关注事件是如何消费的,而ViewGroup则需要关注分发、拦截与消费。
接下来展示一下两个角色与对应方法的伪代码,帮助大家简单理解这两个角色具体的实现:
- View
- dispatchTouchEvent
#View dispatchTouchEvent 伪代码 public boolean dispatchTouchEvent(MotionEvent event) { //对于View来说,因为没有子View了,所以直接调用自己的onTouchEvent return onTouchEvent(event); }- onTouchEvent
#View onTouchEvent 伪代码 public boolean onTouchEvent(MotionEvent event) { //判断有没设置点击事件,有的话默认消费事件 if(onClickListenr!=null){ onClickListener.onClick(this); return true; } //判断有没设置Clickable标志位,有的话默认消费该事件 return isClickable; } - ViewGroup
- dispatchTouchEvent
#ViewGroup dispatchTouchEvent 伪代码 public boolean dispatchTouchEvent(MotionEvent event) { //先判断是否拦截时间,如果是的话则不分发事件,并且调用自己的onTouchEvent if(onInterceptEvent(event)){ this.onTouchEvent(event); return false; }else { //如果自身不拦截事件,则开始递归在点击范围内的子控件,调用他们的dispatchTouchEvent分发事件 for(child in legalChildren){ //如果子类消费时间,则返回true,当前方法出栈 if(child.dispatchTouchEvent(event)){ return true; } } } //如果自身不拦截,但是子类又不消费的时候,则调用自身onTouchEvent方法,决定是非消费 return this.onTouchEvent(); }- onInterceptEvent
#ViewGroup onInterceptTouchEvent 伪代码 public boolean onInterceptTouchEvent(MotionEvent ev) { if (需要拦截) { return true; } return false; }- onTouchEvent(与View一致就不重复贴了)
Ok,那么流程图已经明确将大流程阐述清楚,也通过伪代码去说明了程序具体怎么执行的,当然这里屏蔽了很多细节,只关注事件的分发、拦截、消费这三件事来讲,相信结合伪代码来看流程图,很快就可以掌握到ViewTree的事件分发流程是怎么样的。
那么接下来我们抛出一个疑问,DecorView的事件究竟是谁传给它的呢?接下来我们狠狠解剖一下!往着进阶的知识点进发!
2. 应用层是如何接收触摸事件的?
思考了好久这里要怎么说,还不如直接一个断点,把堆栈打出来来的实际!
可能有些童鞋一看就直呼卧槽,不要紧,我们只是看看整个事件传递的过程是怎么样的,大部分基本都是负责传递的逻辑,最后说一下,不知道为啥DecorView的superDispatchTouchEvent就是断不了点,只能断在上一步PhoneWindow中。
ok,我们先看看有哪些类参与了传递事件到DecorView:
- PhoneWindow
- Activity
- DecorView(WTF?居然有自己)
- ViewRootImpl
- ViewRootImpl(相关内部类)
- ViewPostImeInputStage
- InputStage
- AsyncInputStage
- WindowInputEventReciver
根据以上的类这里会涉及几个前置的知识点要先说一下,直接说以上几个类童鞋们会有点懵逼。
我们要先搞清楚一个核心问题,View是到底是怎么渲染到屏幕的,只有View渲染到屏幕,在用户跟屏幕交互的时候,事件才能准确传递到对应的View里。
为了搞清楚上面的核心问题,我们要先简单的了解下面的问题。
- PhoneWindow是什么,跟Activity的关系是什么?
- DecorView是什么时候初始化的?
- ViewRootImpl又是什么,跟PhoneWindow的关系是什么?
- 了解了上面三点之后,我们会在ViewRootImpl初始化的过程中,了解到应用层是怎么接收到WMS传递的触摸事件。
- 最后知道了怎么注册之后,再了解事件是怎么一层一层往下传递的?
了解完上面4个问题之后,我们最后再开始分析第五点,也就是在应用层事件分析的源头到传递到DecorView的过程。
我们要带着以上4个疑问来阅读下面的内容,了解它们的关系,最终目的是了解我们应用层是怎么注册触摸事件的接收的。
2.1 简单介绍PhoneWindow是什么?
PhoneWindow是什么?
- Window是一个抽象类,具体实现只有一个PhoneWindow类。
- Window是一个抽象的概念,表示了一个窗口,是View Hierarchy(视图树)的容器;
- 对Window的操作只能通过WindowManager,最终经IPC由WindowManagerService最终实现;
- Window不直接管理View Hierarchy,而是通过ViewRootImpl;
我们再用一张图说明PhoneWindow与Activity的关系。
再简单说一下PhoneWindow是在Activity的什么时候初始化的
//这里实现了Window的Callback接口,让Activity拥有了事件监听回调方法
public class Activity implements Window.Callback{
final void attach(Context context,...)
//省略部分代码
mWindow = new PhoneWindow(this, window, activityConfigCallback);
//这个Callback其中一个关键就是定义了触摸事件方法
mWindow.setCallback(this);
//为PhoneWindow设置WM
mWindow.setWindowManager(
(WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
mToken, mComponent.flattenToString(),
(info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
}
}
//我们再来看看这个Callback有哪些关键的方法
public interface Callback {
//让Activity拥有了接受事件分发的能力
public boolean dispatchTouchEvent(MotionEvent event);
}
通过上面代码我们可以得知,PhoneWindow在Activity的attach方法中进行了初始化,并且Activity作为Window.Callback的身份设置进去,而Window.Callback其中一个需要重写的方法是dispatchTouchEvent,那么可以说明,Activity的事件分发至少跟PhoneWindow有关系的。
2.2 DecorView在什么时候初始化的?
答案是从Activity的setContentView开始的,我们一步一步跟。
public class Activity{
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
}
这里调用了getWindow(),而getWindow其实获得的就是PhoneWindow,那么我们进入PhoneWindow的setContentView看看。
class PhoneWindow extends Window {
@Override
public void setContentView(int layoutResID) {
//1.如果mContentParent为空,就初始化DecorView
if (mContentParent == null) {
installDecor();
}
//2.把我们的布局装载到decorView的content区域
mLayoutInflater.inflate(layoutResID, mContentParent);
}
private void installDecor() {
//省略无关代码...
//1.1如果DecorView为空就生成一个DecorView
if (mDecor == null) {
mDecor = generateDecor(-1);
}
//1.2如果ContentView为空就从DecordView中获取ContentView
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
}
}
}
简单说完了PhoneWindow与DecorView的初始化,总结了以下几点:
- 了解了Activity与PhoneWindow与DecorView的关系。
- Activity实现dispatchTouchEvent方法是由Window.Callback接口声明的,并且Activit将自己作为Callback传进去PhoneWindow中,这说明了Activity的触摸事件分发是经过了PhoneWindow。
- 了解了DecorView的初始化过程。
2.3 ViewRootImpl
ViewRootImpl是什么,是基于Activity与WindowsManagerService通信的中介组件,主要负责Activity与WindowsManager的通信,并且主要负责以下几点:
- 通过WindowManager向WindowManagerService注册窗口。
- 负责了控制整个布局的测量布局绘制,并且将窗口绘制的结果传输到WindowManager。
- 接收WindowManagerService的事件回调。
- 本篇核心:向WindowManager注册InputChannel,并且通过InputEventReceiver,接收Channel中的触摸事件
ok,那2,3点我们本篇暂时不关心,我们关心第一第四点具体的代码实现是怎么样的。
接着我们就看看ViewRootImpl的初始化,顺便搞清楚以下几件事
- ViewRootImpl是怎么注册InputChannel,并且怎么样用InputEventReceiver监听InputChannel,获得事件的。
- 搞清楚ViewRootImpl与PhoneWindow的关系。
ViewRootImpl的一切的根源在ActivityThread的handleResumeActivity中开始的。
public final class ActivityThread{
@Override
public void handleResumeActivity(ActivityClientRecord r) {
final Activity a = r.activity;
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
//上面只是获取对象的逻辑,重点是下面这里会调用WindowManagerGlobal的addView
wm.addView(decor);
}
}
}
}
}
//我们接着wm.addView往下跟
public class WindowManagerGlobal{
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow, int userId) {
ViewRootImpl root;
synchronized (mLock) {
//这个IWindowSession其实就是跟WMS通信用的
IWindowSession windowlessSession = null;
if (wparams.token != null) {
for (int i = 0; i < mWindowlessRoots.size(); i++) {
ViewRootImpl maybeParent = mWindowlessRoots.get(i);
if (maybeParent.getWindowToken() == wparams.token) {
windowlessSession = maybeParent.getWindowSession();
break;
}
}
}
//创建一个ViewRootImpl对象
if (windowlessSession == null) {
root = new ViewRootImpl(view.getContext(), display);
} else {
root = new ViewRootImpl(view.getContext(), display,
windowlessSession);
}
try {
//调用其ViewRootImpl对象的setView函数
root.setView(view, wparams, panelParentView, userId);
} catch (RuntimeException e) {
throw e;
}
}
}
}
接着就是核心看看ViewRootImpl的setView方法,这里就是向AMS监听触摸事件的关键。
public final class ViewRootImpl{
//关注该方法
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
int userId) {
synchronized (this) {
if (mView == null) {
//1.我去,终于看到你了,InputChannel,这个就是跟WMS通信触摸事件的对象
InputChannel inputChannel = null;
if ((mWindowAttributes.inputFeatures
& WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
//2.进行初始化
inputChannel = new InputChannel();
}
try {
//3.将inputChannel注册到WMS接收信息
res = mWindowSession.addToDisplayAsUser(mWindow,inputChannel,...省略部分参数);
} catch (RemoteException e) {
}
if (inputChannel != null) {
if (mInputQueueCallback != null) {
mInputQueue = new InputQueue();
mInputQueueCallback.onInputQueueCreated(mInputQueue);
}
//4.通过WindowInputEventReceiver类来代理触摸事件通信的行为
mInputEventReceiver = new WindowInputEventReceiver(inputChannel,
Looper.myLooper());
}
}
}
}
}
这里源码有点长,以上源码其实就是讲清楚了我们一开始断点堆栈里面,在应用层的接收到事件的开端WindowInputEventReceiver这个类,是怎么注册的!
3. 知道了注册流程后,就去了解事件怎么传递了!
我们重新看回这张图,我不会完整的讲完每个调用哈~只会说关键的传递,不然篇幅要爆炸了。
我们看看一开始的调用,InputEventReceiver,其实就是WindowINputEventReceiver的父类我们看看其一开始回调的方法。
public abstract class InputEventReceiver {
//1.这里其实是调用了子类复写的onInputEvent
private void dispatchInputEvent(int seq, InputEvent event) {
mSeqMap.put(event.getSequenceNumber(), seq);
//1.1我们往下跟
onInputEvent(event);
}
}
final class WindowInputEventReceiver extends InputEventReceiver {
@Override
public void onInputEvent(InputEvent event) {
//...省略部分代码
//2.这里其实是调用了外部类ViewRootImpl的方法
enqueueInputEvent(event, this, 0, true);
}
}
class ViewRootImpl{
void enqueueInputEvent(InputEvent event){
//...省略代码
//3.是否立即执行
if (processImmediately) {
//3.1 立即执行
doProcessInputEvents();
} else {
//3.2 延迟执行,最后还是会执行doProcessInputEvents方法
scheduleProcessInputEvents();
}
}
}
接下来省略一部分传递流程,有兴趣的童鞋可以去看看源码,我直接用流程图来简化这一段描述
graph TB
ViewRootImpl.doProcessInputEvents --> ViewRootImpl.deliverInputEvent --> ViewRootImpl.ViewPostImeInputStage#内部类#.onProcess --> ViewRootImpl.ViewPostImeInputStage#内部类#.processPointerEvent
Ok,然后就到了有关DecorView这里了,我们看看ViewRootImpl.ViewPostImeInputStage#内部类#.processPointerEvent 具体实现。
#ViewRootImpl.ViewPostImeInputStage#内部类#.processPointerEvent
private int processPointerEvent(QueuedInputEvent q) {
final MotionEvent event = (MotionEvent)q.mEvent;
//...省略部分代码
//执行decorView的dispatchPointerEvent,开始分发事件,返回值表示事件是否有view处理
boolean handled = mView.dispatchPointerEvent(event);
maybeUpdatePointerIcon(event);
maybeUpdateTooltip(event);
mAttachInfo.mHandlingPointerEvent = false;
return handled ? FINISH_HANDLED : FORWARD;
}
接着看看DecorView代码
//事实上DecorView没实现这个方法,调的是父类View的方法
#View.dispatchPointerEvent
public final boolean dispatchPointerEvent(MotionEvent event) {
if (event.isTouchEvent()) {//如果是触摸事件则是true
return dispatchTouchEvent(event)
} else {
return dispatchGenericMotionEvent(event);
}
}
接下来看看dispatchTouchEvent方法
#DecorView
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
//这个mWindow是PhoneWindow,还记得我们讲PhoneWindow初始化流程吗,这个Callback其实就是Activity,
final Window.Callback cb = mWindow.getCallback();
//调用:Activity.dispatchTouchEvent
return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
}
终于到了Activity接收事件了!我们回到熟悉的地方啦!
#Activity
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
//调用PhoneWindow的superDispatchTouchEvent方法,实际上是开始让DecorView
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
//如果没有View消费,则调用Activity自己的onTouchEvent方法
return onTouchEvent(ev);
}
快到尾声了,我们再看看PhoneWindow的superDispatchTouchEvent
#PhoneWindow
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
//看吧,最后就是调用的DecorView的superDispatchTouchEvent
return mDecor.superDispatchTouchEvent(event);
}
快结束了,事件来到了DecorView
#DecorView
public boolean superDispatchTouchEvent(MotionEvent event) {
//结束了,去到ViewTree的流程了这里调用的是ViewGroup的dispatchTouchEvent
return super.dispatchTouchEvent(event);
}
本次触摸事件快递到站,回到了第一章的ViewTree触摸事件流程!
#ViewGroup dispatchTouchEvent 伪代码
public boolean dispatchTouchEvent(MotionEvent event) {
//先判断是否拦截时间,如果是的话则不分发事件,并且调用自己的onTouchEvent
if(onInterceptEvent(event)){
this.onTouchEvent(event);
return false;
}else {
//如果自身不拦截事件,则开始递归在点击范围内的子控件,调用他们的dispatchTouchEvent分发事件
for(child in legalChildren){
//如果子类消费时间,则返回true,当前方法出栈
if(child.dispatchTouchEvent(event)){
return true;
}
}
}
//如果自身不拦截,但是子类又不消费的时候,则调用自身onTouchEvent方法,决定是非消费
return this.onTouchEvent();
}
恭喜,结束了,但是只是结束了一半,本篇只是针对应用层的完整事件传递做一个简易版的流程分析,但是对WMS、对IMS甚至对InputChannel都没有展开来说,因为篇幅问题,我决定深入了解触摸事件这个文章分开两章来说,至于下一章什么时候写出来,那就要看大家的点赞和关注啦!
首先做一个总结,本篇文章说了以下几点:
- 在第一章中回顾了ViewTree的触摸事件机制。
- 在第二章中,我们了解了在Activity与PhoneWindow与DecorView与ViewRootImpl的关系,与他们初始化的过程,并且了解了应用层的事件是通过ViewRootImpl向WMS注册的。
- 在第三章中,我们了解事件是如何一步一步分发到Activity和DecorView,再从DecorView分发到ViewTree中的。
看到这里的童鞋估计也不容易了,不妨点个赞,收个藏,鼓励鼓励作者顺便鼓励鼓励自己看完这篇文章,那么有关Framework层的事件相关,就期待下一章吧!谢谢大家