MotionEvent事件的InputStage队列:
- 从《ViewRootImpl事件队列过程》文章,我们知道了InputStage的事件队列一层一层下来,如果没有持续地转发或者标示完成。就会回调到InputStage自己的onProcess()函数,去进行深一步的事件分发处理流程。
- 由此,我们通过事件的大概分类,分别对InputStage的onProcess()函数进行详细的分析,看看事件是怎么传递和分发的。
- 我们大致可以将事件划分为:按键事件(键盘、实体按键)、触摸事件(屏幕触摸,鼠标)
- 上篇文章,我们看到过两个事件处理队列:分别代表是否经输入法预处理事件。
- 而区分两者的方案就是:判断事件是否是鼠标指针、屏幕触摸
- 因此我们可以直接认为:
- 按键事件,走的就是需要输入法预处理的事件队列:
- 触摸事件,走的就是不需要输入法预处理的事件队列:
InputStage mFirstInputStage;//输入法预处理队列的表头
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
...
InputStage viewPostImeStage = new ViewPostImeInputStage(mSyntheticInputStage);
InputStage nativePostImeStage = new NativePostImeInputStage(viewPostImeStage,
"aq:native-post-ime:" + counterSuffix);
InputStage earlyPostImeStage = new EarlyPostImeInputStage(nativePostImeStage);
InputStage imeStage = new ImeInputStage(earlyPostImeStage,
"aq:ime:" + counterSuffix);
InputStage viewPreImeStage = new ViewPreImeInputStage(imeStage);
InputStage nativePreImeStage = new NativePreImeInputStage(viewPreImeStage,
"aq:native-pre-ime:" + counterSuffix);
mFirstInputStage = nativePreImeStage;
mFirstPostImeInputStage = earlyPostImeStage;
...
}
触摸事件:
- 从上面的事件队列的构造中,我们可以看到mFirstPostImeInputStage最后被赋值为earlyPostImeStage。这说明,输入法不处理的事件队列的表头,就是EarlyPostImeInputStage。
EarlyPostImeInputStage:
- 经过前面的KeyEvent事件的InputStage传递,我们知道了代码首先会进入deliver()函数,随后调用对应的InputStage实现类的onProcess()函数,我们直接来看EarlyPostImeInputStage的onProcess():
@Override
protected int onProcess(QueuedInputEvent q) {
if (q.mEvent instanceof KeyEvent) {//如果是键盘事件,走这个分支
return processKeyEvent(q);
//在KeyEvent的InputStage处理文章的结尾,我们跟到事件在异步处理后,其实是走到了这里来。
} else {//否则就走入到Touch事件的分支
final int source = q.mEvent.getSource();
if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
//如果事件源是SOURCE_CLASS_POINTER,就走入到下面的函数
//SOURCE_CLASS_POINTER:在Google源码的注释中有提到:SOURCE_TOUCHSCREEN、SOURCE_MOUSE
//因此我们可以判断,屏幕触摸和鼠标点击操作,都会走到这个分支
return processPointerEvent(q);
}
}
return FORWARD;
}
- Touch事件在EarlyPostImeInputStage的onProcess中,最终走到了processPointerEvent()函数中。
- 在这个函数中,有一些比较有意思的逻辑处理,但是其末尾的返回值类型是FORWARD,表示会将事件进行进一步转发。
- 来看下processPointerEvent()函数:
private int processPointerEvent(QueuedInputEvent q) {
//获取MotionEvent对象
final MotionEvent event = (MotionEvent)q.mEvent;
...
// Enter touch mode on down or scroll, if it is coming from a touch screen device,
// exit otherwise.
//有意思的地方在这里,Android会先检查是否当前是否处于TouchMode
//如果是MotionEvent传过来,还不是TouchMode的话,会先转换系统为TouchMode状态
//TouchMode是什么意思呢:鼠标和键盘操作时,有些View是可以存在Focus状态、Hover状态的。这时候,区别于触摸模式下,Focus和Hover状态不需要出现。因此TouchMode表示的就是触摸模式
final int action = event.getAction();
if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_SCROLL) {
if (mView != null
&& mView.getContext() != null
&& mView.getContext().getDisplay() != null) {
ensureTouchMode(true);
} else {
ensureTouchMode(event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
}
}
...
return FORWARD;
}
- 将窗口设置为TouchMode其实可以分为三步:
- 第一步:客户端通知WindowManager,我现在要改变我的TouchMode状态
- 第二步:修改ViewRootImpl中的mAttachInfo,更新AttachInfo中的TouchMode状态。并且回调TouchMode变化的CallBack
- 第三步:进入或者退出TouchMode。两者区别在于View是否应该获取焦点
- 当这一切都做完以后,TouchMode状态就发生了变化。则此时,事件进行下一步转发。
NativePostImeInputStage
- NativePostImeInputStage和前面的NativePreImeInputStage一样,都是针对InputQueue,用于Native层获取事件的机制,因此只对Native Code生效,因此不再赘述,它将直接把事件再次转发。
ViewPostImeInputStage:
- 其实MotionEvent的时间处理,主要还是在这里进行的,甚至我们可以说,KeyEvent事件的实际处理也是在这里。我们先来看下它的onProcess()函数
@Override
protected int onProcess(QueuedInputEvent q) {
if (q.mEvent instanceof KeyEvent) {
//之所以说KeyEvent事件也在这里处理。是因为我们前文提到过,最终输入法不处理的异步KeyEvent事件
//将会被转发到ViewPostImeInputStage中,走到这个处理逻辑中,这个我们后面再聊
return processKeyEvent(q);
} else {
//MotionEvent
final int source = q.mEvent.getSource();
if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
//如果是鼠标操作,手指Touch操作,走这里
return processPointerEvent(q);
} else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
//如果是鼠标的轨迹球,走这里(现在已经没有这种鼠标了吧)
return processTrackballEvent(q);
} else {
//以上两者都不是没走这里
return processGenericMotionEvent(q);
}
}
}
Touch和鼠标:
private int processPointerEvent(QueuedInputEvent q) {
//去除MotionEvent
final MotionEvent event = (MotionEvent)q.mEvent;
...
//分发给ViewRootImpl的DecorView
boolean handled = mView.dispatchPointerEvent(event);
...
//DecorView是否处理,返回相应结果
return handled ? FINISH_HANDLED : FORWARD;
}
- 到这里,事件终于从一层一层的队列,转发,到达了App开发者比较熟悉的领域了。
mView:
- mView的声明对象是View类。
- 当前我们处于ViewRootImpl.java中,众所周知,ViewRootImpl就是整个App的整个View层级的根,而在App开发中,我们的根View又对应的是DecorView。
- 因此ViewRootImpl中管理的mView,其实就是DecorView对象本身。
- 关于DecorView和ViewRootImple的关系,我们后面再单独聊一聊。
- 至于目前,时间分发到了View的函数中时,基本就回到了App开发中的事件分发逻辑了。
//该函数DecorView没有覆写,所以直接在View.java中
public final boolean dispatchPointerEvent(MotionEvent event) {
if (event.isTouchEvent()) {
//如果是Touch事件,比如鼠标的移动、hover这些不算是Touch
//按下操作才算是touch
//此时,如果是Touch事件,回调DecorView的dispatchTouchEvent()函数,它被DecorView覆写了
return dispatchTouchEvent(event);
} else {
return dispatchGenericMotionEvent(event);
}
}
- MotionEvent事件,被传到了DecorView的dispatchPointerEvent()函数中:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
//这里,会去或者DecorView所在PhoneWindow的Callback对象
final Window.Callback cb = mWindow.getCallback();
//如果callback不为空,窗口没销毁,就回调CallBack
//否则就直接回调ViewGroup中的dispatchTouchEvent()函数
return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
}
- 增加这个窗口回调的作用是,允许客户端从窗口层面上,将事件进行拦截。
- 接下来的步骤,就是回调Android的View事件分发流程了。我们在下一篇文章中在详细地去聊一聊这一部分。现在我们还有KeyEvent事件还没有完全分析完。
键盘KeyEvent:
- 在ViewPostImeInputStage的onProcess()中,KeyEvent事件被单独转发给了另一个函数处理:
private int processKeyEvent(QueuedInputEvent q) {
//获取KeyEvent事件对象
final KeyEvent event = (KeyEvent)q.mEvent;
// Deliver the key to the view hierarchy.
//将KeyEvent转发给ViewRootImpl的DecorView
if (mView.dispatchKeyEvent(event)) {
//如果事件被处理了,返回相应结果
return FINISH_HANDLED;
}
...
//后续是对一些修饰键,事件丢弃的判断,暂时就不展开讲了
}
- 再来看DecorView的dispatchKeyEvent()函数:
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
final int keyCode = event.getKeyCode();
final int action = event.getAction();
final boolean isDown = action == KeyEvent.ACTION_DOWN;
...
//前面主要是修饰键、快捷键的一些逻辑
if (!mWindow.isDestroyed()) {
//如果Window的Callback存在,则有限Callback。
//这里和MotionEvent事件是一样的,也提供了Callback允许客户端自己拦截
final Window.Callback cb = mWindow.getCallback();
final boolean handled = cb != null && mFeatureId < 0 ? cb.dispatchKeyEvent(event)
: super.dispatchKeyEvent(event);
if (handled) {
return true;
}
}
//如果Window的Callback和View都没有处理KeyEvent事件,才将事件传给PhoneWindow自己处理
return isDown ? mWindow.onKeyDown(mFeatureId, event.getKeyCode(), event)
: mWindow.onKeyUp(mFeatureId, event.getKeyCode(), event);
}
总结:
- 整篇文章,我们将触摸事件和KeyEvent事件最终传递到View中进行事件分发的过程细化了一遍。发现其实在回调到View之前,Android还是为我们做了很多的事情的。这一块还是比较有利于我们了解Android的设计原理,有兴趣的同学可以自己Debug走一走,加深理解。