事件分发
输入事件的分发是个经典的话题。我将它分为“应用进程内”和“应用进程外”来说明。
应用进程内
概述
由主线程的looper收到事件后,逐步交由ViewRootImpl
的inputStage
处理链处理。一般情况下,会由ViewPostImeInputStage
此节点来处理:
private int processPointerEvent(QueuedInputEvent q) {
final MotionEvent event = (MotionEvent)q.mEvent;
...
boolean handled = mView.dispatchPointerEvent(event);
...
return handled ? FINISH_HANDLED : FORWARD;
}
DecorView
的dispatchTouchEvent
来处理:
public boolean dispatchTouchEvent(MotionEvent ev) {
final Window.Callback cb = mWindow.getCallback();
return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
}
Activity
, Dialog
在构造时会window.setCallback
由此可以在activity
, dialog
的子类里复写dispatchXxxEvent
:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
Activity
会先遵从Window
的意愿:
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
ViewGroup
的触摸事件分发:
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
Activity
的onTouchEvent
是:
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
View的Touch事件分发框架概述
在View的层级结构里,会从上到下,即从父View到子View依次分发触摸事件。一般按ViewGroup的行为来讲,会检测可见的子View的边界是否和触摸事件坐标相交,在相交的前提下才会继续分发。
事件分发过程主要涉及的方法包含:
分发处理相关:
- View#dispatchTouchEvent
- ViewGroup#dispathTransformedTouchEvent
- View#onTouchListner.onTouch
- View#onTouchEvent,可能继而触发clickListener.onClick等
拦截相关:
- ViewGroup#onInterceptTouchEvent
- ViewGroup#requestDisallowInterceptTouchEvent
嵌套滚动相关:
- View#startNestedScroll
- ViewGroup#onStartNestedScroll
典型的触摸事件流(即一系列要分发的触摸事件)是(*代表可选):
- ACTION_DOWN
- ACTION_MOVE / ACTION_DOWN(多点触摸,可根据pointerIndex判断触摸点编号)
- ACTION_CANCEL / ACTION_UP(若是多点触摸则会有多次)
即ACTION_DOWN
作为起始,ACTION_UP
作为结束,如果有意外发生,则会以一个ACTION_CANCEL
作为结束。之间可以出现多次ACTION_MOVE
以及子序列(除了ACTION_CANCEL
的情况)
若需要保留MotionEvent
实例,请使用MotionEvent.obtain(MotionEvent)
方法,而不要作为成员变量,因为MotionEvent
实例是被高度复用的。
分发的机制如下:
- 对于Down事件,父View按默认的行为,在
dispatchTouchEvent
中分发给可以获取焦点、可见、且触摸位于其边界内、层级更高的子View。若返回了true
则代表消费此事件,并会继续消费后续事件。若返回false
,则后续事件将不再分发至此。即,任何View必须在Down
事件决定是否接收后续事件流。 - 对于确认消费事件流的子View,被认定为事件分发目标。在它层级之上的父View在后续触摸事件中,
dispatchTouchEvent
依然会触发(因为它在Down事件返回的值就是目标子View返回的true),此外,它的onInterceptTouchEvent
也会被调用,除非分发目标View正是自己。在onInterceptTouchEvent
中若返回了true
,则代表父View拦截了后续所有事件。子View将无法收到此次事件,转而会收到ACTION_CANCEL
事件(即拦截判断发生在分发给目标View之前)。 - 子View可以通过
requestDisallowInterceptTouchEvent
来阻止父View对触摸事件拦截。当然,此调用必须发生地尽可能早。若父View已经开始拦截时此调用无意义。 - 此外,子View可以通过
startNestedScroll
表示自己进行了嵌套滚动。这更类似一种hint
机制,父View的onStartNestedScroll
会按层级得到回调。若返回true
表示接受,false
表示拒绝。若接受了,一般隐含意思是,将不再拦截。 - View可以添加
onTouchListner
和onClickListener
,以及onLongClickListener
等,View也有onTouchEvent
回调。它们的关系是,优先由onTouchListener
判断是否处理触摸事件。若不处理,则再交给View#onTouchEvent
回调,而这个回调中会包含一系列的手势检测,对于ACTION_UP
时,则会试图调用onClickListener
。它也会记录按压时长,从而在释放时根据需要调用onLongClickListener
。
滚动检测
一般会以GestureDetector
实例,在onDispatchTouchEvent
中为其传递触摸事件,并提供不同手势触发的回调实现,来完成手势检测。但有时候需要手动检测时,步骤一般如下:
- 在ACTION_DOWN中返回true,从而可以得到后续事件,并记录DOWN事件的坐标。
- 在ACTION_MOVE时,和DOWN事件坐标对比得出手指移动偏移量,和
ViewConfiguration.TOUCH_SLOP
或其他参考值对比,来确认是否可以认为发生了滚动。
嵌套滚动案例
下面来举一个嵌套滚动的场景。有一个可滚动容器A,内部包含了一个内部也可以上下滚动的view B。当View B内部上下滚动到边界时,外部容器A继续滚动。
解决此场景类似于NestedScrollView
中对onInterceptTouchEvent
的处理,即子View若判断出了是竖直滚动,若可以滚动,则调用startNestedScroll
,而父View除了复写onNestedScroll
以接受嵌套滚动外,在onInterceptTouchEvent
中的ACTION_MOVE
中需要通过getNestedScrollAxis
来检查是否有竖直方向的嵌套滚动正在进行,若有,则不应该拦截,否则,可以在它也判断出是竖直方向滚动时,返回true
拦截此事件。一旦拦截后,子View将不再收到事件。
应用进程外
这里主要讨论输入事件是如何分发到应用进程中
todo 待补充
dispatchTouchEvent调用栈
dispatchTouchEvent:53, MainActivity$TestView (org.tu.android)
dispatchTransformedTouchEvent:3131, ViewGroup (android.view)
dispatchTouchEvent:2731, ViewGroup (android.view)
dispatchTransformedTouchEvent:3131, ViewGroup (android.view)
dispatchTouchEvent:2731, ViewGroup (android.view)
superDispatchTouchEvent:528, DecorView (com.android.internal.policy)
superDispatchTouchEvent:1857, PhoneWindow (com.android.internal.policy)
dispatchTouchEvent:4105, Activity (android.app)
dispatchTouchEvent:69, WindowCallbackWrapper (androidx.appcompat.view)
dispatchTouchEvent:478, DecorView (com.android.internal.policy)
dispatchPointerEvent:13887, View (android.view)
processPointerEvent:6209, ViewRootImpl$ViewPostImeInputStage (android.view)
onProcess:5947, ViewRootImpl$ViewPostImeInputStage (android.view)
deliver:5400, ViewRootImpl$InputStage (android.view)
onDeliverToNext:5460, ViewRootImpl$InputStage (android.view)
forward:5419, ViewRootImpl$InputStage (android.view)
forward:5584, ViewRootImpl$AsyncInputStage (android.view)
apply:5427, ViewRootImpl$InputStage (android.view)
apply:5641, ViewRootImpl$AsyncInputStage (android.view)
deliver:5400, ViewRootImpl$InputStage (android.view)
onDeliverToNext:5460, ViewRootImpl$InputStage (android.view)
forward:5419, ViewRootImpl$InputStage (android.view)
apply:5427, ViewRootImpl$InputStage (android.view)
deliver:5400, ViewRootImpl$InputStage (android.view)
deliverInputEvent:8356, ViewRootImpl (android.view)
doProcessInputEvents:8325, ViewRootImpl (android.view)
enqueueInputEvent:8276, ViewRootImpl (android.view)
onInputEvent:8495, ViewRootImpl$WindowInputEventReceiver (android.view)
dispatchInputEvent:188, InputEventReceiver (android.view)
nativePollOnce:-1, MessageQueue (android.os)
next:336, MessageQueue (android.os)
loop:184, Looper (android.os)
main:7830, ActivityThread (android.app)
invoke:-1, Method (java.lang.reflect)
run:492, RuntimeInit$MethodAndArgsCaller (com.android.internal.os)
main:1040, ZygoteInit (com.android.internal.os)