目录
- 正文
- 1. InputMapper 处理触摸事件
- 2. 收集触摸事件信息
- 3. 处理同步事件
- 结束
正文
手机一般有两种类型的输入设备。一种是键盘类型的输入设备,通常它包含电源键和音量下键。另一种是触摸类型的输入设备,触摸屏就属于这种类型。
键盘类型的输入设备一般都是产生按键事件,前面已经用几篇文章,分析了按键事件的分发流程。
触摸类型的输入设备一般都是产生触摸事件,本文就开始分析触摸事件的分发流程。
1. InputMapper 处理触摸事件
由 Input系统: InputReader 处理按键事件 可知,InputReader 从 EventHub 获取到事件后,最终把事件交给 InputMapper 进行处理。
InputMapperKeyboardInputMapperTouchInputMapperSingleTouchInputMapperMultiTouchInputMapper
对于键盘类型的输入设备,它的按键事件由 KeyboardInputManager 处理。对于触摸类型的输入设备,如果设备支持多点触摸,它的触摸事件由 MultiTouchInputMapper 处理,而如果只支持单点触摸,它的触摸事件由 SingleTouchInputMapper 处理。
通常,手机上的触摸屏都是支持多点触摸的,那么就看看 MultiTouchInputMapper 处理触摸事件的流程
为了方便大家理解这里的处理过程,我展示一段在触摸屏上滑动手指所产生的触摸事件序列
对于每一次的触摸事件,例如手指按下或者移动,驱动会先上报它的信息事件,例如 x, y 坐标事件,再加上一个同步事件(SYN_REPORT)。
那么,MultiTouchInputMapper 处理触摸事件的过程就很好理解了,如下
- 使用累加器 MultiTouchMotionAccumulator 收集触摸事件的信息。参考【2. 收集触摸事件信息】
- 调用父类 TouchInputMapper::process() 处理同步事件。参考 【3. 处理同步事件】
2. 收集触摸事件信息
在分析累加器收集触摸事件信息之前,首先得理解多点触摸协议,也就是 A / B 协议。B 协议也叫 slot 协议,下面简单介绍下这个协议。
当第一个手指按下时,会有如下事件序列
事件 ABS_MT_SLOT,表明触摸信息事件,是由哪个槽(slot)进行上报的。一个手指产生的触摸事件,只能由同一个槽进行上报。
事件 ABS_MT_TRACKING_ID ,表示手指ID。手指 ID 才能唯一代表一个手指,槽的 ID 并不能代表一个手指。因为假如一个手指抬起,另外一个手指按下,这两个手指的事件可能由同一个槽进行上报,但是手指 ID 肯定是不一样的。
事件 ABS_MT_POSITION_X 和 ABS_MT_POSITION_Y 表示触摸点的 x, y 坐标值。
事件 SYN_REPORT 是同步事件,它表示系统需要同步并处理之前的事件。
当第一个手指移动时,会有如下事件
此时没有指定 ABS_MT_SLOT 事件和 ABS_MT_TRACKING_ID 事件,默认使用前面的值,因为此时只有一个手指。
当第二个手指按下时,会有如下事件
很简单,第二个手指的事件,由另外一个槽进行上报。
当两个手指同时移动时,会有如下事件
通过指定槽,就可以清晰看到事件由哪个槽进行上报,从而就可以区分出两个手指产生的事件。
当其中一个手指抬起时,会有如下事件
当一个手指抬起时,ABS_MT_TRACKING_ID 事件的值为 -1,也就是十六进制的 ffffffff。通过槽事件,可以知道是第一个手指抬起了。
如果最后一个手指也抬起了,会有如下事件
通过 ABS_MT_TRACKING_ID 事件可知,手指是抬起了,但是哪个手指抬起了呢?由于抬起的是最后一个手指,因此省略了槽事件。
现在已经了解了 slot 协议,现在让我来看看累加器 MultiTouchMotionAccumulator 是如何收集这个协议上报的数据的
收集 slot 协议上报的数据的过程如下
- 首先根据 ABS_MT_SLOT 事件,获取数组索引。如果上报的数据中没有指定 ABS_MT_SLOT 事件,那么默认用最近一次的 ABS_MT_SLOT 事件的值。
- 根据索引,从数组 mSlots 获取 Slot 元素,并填充数据。
很简单,就是用 Slot 数组的不同元素,收集不同手指所产生的事件信息。
3. 处理同步事件
根据前面的分析可知,驱动每次上报完触摸事件信息后,都会伴随着一个同步事件。刚才已经收集了触摸事件的信息,现在来看下如何处理同步事件
处理同步事件的过程如下
- 调用 syncTouch() 把累加器收集到数据,同步到 mRawStatesPending 最后一个元素中。syncTouch() 由子类实现。参考【3.1 同步数据】
- 处理同步过来的数据。同步过来的数据,基本上还是元数据,因此需要对它加工,最终要生成高级事件,并分发出去。参考【3.2 处理同步后的数据】
3.1 同步数据
累加器收集的数据是由驱动直接上报的元数据,这里把元数据同步到 RawState::rawPointerData,它的类型为 RawPointerData ,结构体定义如下
介绍下 RawPointerData 的几个成员变量,就可以知道同步后的数据有哪些了
- uint32_t pointerCount : 保存触摸的手指数量。
- BitSet32 touchingIdBits : 保存所有手指的ID。
- Pointer pointers[MAX_POINTERS] : 保存所有手指的触摸事件的元数据。
- uint32_t idToIndex[MAX_POINTER_ID + 1] : 保存手指 ID 到 index 的映射。这个 index 就是数组 pointers 的索引。
在这里,我要强调几点事
- 只有手指 ID 才能唯一代表一个手指。
- index 只能作为数据的索引,来获取手指的触摸事件信息。
- 如果你知道了手指ID,那么就可以通过 idToIndex 获取索引,然后根据索引获取手指对应的触摸事件信息。
我曾经写了一篇文章 多手指触控,其实也不是很难 ,这篇文章中强调了,在多手指触摸的情况下,只有手指 ID 能唯一代表一个手指,如果想获取某一个手指的触摸事件,那么必须先将 ID 转化为 index,然后使用这个 index 从数组中获取触摸事件的数据。现在,你懂了吗?
3.2 处理同步后的数据
现在数据已经同步到 mRawStatesPending 最后一个元素中,但是这些数据基本上是元数据,是比较晦涩的,接下来看看如何处理这些数据
开始处理元数据之前,首先使用 mCurrentRawState 复制了当前正在处理的数据,后面会使用它进行前后两次的数据对比,生成高级事件,例如 DOWN, MOVE, UP 事件。
然后调用 cookAndDispatch() 对数据进行加工和分发
加工和分发事件的过程如下
- 使用 cookPointerData() 进行加工事件。加工什么呢?例如,由于手指是在输入设备上触摸的,因此需要把输入设备的坐标转换为显示屏的坐标,这样窗口就能接收到正确的坐标事件。参考【3.2.1 加工数据】
- 使用 dispatchTouches() 进行分发事件。底层上报的数据毕竟晦涩难懂,因此需要包装成 DOWN/MOVE/UP 事件进行分发。参考【3.2.2 分发事件】
3.2.1 加工数据
加工的元数据保存到了 CookedState::cookedPointerData 中,它的类型为 CookedPointerData ,结构体定义如下
一看就明白了什么意思把,就不过多介绍了。
对于手机来的触摸屏来说,触摸事件的加工,最主要的就是把触摸屏的坐标点转换为显示屏的坐标点,如下
这是一道初中的坐标系转换的数学题目,我就不献丑去细致分析了,主要过程如下
- 首先根据坐标轴的缩放比例 mXScale 和 mYScale,计算触摸屏的坐标点在显示屏的坐标系中的x, y轴的缩放值。
- 根据显示屏 x, y 轴的偏移量,以及旋转角度,最终计算出显示屏上的坐标点。
3.2.2 分发事件
元数据已经加工完成,现在是时候来分发了
void TouchInputMapper::dispatchTouches(nsecs_t when, nsecs_t readTime, uint32_t policyFlags) {
BitSet32 currentIdBits = mCurrentCookedState.cookedPointerData.touchingIdBits;
BitSet32 lastIdBits = mLastCookedState.cookedPointerData.touchingIdBits;
int32_t metaState = getContext()->getGlobalMetaState();
int32_t buttonState = mCurrentCookedState.buttonState;
if (currentIdBits == lastIdBits) {
if (!currentIdBits.isEmpty()) {
dispatchMotion(when, readTime, policyFlags, mSource, AMOTION_EVENT_ACTION_MOVE, 0, 0,
metaState, buttonState, AMOTION_EVENT_EDGE_FLAG_NONE,
mCurrentCookedState.cookedPointerData.pointerProperties,
mCurrentCookedState.cookedPointerData.pointerCoords,
mCurrentCookedState.cookedPointerData.idToIndex, currentIdBits, -1,
mOrientedXPrecision, mOrientedYPrecision, mDownTime);
}
} else {
BitSet32 upIdBits(lastIdBits.value & ~currentIdBits.value);
BitSet32 downIdBits(currentIdBits.value & ~lastIdBits.value);
BitSet32 moveIdBits(lastIdBits.value & currentIdBits.value);
BitSet32 dispatchedIdBits(lastIdBits.value);
bool moveNeeded =
updateMovedPointers(mCurrentCookedState.cookedPointerData.pointerProperties,
mCurrentCookedState.cookedPointerData.pointerCoords,
mCurrentCookedState.cookedPointerData.idToIndex,
mLastCookedState.cookedPointerData.pointerProperties,
mLastCookedState.cookedPointerData.pointerCoords,
mLastCookedState.cookedPointerData.idToIndex, moveIdBits);
if (buttonState != mLastCookedState.buttonState) {
moveNeeded = true;
}
while (!upIdBits.isEmpty()) {
uint32_t upId = upIdBits.clearFirstMarkedBit();
bool isCanceled = mCurrentCookedState.cookedPointerData.canceledIdBits.hasBit(upId);
if (isCanceled) {
ALOGI("Canceling pointer %d for the palm event was detected.", upId);
}
dispatchMotion(when, readTime, policyFlags, mSource, AMOTION_EVENT_ACTION_POINTER_UP, 0,
isCanceled ? AMOTION_EVENT_FLAG_CANCELED : 0, metaState, buttonState, 0,
mLastCookedState.cookedPointerData.pointerProperties,
mLastCookedState.cookedPointerData.pointerCoords,
mLastCookedState.cookedPointerData.idToIndex, dispatchedIdBits, upId,
mOrientedXPrecision, mOrientedYPrecision, mDownTime);
dispatchedIdBits.clearBit(upId);
mCurrentCookedState.cookedPointerData.canceledIdBits.clearBit(upId);
}
if (moveNeeded && !moveIdBits.isEmpty()) {
ALOG_ASSERT(moveIdBits.value == dispatchedIdBits.value);
dispatchMotion(when, readTime, policyFlags, mSource, AMOTION_EVENT_ACTION_MOVE, 0, 0,
metaState, buttonState, 0,
mCurrentCookedState.cookedPointerData.pointerProperties,
mCurrentCookedState.cookedPointerData.pointerCoords,
mCurrentCookedState.cookedPointerData.idToIndex, dispatchedIdBits, -1,
mOrientedXPrecision, mOrientedYPrecision, mDownTime);
}
while (!downIdBits.isEmpty()) {
uint32_t downId = downIdBits.clearFirstMarkedBit();
dispatchedIdBits.markBit(downId);
if (dispatchedIdBits.count() == 1) {
mDownTime = when;
}
dispatchMotion(when, readTime, policyFlags, mSource, AMOTION_EVENT_ACTION_POINTER_DOWN,
0, 0, metaState, buttonState, 0,
mCurrentCookedState.cookedPointerData.pointerProperties,
mCurrentCookedState.cookedPointerData.pointerCoords,
mCurrentCookedState.cookedPointerData.idToIndex, dispatchedIdBits,
downId, mOrientedXPrecision, mOrientedYPrecision, mDownTime);
}
}
}
分发事件的过程,其实就是对比前后两次的数据,生成高级事件 AMOTION_EVENT_ACTION_POINTER_DOWN, AMOTION_EVENT_ACTION_MOVE, AMOTION_EVENT_ACTION_POINTER_UP,然后调用 dispatchMotion() 分发这些高级事件。
void TouchInputMapper::dispatchMotion(nsecs_t when, nsecs_t readTime, uint32_t policyFlags,
uint32_t source, int32_t action, int32_t actionButton,
int32_t flags, int32_t metaState, int32_t buttonState,
int32_t edgeFlags, const PointerProperties* properties,
const PointerCoords* coords, const uint32_t* idToIndex,
BitSet32 idBits, int32_t changedId, float xPrecision,
float yPrecision, nsecs_t downTime) {
PointerCoords pointerCoords[MAX_POINTERS];
PointerProperties pointerProperties[MAX_POINTERS];
uint32_t pointerCount = 0;
while (!idBits.isEmpty()) {
uint32_t id = idBits.clearFirstMarkedBit();
uint32_t index = idToIndex[id];
pointerProperties[pointerCount].copyFrom(properties[index]);
pointerCoords[pointerCount].copyFrom(coords[index]);
if (changedId >= 0 && id == uint32_t(changedId)) {
action |= pointerCount << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT;
}
pointerCount += 1;
}
ALOG_ASSERT(pointerCount != 0);
if (changedId >= 0 && pointerCount == 1) {
if (action == AMOTION_EVENT_ACTION_POINTER_DOWN) {
action = AMOTION_EVENT_ACTION_DOWN;
} else if (action == AMOTION_EVENT_ACTION_POINTER_UP) {
if ((flags & AMOTION_EVENT_FLAG_CANCELED) != 0) {
action = AMOTION_EVENT_ACTION_CANCEL;
} else {
action = AMOTION_EVENT_ACTION_UP;
}
} else {
ALOG_ASSERT(false);
}
}
float xCursorPosition = AMOTION_EVENT_INVALID_CURSOR_POSITION;
float yCursorPosition = AMOTION_EVENT_INVALID_CURSOR_POSITION;
if (mDeviceMode == DeviceMode::POINTER) {
auto [x, y] = getMouseCursorPosition();
xCursorPosition = x;
yCursorPosition = y;
}
const int32_t displayId = getAssociatedDisplayId().value_or(ADISPLAY_ID_NONE);
const int32_t deviceId = getDeviceId();
std::vector<TouchVideoFrame> frames = getDeviceContext().getVideoFrames();
std::for_each(frames.begin(), frames.end(),
[this](TouchVideoFrame& frame) { frame.rotate(this->mSurfaceOrientation); });
NotifyMotionArgs args(getContext()->getNextId(), when, readTime, deviceId, source, displayId,
policyFlags, action, actionButton, flags, metaState, buttonState,
MotionClassification::NONE, edgeFlags, pointerCount, pointerProperties,
pointerCoords, xPrecision, yPrecision, xCursorPosition, yCursorPosition,
downTime, std::move(frames));
getListener()->notifyMotion(&args);
}
可以看到,数据最终被包装成 NotifyMotionArgs 分发到下一环 InputClassifier。
但是,在这之前,还对 action 做了如下处理
- 为 action 添加一个 index。由于 index 是元数据数组的索引,因此 action 也就是绑定了触摸事件的数据。
- 如果是第一个手指按下,把 AMOTION_EVENT_ACTION_POINTER_DOWN 转换为 AMOTION_EVENT_ACTION_DOWN。
- 如果是最后一个手指抬起,把 AMOTION_EVENT_ACTION_POINTER_UP 转换成 AMOTION_EVENT_ACTION_UP。
第2点和第3点,在自定义 View 中处理多手指事件时,是不是很熟悉。
结束
闭上眼睛,想想 InputReader 如何处理触摸事件的。其实就是通过 InputMapper 把触摸屏的坐标点转换为显示屏的坐标点,然后对比前后两次的数据,生成高级事件,然后分发给下一环。so easy !
看我文章的人,是不是大部分是上层的人,前面两篇文章正好是上层应用类型的文章,所以得到大量的点赞反馈。但是须知,经济基础才能决定上层建筑,只有掌握了基础,才能以不变应万变。
关于触摸事件,我也会打算写一篇手势导航的文章,也就是我们经常使用的通过手势进行返回,通过手势回到桌面,这一定是大家最想看到的东西,更多关于InputReader处理触摸事件的资料请关注编程学习网其它相关文章!