安卓事件分发是个老生常谈的话题, 在ScrollView高度测量原理介绍Android View测量原理, 现在再用个实例讲解事件分发原理。
本文要搞懂2个问题。
1、 事件是怎么产生的?
2、事件是怎么传递的?
手指在屏幕上一滑, framework层的ViewRootImpl中广播监听WindowInputEventRecevier最先收到事件, 将native层传来的物理坐标转换成MotionEvent后继续传递。
事件传递过程可总结为 native层--->ViewRootImpl--->DecorView--->Activity--->PhoneWindow--->DecorView--->ViewGroup(可以理解为contentView)。 PS:记住这个顺序,很重要~~~。
dispatchTouchEvent:2604, ViewGroup (android.view)
superDispatchTouchEvent:549, DecorView (com.android.internal.policy)
superDispatchTouchEvent:1953, PhoneWindow (com.android.internal.policy)
dispatchTouchEvent:3548, Activity (android.app)
dispatchTouchEvent:21, EventActivity (com.byrcegao.myscrollviewtest.event)
dispatchTouchEvent:502, DecorView (com.android.internal.policy)
dispatchPointerEvent:12027, View (android.view)
processPointerEvent:5278, ViewRootImpl$ViewPostImeInputStage (android.view)
......
onInputEvent:7325, ViewRootImpl$WindowInputEventReceiver (android.view)
dispatchInputEvent:192, InputEventReceiver (android.view)
nativePollOnce:-1, MessageQueue (android.os)
next:379, MessageQueue (android.os)
loop:144, Looper (android.os)
main:7425, ActivityThread (android.app)
invoke:-1, Method (java.lang.reflect)
run:245, Zygote$MethodAndArgsCaller (com.android.internal.os)
main:921, ZygoteInit (com.android.internal.os)
小结:1、在Framework层的ViewRootImpl.java里产生了MotionEvent; 2、事件是从根节点传递到叶子节点(一般安卓视图是个多叉树结构)
上面是framework层的事件传递顺序,那么我们实际开发的View视图是如何传递的呢? 看个例子: 红色FrameLayout是祖父容器、绿色FrameLayout是父容器、蓝色TextView是子View(多叉树的叶子节点)。
假如手指在蓝色区域点击一下, 事件是如何传递的??? 做个比喻, 事件传递就像西游记里猪八戒问大师兄你吃不吃, 大师兄不吃则被八戒吃掉了。
View包括dispatchTouchEvent(分发)和onTouchEvent(处理)函数, ViewGroup继承于View并新增了onInteruptTouchEvent(拦截器)函数。
时序 | GrandView(ViewGroup派生类) | FatherView(ViewGroup派生类) | SonView(View派生类) |
1 | dispatchTouchEvent | ||
2 | onInteruptTouchEvent | ||
3 | dispatchTouchEvent | ||
4 | onInterptTouchEvent | ||
5 | dispatchTouchEvent | ||
6 | onTouchEvent | ||
7 | onTouchEvent | ||
8 | onTouchEvent | ||
上面是事件传递的最简单模型, 画成表格后形状就像大于号。 从日志可以看到GrandView、FatherView和SonView只收到了ACTION_DOWN, 都没收到ACTION_UP事件。
PS:DOWN是事件的起点, 如果你不消费(消费就是返回true)DOWN事件则父容器不会再传递后续的MOVE/UP/CANCEL事件给自己。 原理可以查看ViewGroup的dispatchTouchEvent函数。
下面修改一下代码, FatherView拦截Down事件(在onInteruptTouchEvent返回true), SonView还能收到事件吗? 答案是不能。
public class FatherView extends FrameLayout {
...
@Override public boolean onInteruptTouchEvent(MotionEvent ev) {
Log.d("brycegao", "FatherView dispatchTouchEvent:" + ev.toString());
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
return true; //子View不会收到任何事件
}
return onInteruptTouchEvent(ev);
}
...
}
在SonView的父容器FatherView里拦截Down事件后, SonView不会收到任何事件。
删除FatherView的拦截down逻辑, 在FatherView的onTouchEvent函数里消费down事件。 又会怎么样呢?
public class FatherView extends FrameLayout {
...
@Override public boolean dispatchTouchEvent(MotionEvent ev) {
Log.d("brycegao", "FatherView dispatchTouchEvent:" + ev.toString());
return super.dispatchTouchEvent(ev);
}
@Override public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.d("brycegao", "FatherView onInterceptTouchEvent:" + ev.toString());
return super.onInterceptTouchEvent(ev);
}
@Override public boolean onTouchEvent(MotionEvent ev) {
Log.d("brycegao", "FatherView onTouchEvent:" + ev.toString());
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
return true;
}
return super.onTouchEvent(ev);
}
}
消费down事件后当前view可以收到后续的move、up事件, 且消费事件后就不需要再继续传递了(GrandView的onTouchEvent没收到down,因为被FatherView消费了)。从日志看到SonView仍然无法收到move、up事件。
应用层的事件传递逻辑,都在ViewGroup的dispatchTouchEvent函数里。
总结:
1、View的事件传递是自根节点向叶子节点询问, 如果被某个节点消费了(onTouchEvent或onTouch回调返回true),就不再执行父容器的onTouchEvent函数。 (从根节点DecorView到叶子节点的某个View消费了事件)
2、如果拦截了Down事件(onInteruptTouchEvent函数返回true), 则子节点不会收到任何事件;
3、如果当前View消费了Down事件(onTouchEvent函数或onTouch回调返回true), 则当前View才能收到后续的Move、Up和Cancel事件;
坑: onTouch和onTouchEvent的关系???
在View.java的dispatchTouchEvent函数里处理了onTouch回调, 执行时序dispatchTouchEvent---onTouch回调---onTouchEvent, 即onTouch回调在前,onTouchEvent在后。
如果onTouch返回true,则不会执行当前视图和父容器的onTouchEvent函数。
/**
* Interface definition for a callback to be invoked when a touch event is
* dispatched to this view. The callback will be invoked before the touch
* event is given to the view.
*/
public interface OnTouchListener {
/**
* Called when a touch event is dispatched to a view. This allows listeners to
* get a chance to respond before the target view.
*
* @param v The view the touch event has been dispatched to.
* @param event The MotionEvent object containing full information about
* the event.
* @return True if the listener has consumed the event, false otherwise.
*/
boolean onTouch(View v, MotionEvent event);
}