Android鼠标源码研究(五)--输入事件处理_java

 

本文摘要:

1,CursorInputMapper.sync内容

 

不要考虑你是什么人应该做什么, 而是你选择怎么做,才决定了你是什么样的人 --By fanfan

1概要

接上文,继续分析来看当获取到鼠标事件后,InputReader的处理.也就是CursorInputMapper.sync所处理的内容.

代码还挺多,大致看下,sync做了三件事儿

  • event信息获取:上文说event有三大类,所以信息获取是指按键相关(WhichKey哪一个按键,StateOfKey按键状态),滚轮滚动WheelScroll数据,鼠标移动move数据

  • event数据调整:按键相关的数据没什么需要调整的,但是move和scroll的数据需要根据speed来调整

  • event重新组装交给dispatcher:虚拟出inputevent事件,交给InputDispathcer,之后会分发给应用处理

但是如果是针对鼠标的话,就会又多了一个事件,因为鼠标的光标的移动和绘制不是应用控制的.

所以,也就是说:鼠标的move和button时图标的绘制需要在framework进行一个处理.

针对鼠标来说,本文需要分析的就是四个问题了.

但其实,归根结底,就是两个问题,一个是event的调整,一个是event的处理,包括交给dispatcher分发和控制pointer移动

2event调整

这个必须要结合上一文中的process方法来看了.event信息获取包括三个

  • 按键key

  • 鼠标移动move

  • 滚轮滚动scroll

 

01

第一,key按键相关

WhichKey哪一个按键

针对鼠标所考虑到的按键有三个:left/right/middle.根据buttonState来获取到是哪个按键

1int32_t currentButtonState = mCursorButtonAccumulator.getButtonState();

在确定是哪个按键之后,接下来就是判断按键是不是鼠标按键了

btnState
1bool down = isPointerDown(currentButtonState);

isPointerDown方法也很简单,会过滤三个值:left/right/middle.

当且仅当buttonState符合其中一个时,才代表是按键按下.

也就是说,只要buttonState符合其中一个值,就代表是down.

那如何实现按键up呢?这也是涉及到一个虚拟输入事件的问题了.

如果你想实现一个按键的down和up事件,那么在down时要传入对应key值代表哪一个按键按下

在Up时要传入0代表没有按键按下也就是up了.

目前为止,可以确定按键的down和up了.

代码中除了down和up,还有一组button相关的状态:press和released.

这个和down/up有什么区别呢?看下press/release是怎么定义的?

1//如果当前
2int32_t buttonsPressed = currentButtonState & ~lastButtonState;
3int32_t buttonsReleased = lastButtonState & ~currentButtonState;

从代码可以总结出press的含义:只有当前的btnState和上一次的btnState不同时,才表示press.

也就是btn的state发生了改变,并且是由false->true的改变

也就是说press和按下后的第一个down事件是一致的.

但如果是长按,那么down一直是true,而press却只有在第一个down时才为true.

同理,release的含义以及和up的区别就不再介绍.

02

第二,move移动

鼠标的移动距离对应到手机平面就是x,y方向上的偏移relativeX,relativeY.这个值跟什么有关系?

首先输入设备会输入一组偏移量的值getRelative,那么如何和屏幕上移动的距离对应呢?就需要按照一定的比例移动.

1float deltaX = mCursorMotionAccumulator.getRelativeX() * mXScale;
2float deltaY = mCursorMotionAccumulator.getRelativeY() * mYScale;

这里的mXscale|mYscale就是对应的比例.什么意思?

scale是说鼠标设备移动1尺寸,对应到屏幕上光标要移动的距离,也就是相当于地图上的图例,地图一公里相当于实际多少公里.

到此,就可以得到屏幕上的光标需要移动的距离参数

但是,仅仅到这儿还不够,我们所使用的手机基本上都是长方形状.而目前这组偏移量的值的x,y是针对鼠标来说的.

对鼠标来说,x方向偏移量就是鼠标水平移动,y方向偏移量就是鼠标垂直移动.

可对于手机来说怎么处理?也就是说如何把鼠标设备上的x|y方向上的移动,映射到手机的x|y方向上?这个要看手机是横屏还是竖屏了

 1static void rotateDelta(int32_t orientation, float* deltaX, float* deltaY) {
2    float temp;
3    switch (orientation) {
4    case DISPLAY_ORIENTATION_90:
5        temp = *deltaX;
6        *deltaX = *deltaY;
7        *deltaY = -temp;
8        break;
9
10    case DISPLAY_ORIENTATION_180:
11        *deltaX = -*deltaX;
12        *deltaY = -*deltaY;
13        break;
14
15    case DISPLAY_ORIENTATION_270:
16        temp = *deltaX;
17        *deltaX = -*deltaY;
18        *deltaY = temp;
19        break;
20    }
21}

这个也很好理解,rotateDelta代码也很简单.是根据手机屏幕方向来调整对应的偏移量.

如果竖屏0度(手机坐标远点开始于左上角,向右为x正轴,向下为y正轴),也就是默认情况下,手机的坐标轴与鼠标设备坐标轴对应

如果是竖屏180度,也就相当于坐标轴顺时针旋转180度,那么此时手机的y的负半轴对应鼠标y的正半轴,手机x的负半轴对应鼠标x的负半轴.

所以手机x轴偏移量等于负的鼠标设备x轴偏移量.手机y轴偏移量则是负的鼠标设备y轴的偏移量

如果横屏90度,也就相当于坐标轴顺时针旋转90度,那么此时手机的y的负半轴对应鼠标x正半轴,手机的x正半轴对应鼠标y正半轴.

所以手机x轴偏移量等于鼠标设备y轴偏移量.而手机y轴偏移量则是负的鼠标设备x轴的偏移量

如果横屏270度,相当于坐标轴顺时针旋转270度,那么此时手机的y正半轴对应鼠标x正半轴方向,手机的x负半轴轴对应鼠标y正半轴.

所以手机x轴偏移量等于负的鼠标设备y轴偏移量,而手机y轴偏移量则等于鼠标设备x轴的偏移量.

这么多字,都快晕了,直接看图吧.

Android鼠标源码研究(五)--输入事件处理_java_02

到这一步,手机和鼠标坐标轴方向上的偏移量已经一一对应了.接下来其实完全可以在手机上移动光标了.

但是代码还做了一层处理,算上了光标的移动速度,滚轮滚动时同样会计算速度

所以分析完scroll之后,再来分析是如何获取speed以及如何影响偏移量的

03
第三,scroll滚动

scroll的方向有两个,一个是竖直scroll一个是水平scroll.这个跟手机的坐标轴方向没关系

1float vscroll = mCursorScrollAccumulator.getRelativeVWheel();
2float hscroll = mCursorScrollAccumulator.getRelativeHWheel();

某一个event中,vscroll和hscroll是不会同时存在的.

上一篇文章也看到过,scroll时值是固定的.接下来就是针对speed的处理了.

手机的move和scroll的距离,除了直接从鼠标的偏移距离映射过来之外,还和一个speed有关系,也就是说关系如下

手机的距离=鼠标距离*scale比例*speed速度

那这就奇怪了,地图距离*图例不就是实际距离吗?为什么还有speed?其实相当于,不同的速度对应不同的比例,归根结底手机距离=鼠标距离*scale比例接下来就是一个关键难点:speed的计算
1mWheelYVelocityControl.move(when, NULL, &vscroll);
2mWheelXVelocityControl.move(when, &hscroll, NULL);
3mPointerVelocityControl.move(when, &deltaX, &deltaY);

speed是通过VelocityControl.move进行计算.这个目前涉及到矩阵算法问题,感兴趣的可以看下,具体涉及到VelocityControl和VelocityTracher.

到目前为止,event信息的获取基本已经转换成了与手机对应的值,接下来就是处理了

3event的处理

鼠标的绘制和移动是由系统处理的,不需要分发给应用控制.

所以移动鼠标时,一个是需要完成光标的移动,一个是需要分发给应用.

01

先来看光标的移动

1if (moved) {
2        mPointerController->move(deltaX, deltaY);
3}

这么简单的咧,直接交给pointerControll去move,数据也不再进行处理.也不能这么说,还是有个min和max的限制的.

ok,暂时遗留第二问,从这儿之后的鼠标move就是和绘制相关了.

02

02

除了需要单独绘制鼠标,还需要构造motionEvent事件交给dispatcher.

如何构造?event事件类型有很多,先拿一个举例

 1  // Send scroll events.
2  if (scrolled) {
3     pointerCoords.setAxisValue(AMOTION_EVENT_AXIS_VSCROLL, vscroll);
4     pointerCoords.setAxisValue(AMOTION_EVENT_AXIS_HSCROLL, hscroll);
5
6     NotifyMotionArgs scrollArgs(when, getDeviceId(), mSource, policyFlags,
7            AMOTION_EVENT_ACTION_SCROLL, 0, 0, metaState, currentButtonState,
8            AMOTION_EVENT_EDGE_FLAG_NONE,
9            displayId, /* deviceTimestamp */ 0, 1, &pointerProperties, &pointerCoords,
10            mXPrecision, mYPrecision, downTime);
11     getListener()->notifyMotion(&scrollArgs);
12}

第一步,构造NotifyMotionArgs参数

虽然主要是分析代码流程,但是看到这么多不认识的参数还是忍不住想要分析一下.

NotifyMotionArgs这家伙一下子一二十个参数,到底要干啥,我也真心奇怪用得了那么多吗?有那么多输入事件需要区分吗?难道必须要这么多参数才能将不同的事件区分开来吗?

直接拿构造方法看一下都是什么东西

1NotifyMotionArgs::NotifyMotionArgs(nsecs_t eventTime, int32_t deviceId, uint32_t source,
2       uint32_t policyFlags,
3        int32_t action, int32_t actionButton, int32_t flags, int32_t metaState,
4        int32_t buttonState, int32_t edgeFlags, int32_t displayId, uint32_t pointerCount,
5        const PointerProperties* pointerProperties, const PointerCoords* pointerCoords,
6        float xPrecision, float yPrecision, nsecs_t downTime)
7.....
8}

如果你觉得看c++代码不太方便,可以直接去MotionEvet.java文件,查看这些参数说明

  • eventTime:ns级别,指示本次event发生的时间戳

  • deviceId:输入设备的uid唯一标识,用于识别是哪个设备

  • source:表明设备类型,究竟是属于鼠标类还是键盘类等

  • policyFlags:这个参数取值和WindowManagerPolicy中的flag对应,比如flag_wake,表示event来临时唤醒屏幕

  • action:事件的action,准确来说是当前event对应的动作

  • actionButton:暂时未知

  • flags:暂时未知

  • metaState:key相关的状态,这个一般都默认为0,只有keyBoard类型的输入设备,会记录key的状态

  • buttonState:拿鼠标来说,可以指明按下的是left/right/middle哪一个按键

  • edgeFlags:这个边界标识,具体何用暂时不明.但看了下取值有五个,具体取值见下文,想起来以前工程测试有接触过,测试手机屏幕四周的触摸事件,验证手机屏幕边界是否响应正常

  • displayId:跟输出设备有关,什么意思?显示屏的id,也就是显示屏有可能不一致,比如手机连接显示屏,此时有两个display

  • pointerCount:event中坐标点的数量

  • pointerProperties:坐标点的属性,包含pointer的唯一表示id

  • pointerCoords:坐标点的值,拿鼠标来说,会有当前坐标点,以及索要偏移的偏移量的大小

  • xPrecision/yPrecision:坐标轴上的精度,用处暂时不明

  • downTime:记录上一次down事件的时间戳

edgeflags的取值有五个:分别是

屏幕上方边界事件AMOTION_EVENT_EDGE_FLAG_TOP(0x01),屏幕下方边界事件,屏幕左方和屏幕右方以及不限制边界.

值的定义在input.h文件中,目前猜测用于工程测试,测试手机屏幕的性能,也就是测试手机四周是否能够正常响应输入事件.

第二步,notifyMotion通知listener将event插入队列

这个getListener是什么?

是一个QueuedInputListener.在构造reader对象时传入的InputDispatcher.并且把inputDispatcher作为一个innerListener交给了这个QueuedInputListener.

所以,参数构造完毕后会触发listener的notifyMotion.

1void QueuedInputListener::notifyMotion(const NotifyMotionArgs* args) {
2    mArgsQueue.push(new NotifyMotionArgs(*args));
3}

也很简单,就是向队列中push一个数据.到此,InputMapper的处理就完成了

接着回到loopOnce方法中.会触发listener的flush方法.该方法会将队列中的数据notify给InputDispatcher.

这里涉及到两个listener(QueueInputListener和InputDispatcher),简单介绍一下

首先,两个listener的来源:dispatcher是在InputManager中构造reader时传入的dispatcher对象.

而queue是属于reader的一个成员,queue中会嵌套一个InputListener,也就是会把dipatcher交给queue来支配.

两个listenenr作用:当reader的event发生改变时,queueListener会把event存入队列并且通知给dispatcher,之后dispatcher就负责分发了

也就是说reader和dispatcher之间还有一个纽带,那就是queueListener,至于为何这么设计?

猜测应该是因为reader负责读取events并交给对应listener的队列存储.   dispatcher负责分发events

如果没有queueListener存在,那么相当于用于存储event的队列和用于分发event的队列是同一个

既然是同一个队列操作,那么一定是需要线程同步和线程安全的

不论是reader线程还是dispatcher线程,都是需要loop并且处于阻塞的,reader和dispatcher两者之间的操作会相互影响

相比较来看,现在的这种设计模式就避免了这种尴尬:reader读取events,之后交给queueListener就不再过问,继续loop自己的线程.

queue也会去通知dispatcher去刷新存储队列,dispatcher会异步分发事件.

以上是我自己的揣测,至于google大佬心里怎么想的,以及为什么这么设计,估计肯定是不会这么简单的了.

不废话了,接着看吧

紧接着就是dispatcher的处理了.在notifyMotion中会构造motionEvent事件,并插入到队列之中,等待分发

 1 void InputDispatcher::notifyMotion(const NotifyMotionArgs* args) {
2 ....
3
4       ...
5      //在插入队列之前先交给inputManager进行一个处理
6      mPolicy->interceptMotionBeforeQueueing(args->eventTime, /*byref*/ policyFlags);
7      ...
8 // Just enqueue a new motion event.
9 MotionEntry* newEntry = new MotionEntry(args->eventTime,
10        args->deviceId, args->source, policyFlags,
11        args->action, args->actionButton, args->flags,
12        args->metaState, args->buttonState,
13        args->edgeFlags, args->xPrecision, args->yPrecision, args->downTime,
14        args->displayId,
15        args->pointerCount, args->pointerProperties, args->pointerCoords, 0, 0);
16        needWake = enqueueInboundEventLocked(newEntry);
17...
18}
19
20//插入队列
21bool InputDispatcher::enqueueInboundEventLocked(EventEntry* entry) {
22  ...
23   mInboundQueue.enqueueAtTail(entry);
24  ...
25}

其实看到这儿,我差点儿蒙住了,没看到有事件的分发啊,只看到了event插入队列就完事儿了

还记得InputManager开启的dispatcher线程吗?开始起作用了,线程的作用就是一直从队列中取event

1void InputDispatcher::dispatchOnce() {
2  ...
3  dispatchOnceInnerLocked(&nextWakeupTime);
4  ...
5}
6
7void InputDispatcher::dispatchOnceInnerLocked(nsecs_t* nextWakeupTime) {
8 ...
9 // Inbound queue has at least one entry.
10 mPendingEvent = mInboundQueue.dequeueAtHead();
11 ...
12}

以key分发为例,继续分析代码就会发现,从dispatcher分发给IMS,而在IMS中注册了windowManagerCallBack监听(这段代码在SystemServer中,ims和wms绑定了起来)。

而最终会触发WMS中的policy对象也就是PhoneWindowManager的interceptKeyBeforeQueueing,接下来就是PWM的处理了

ok,之后就开始根据参数进行对应分发了.流程就是这么个流程,相当于基本把树干找到了,细枝末节更多的就可以针对性分析了

总结下来,本文讲述reader读取到event之后的处理以及dispatcher对于event的分发 

忙了好多天, 回头发现前段儿时间的笔记, 突然就踏实了

庆幸, 我还在写, 很乐意写......

 

原创声明

本文为作者原创文章,未经本人同意,禁止转载和挪作他用

https://mp.weixin.qq.com/s/dOiDRHr5TzhF9wv6p97W5A