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走一走,加深理解。

 评论