漏学Input知识系列之“偷”走了其他窗口的事件pilferPointers
背景:
学员群里近来老是学员朋友问到一个触摸事件相关实际场景问题:
如何在全局监听到某个类型触摸事件比如NavigationBar导航手势的上滑动,不会让这个上滑动事件不影响app。
简单说就是触摸事件正常情况会传递给导航手势和顶部Activity,但是很多情况下导航手势都对Activity的触摸事件不感兴趣,但是一旦是Activity底部开始上滑这个动作,那么导航手势就感兴趣,他要开始处理整体的上滑逻辑了,自然这个上滑事件再传递给Activity就不合适了,那么系统是怎么做到的不再传递给Activity呢?
这个就是本文带大家了解的pilferPointers方法,“偷”走了其他窗口的事件。这里用了一个表达很到位的英文单词pilfer即“偷”。为啥叫做“偷”不叫“拦截”,因为拦截事件一般是指上层策略压根事件不让给你,“偷”事件则表达事件之前可能是传递给你的,本来也一直要传递给你,但是因为导航手势业务把给你事件都偷走了。
具体如下图所示:
直观理解“偷”事件
下面以App的Activity角度的触摸事件情况来理解这个“偷”事件
到Activity的dispatchTouchEvent方法中加入相关的打印查看:
正常Activity非底部上滑
Activity底部上滑触发导航手势
也就是直观上其实可以看出这个pilferPointers方法是怎么“偷”事件的。原理如下:
在导航手势还没有识别到自己上滑动作前,其实所有的触摸事件是已经正常传递给了Activity,但是一旦导航手势识别了自己有上滑动作,那么就会独占这个上滑事件,不给Activity,但是前面的DOWN事件已经传递给了Activity,为了事件完整性就需要系统给一个CANCEL事件给Activity。这里是不是额知道了一个CANCEL事件产生的场景。
通过其他日志查看“偷”事件方式:
通过查看events日志
adb logcat -b events | grep cancel
test@test:~/aosp15$ adb logcat -b events -c;adb logcat -b events | grep cancel
01-10 10:44:53.298 546 2344 I input_cancel: [59551b2 com.example.anrdemo/com.example.anrdemo.MainActivity (server),reason=input channel stole pointer stream]
01-10 10:44:53.298 546 2344 I input_cancel: [[Gesture Monitor] edge-swipe (server),reason=input channel stole pointer stream]
01-10 10:44:53.298 546 2344 I input_cancel: [[Gesture Monitor] onehanded-touch (server),reason=input channel stole pointer stream]
可以看到上面就有被偷每个窗口input_cancel这样事件产生,产生原因是input channel stole pointer stream。
也可以使用如下命令过滤stealing关键字
adb logcat | grep stealing
可以看到如下日志输出,这里看到了
01-10 10:49:03.629 546 2344 I InputDispatcher: Channel [Gesture Monitor] swipe-up (server) is stealing input gesture for device 12 from [512067 com.android.launcher3/com.android.launcher3.uioverrides.QuickstepLauncher, dca494a com.android.systemui.wallpapers.ImageWallpaper, [Gesture Monitor] edge-swipe, [Gesture Monitor] onehanded-touch]
这个日志就明显展示了,事件被 swipe-up这个Monitor独占了,把事件从其他window中偷走了。
使用pilferPointers及源码分析
以导航手势上滑为例,剖析一下使用
相关的pilferPointers调用堆栈如下
pilferPointers:59, InputMonitor (android.view)
pilferPointers:55, InputMonitorCompat (com.android.systemui.shared.system)
notifyGestureStarted:382, OtherActivityInputConsumer (com.android.quickstep.inputconsumers)
onMotionEvent:339, OtherActivityInputConsumer (com.android.quickstep.inputconsumers)
onMotionEvent:116, NavHandleLongPressInputConsumer (com.android.quickstep.inputconsumers)
onMotionEvent:138, OneHandedModeInputConsumer (com.android.quickstep.inputconsumers)
onInputEvent:937, TouchInteractionService (com.android.quickstep)
$r8$lambda$JDK2db7a_ZtzI6D1EJ5hYukunXo:-1, TouchInteractionService (com.android.quickstep)
onInputEvent:0, TouchInteractionService$$ExternalSyntheticLambda5 (com.android.quickstep)
onInputEvent:78, InputChannelCompat$InputEventReceiver$1 (com.android.systemui.shared.system)
dispatchInputEvent:295, InputEventReceiver (android.view)
nativeConsumeBatchedInputEvents:-1, InputEventReceiver (android.view)
consumeBatchedInputEvents:259, InputEventReceiver (android.view)
onBatchedInputEventPending:58, BatchedInputEventReceiver (android.view)
nativePollOnce:-1, MessageQueue (android.os)
next:346, MessageQueue (android.os)
loopOnce:189, Looper (android.os)
loop:317, Looper (android.os)
main:8705, ActivityThread (android.app)
invoke:-1, Method (java.lang.reflect)
run:580, RuntimeInit$MethodAndArgsCaller (com.android.internal.os)
main:886, ZygoteInit (com.android.internal.os)
这个全局的monitor就是swipe-up即导航手势
InputMonitor { inputChannel = [Gesture Monitor] swipe-up (client), host = android.view.IInputMonitorHost
S
t
u
b
Stub
StubProxy@f9a7d10, surface = Surface(name=[Gesture Monitor] swipe-up)/@0x1425009 }
对应的代码如下:
在满足对应上滑条件后会调用notifyGestureStarted方法
在notifyGestureStarted方法中主要是调用到了pilferPointers方法
这里使用其实很简单只需要在识别到满足条件的触摸事件后调用pilferPointers方法既可以。
下面重点分析一下pilferPointers的源码
frameworks/base/core/java/android/view/InputMonitor.java
public void pilferPointers() {
try {
mHost.pilferPointers();
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
}
这里主要掉到了 mHost的pilferPointers,这里的mHost是IInputMonitorHost类型,属于一个跨进程通讯,是在InputMonitor构造时候进行赋值的
这里的InputMonitor又是在InputManagerService中调用monitorGestureInput时候构造的
frameworks/base/services/core/java/com/android/server/input/InputManagerService.java
所以上面 mHost.pilferPointers()调用就是调用到了InputManagerService中的InputMonitorHost方法:
这里又会传递一个自己的mInputChannelToken,调用到Native层面,最后通过如下方法调用到了InputDispatcher中
具体InputDispatcher中的pilferPointers方法如下:
status_t InputDispatcher::pilferPointers(const sp<IBinder>& token) {
std::scoped_lock _l(mLock);
return pilferPointersLocked(token);
}
status_t InputDispatcher::pilferPointersLocked(const sp<IBinder>& token) {
const std::shared_ptr<Connection> requestingConnection = getConnectionLocked(token);
//根据上层传递token,找到对应的window
auto [statePtr, windowPtr, displayId] = findTouchStateWindowAndDisplayLocked(token);
ScopedSyntheticEventTracer traceContext(mTracer);
for (const DeviceId deviceId : deviceIds) {
TouchState& state = *statePtr;
TouchedWindow& window = *windowPtr;
// Send cancel events to all the input channels we're stealing from.
//准备好cancel事件
CancelationOptions options(CancelationOptions::Mode::CANCEL_POINTER_EVENTS,
"input channel stole pointer stream", traceContext.getTracker());
options.deviceId = deviceId;
options.displayId = displayId;
std::vector<PointerProperties> pointers = window.getTouchingPointers(deviceId);
std::bitset<MAX_POINTER_ID + 1> pointerIds = getPointerIds(pointers);
options.pointerIds = pointerIds;
std::string canceledWindows;
for (const TouchedWindow& w : state.windows) {
//除了token对应window,其他window都要收到cancel
if (w.windowHandle->getToken() != token) {
synthesizeCancelationEventsForWindowLocked(w.windowHandle, options);
canceledWindows += canceledWindows.empty() ? "[" : ", ";
canceledWindows += w.windowHandle->getName();
}
}
canceledWindows += canceledWindows.empty() ? "[]" : "]";
//这个就是打印
LOG(INFO) << "Channel " << requestingConnection->getInputChannelName()
<< " is stealing input gesture for device " << deviceId << " from "
<< canceledWindows;
// Prevent the gesture from being sent to any other windows.
// This only blocks relevant pointers to be sent to other windows
window.addPilferingPointers(deviceId, pointerIds);
//对state中保存的接受事件window要进行移除掉,只剩下token对应window,那样事件就不会派发给cancelwindow
state.cancelPointersForWindowsExcept(deviceId, pointerIds, token);
}
return OK;
}
总结
“偷”走了其他窗口的事件,本质实现就是调用pilferPointers方法,实现让其他窗口收到CANCEL事件,同时派发的窗口列表中移除非token的window,后续事件派发时候,只派发给token对应的window。
文章来源:https://mp.weixin.qq.com/s/l_mQK1xs8d4dp7VMkl0kyw
更多framework实战干货,请关注下面“千里马学框架”