android 鼠标及外接键盘按键事件分发

2,868 阅读6分钟

1、概述

在android应用中,UI有时需要响应鼠标的hover状态及scroll操作,也即鼠标的移动状态及鼠标的滚轮操作。

比如,在鼠标hover到某个控件时显示一个dialog或popupwindow来介绍功能以及操作鼠标滚轮时进行recyclerview的翻页等等。 同时,在大屏设备上,往往需要提供一个快捷键支持快捷操作,提升功能体验,如ctrl+c、ctrl+v等。

通过对鼠标及外接键盘事件的支持,能为android大屏操作设备带来一定的体验提升。

2、系统对hover事件及scroll事件的支持

Generic motion events describe joystick movements, mouse hovers, track pad touches, scroll wheel movements and other input events.

通过文档描述,generic motion events描述了如操纵杆、鼠标hover、触控板及滚动等输入事件。

android系统在activity及view层面均提供了相应的回调来分发及处理hover及scroll事件。

在view中有如下三个方法

image.png

图一 dispatchGenericMotionEvent方法

顾名思义,dispatchGenericMotionEvent方法负责分发generic motion事件,其中间接调用了 onGenericMotionEvent或 mOnGenericMotionListener.onGenericMotion 。

image.png

图二 onGenericMotionEvent方法

onGenericMotionEvent方法是具体处理motion事件的。 从on开头就可以看出,这是一个回调方法,在适当的时机,系统会调用 onGenericMotionEvent 方法来处理motion事件(如图一所述,该方法是在dispatchGenericMotionEvent方法中被调用的)。 一般对于hover或者scroll的处理逻辑都可以放在这里。 该方法注释很详尽的描述了该方法的用法,在实际开发过程中也可以不时的参考一下。

image.png

图三 onHoverEvent方法

该方法是处理hover事件的一个回调,可以方便快捷的处理hover事件。 该方法也是在dispatchGenericMotionEvent分发方法中被调用的。当事件被识别为hover事件时会调用onHoverEvent方法。

在activity层面,系统也提供了dispatchGenericMotionEvent方法与onGenericMotionEvent方法。 dispatchGenericMotionEvent参与motion event事件的从顶层分发,如果activity所包含的view树中没有消费该motion事件的,则调用本身的onGenericMotionEvent方法。

3、hover及scroll事件的分发顺序

使用一个简单demo来学习及验证事件的分发顺序,该demo界面中包含一个ViewGroup及View对象,如下图所示。

image.png

图四 demo演示

当移动鼠标时,打印的日志如下图(该日志是在activity、viewgroup、view均为消费hover事件前提下打印的)

image.png

图五 hover事件日志

通过日志可以看出,hover事件与touch事件的原理类似。从activity分发到viewgroup,再到view,在view中如果没有消费,再一级级的往上抛。

在打印日志学习过程中发现,当view接收到了hover_enter事件时,activity却还是分发的hover_move事件。这是为啥呢,于是打断点调试了一下,发现在viewgroup中的dispatchHoverEvent方法中,有如下代码片段。

image.png

图六 hover_enter事件变化逻辑

当view没有被hover过时,将hover_move事件修改为hover_enter事件。这就是activity接收到hover_move事件,子view却收到hover_enter事件的原因。

通过修改demo中viewgroup、view的onGenericMotionEvent方法的返回值,可以验证事件是由上到下,在由下到上,只有某个层级的view消费了事件,该事件才不会再继续往上返回。

4、系统对于外接键盘按键事件的支持

与鼠标事件类似,activity与view类均有dispatchKeyEvent负责分发按键事件。

与此同时,view额外提供了dispatchKeyEventPreIme方法,以便在输入法接收事件前有机会处理一些逻辑。 该方法签名如下图,其方法注释也详尽的解释了其作用。

image.png

图七 dispatchKeyEventPreIme方法

实际中,view的dispatchKeyEventPreIme方法会先于activity的dispatchKeyEvent回调。

在activity及view中同时提供了onKeyDown、onKeyUp及其相应的回调接口来处理具体的按键。 需要注意的是,这些方法的注释明确说了,软件盘的按键不一定会回调这些接口。所以软件盘的事件不要依赖这些接口。

5、应用添加快捷键遇到的问题

监听某个快捷键,然后在activity上弹出某个popwindow。但是,在开发过程中发现,当弹出了popupwindow后,activity及其包含的子view均收不到后续的事件了。这是为啥呢?

通过调试后发现,原来当popupwindow弹出后,事件均下发到popupwindow对应的窗口及子view了。

如下图是弹出popupwindow后的调用堆栈

image.png

图八 popupwindow事件分发调用栈

从日志可以看出,事件是发送到对应窗口的ViewRootImpl的,然后在由ViewRootImpl继续负责分发。 popupwindow是通过windowmanager添加到window的,其ViewRootImpl与Activity的ViewRootImpl不是同一个对象。故弹出popupwindow后,后续的事件都会分发到popupwindow所包含的view树中,而与Activity没啥关系。

如下图是直接分发到activity的调用堆栈

image.png

图九 activity事件分发调用栈

从日志可以看出,事件下发到了Activity的DecorView中,然后再层层分发各级子view。

6、基于window的事件分发

通过图八和图九可知,事件是分发到当前获取焦点的窗口上的。注意,在实际开发中要留意窗口是否有焦点(可以系统接口设置window是否获取焦点)

基于窗口的事件分发.png

7、关于焦点问题

1、事件能否分发到view,关键要有焦点。 在接收按键事件时,源码中会自动让相关的视图获取焦点。 在实际开发中,有时焦点容易被抢占,故导致接收不到相关的事件。 可以先通过如下命令

adb shell dumpsys window | findstr mFocusWindow

查看当前窗口是否是目标窗口初步判断。 如果是目标的窗口,那么事件一定会下发到该窗口,再结合断点调试,通过findFocus方法或isFocus等相关方法判断是否有焦点来确定问题原因。

在view的onwindowfocuschanged方法注释上也有相关提醒。

image.png

要想获取按键事件,view所在的窗口和本身都要获取焦点才可以。

2、在项目中遇到过焦点被抢占的问题,此时可以调用requestFocus方法,但是发现有时会失败。在stackoverflow上有回答建议通过handler或view post消息到主线程调用,实际验证可行。

8、参考

感谢以下作者的博客,讲的非常的详细,受益匪浅。

1、android按键事件分发 。

2、为什么popupwindow没有创建PhoneWindow对象?

3、android window机制

4、View.requestFocus聚焦源码分析