我们常见的触摸事件除了按下,弹起,移动之外还有很多,诸如长按,双击,Scroll,Fling等,他们是怎么判断的,还有这些长按,双击等事件的时间能否自由设置。可以在开发者选项中打开“显示点按操作反馈”和“指针位置”,同时可以打开inputflinger模块的log开关做一些调试,分析TP报点。
一般当我们需要处理触摸事件时有两种方式:
- 委托式 : 将事件委托给监听器来进行处理。即定义一个
View.onTouchListener()
子类的监听器,由其onTouch()
方法来处理。 - 回调式 : 通过重写View类自己的
onTouchEvent()
方法来处理,在执行时会回调该方法,在其中执行自定义的代码。
关于主触点,副触点:发送触屏事件的时候,除了此触屏事件所对应的触点之外,如果当前触点多于一个或者等于一个,则此事件为副触点事件,发送此事件的触点叫做副触点。否则为主触点事件,发送此事件的触点为主触点。
MotionEvent对象事件处理
在MotionEvent.java
中,ACTION动作事件定义
1 | ACTION_DOWN = 0; |
(1) 首先当点击下屏幕,触屏事件从View.java
的onTouchEvent()开始处理:
1 | ...... |
(2) 事件响应是先有按下才会有后续事件。因此先查看ACTION_DOWN
。在此case中判断如果是在scrollingContainer中则等待一段时间执行检查是否为Tap事件。因为可能按下之后可能会有scroll操作,如果有将丢弃长按检测。而如果不在container中,则立即执行长按检测。
1 | private final class CheckForTap implements Runnable { |
(3) 在其中执行了setPressed()
操作,其后执行checkForLongClick()
,即等待180ms-500ms来执行longPress操作。
1 | postDelayed(mPendingCheckForLongPress, |
在其中执行performLongClick()
。在该函数中处理长按需要做的事情,例如长按监听器中流程,显示contextMenu,处理长按震动反馈:
1 | handled = li.mOnLongClickListener.onLongClick(View.this); |
Note: 此处有两个时间数据: tapTimeout
和 longPressTimeout
。
定义在frameworks/base/core/java/android/view/ViewConfiguration.java
,时间是可以自定义的,但最好采用google提供的,这是经过大量积累得来的数据。而此处的longTimeout是设置辅助功能界面中’触摸和按住延迟’选项可设置的,如果没有设置那就是用默认的500ms。
1 | private static final int TAP_TIMEOUT = 180; |
MotionEvent底层事件获取(触控事件分发机制)
(1) 在onResume时会将view显示出来,跟踪代码到执行时会调用ActivityThread的handleResumeActivity()
。可以看到获取window的DecorView,即整个window的顶层View。
调用流程为:(创建窗口)
- WindowManager.addView();
- 在实现类WindowManagerImpl中实现addView();
- 最后一行通过root.setView();
- 在ViewRootImpl中实现setView();
- 在其中调用windowSession.add()。
- windowSession为客户端,而服务器端为
Session.java
,在Session中转而调用WindowManagerService的addWindow()来实现add方法。
(2)WindowManagerService中addWindow
这里实现了事件信息传递和交互的通道,内部采用socketpair,通过InputChannel
来实现。
Note:openInputChannelPair(), 在其中创建socketpair,一个匿名的已连接套接字,一个为发送端,一个为接收端,可以进行双工通讯(UNIX网络编程)。
获取InputChannel, 一个置为Input,一个置为output。RegisterInputChannel中调用nativeRegisterInputChannel。
(3)在WindowManagerService
中创建InputManagerService类(InputManagerService.java)对象,并start。
之后通过JNI流程在native中执行,并执行InputManager的start方法。
(4)在创建InputReader时会将dispatcher传入。即InputReader的成员变量mQueuedListener为dispatcher的执行者,具体代码分析flush函数,关注Args,例如MotionArgs, flush执行后,将调用dispatcher->notifyMotion()
;
如果只关注Motion的话,那么就是调用InputDispatcher->notifyMotion()
。
从抓取Systrace可以查看到触屏的整个事件,从InputReader开始,然后到deliverInputEvent触发APP绘制。关于报点可以重点关注inputflinger模块的log打印,会打印input的坐标。
systrace查看Input事件流程
参考: https://www.jianshu.com/p/427b084b0d77
参考: https://mp.weixin.qq.com/s/Q2k6pLEyXhHZvZOIiU5ucA
- 触摸屏每隔几毫秒(如果是60刷新率,则一秒扫描屏幕120次,大概8ms扫描一次)扫描一次,如果有触摸事件,那么把事件上报到对应的驱动。
- InputReader 读取触摸事件交给 InputDispatcher 进行事件派发。
- InputDispatcher 将触摸事件发给注册了 Input 事件的 App。
- App 拿到事件之后,进行 Input 事件分发,如果此事件分发的过程中,App 的 UI 发生了变化,那么会请求 Vsync,则进行一帧的绘制。
详细分析
所以systrace从InputReader开始:(前面还有一点很短的“binder transaction”的时间)
1 | // Dispatch pointer down events using the new pointer locations. |
然后会到InputDispatcher的dispatchMotionLocked函数,并且InputDispatcher会从InboundQueue中取出Input事件派发到各个App(连接)的OutBoundQueue(OutboundQueue区域oq)
1 | bool InputDispatcher::dispatchMotionLocked( |
然后到deliverInputEvent,说明APP UI Thread被Input事件唤醒;(起始点可以看到当前APP的Launcher是1,value=1表示有一个input事件,如果主线程卡顿没法及时处理Input事件,这里的Value会堆积)
之后则是APP的UI线程启动,然后再触发APP的绘制线程进行绘制等等。