掌握事件分发传递机制的使用场景,分析各种场景下的事件冲突(表现在点击滑动失效等),开发与扩展自定义控件的功能,同时也是面试中的重要基础知识点。
基础知识
Event
在不停的应用场景下Event可以分为KeyEvent, TouchEvent, HoverEvent。
KeyEvent:针对遥控或可以按动的按钮
事件流:
dispatchKeyEventPreIme、dispatchKeyEvent、onKeyDown、view.onKeyListener、view.onClickListener
KeyEvent.ACTION_DOWN
KeyEvent.ACTION_UP
TouchEvent:针对支持触摸的屏幕
Android中的事件主要有点按,长按,拖拽,滑动等,所有的这些事件响应都有以下几个事件捕捉作为基础
ACTION_DOWN
ACTION_MOVE
ACTION_UP
我们可以在以上三个状态下去根据具体的自定义控件的业务场景去动态的计算,解析事件
HoverEvent:针对鼠标的事件
在ViewGroup中多一个 onInterceptHoverEvent
ACTION_HOVER_ENTER 指针悬浮在view上
ACTION_HOVER_MOVE 指针在view上移动
ACTION_HOVER_EXIT 指针离开view边界
Android中触摸事件主要设计的三个方法
public boolean dispatchTouchEvent(MotionEvent event)
用于事件的分发,Android中所有事件都首先经过这个方法的分发,然后决定是自身消费当前事件还是继续往下分发给子控件。
返回true表示事件部分发,事件被消费;
返回false则继续往下分发。
public boolean onInterceptTouchEvent(MotionEvent event)
这是ViewGroup特有的方法,因为ViewGroup中可能还有子View,而在Android中View中是不能再包含子View的(iOS可以)。它的作用是负责事件的拦截
返回true表示拦截当前事件,停止往下分发,接着交给自身的onTouchEvent()进行处理。
返回false则不拦截,继续往下传。
public boolean onTouchEvent(MotionEvent event)
用于本级事件的处理
返回true表示消费处理当前事件
返回false则不处理,交给子控件进行继续分发。
事件分发传递机制
View的事件传递机制
主要涉及的方法
public boolean dispatchTouchEvent(MotionEvent event)
public boolean onTouchEvent(MotionEvent event)
试验1
1.写一个类CustomButton继承Button,在dispatchTouchEvent(),onTouchEvent()中对应的动作状态下打印日志
2.在Activity中进行如上动作
3.在Activity中设置CustomButton的onTouchListener(),onClickListener(),
并在onTouchListener()中设置对应的动作状态下打印日志
最终结果如下
I/System.out: Activity__dispatchTouchEvent__DOWN
I/System.out: CButton_dispatchTouchEvent_DOWN
I/System.out: CButton__onTouch__DOWN
I/System.out: CButton_onTouchEvent_DOWN
I/System.out: Activity__dispatchTouchEvent__UP
I/System.out: CButton_dispatchTouchEvent_UP
I/System.out: CButton__onTouch__UP
I/System.out: CButton_onTouchEvent_UP
I/System.out: CButton__onClickListener clicked!
1.可以看到事件最先到达Activity的dispatchTouchEvent()中
/**
* Called to process touch screen events. You can override this to
* intercept all touch screen events before they are dispatched to the
* window. Be sure to call this implementation for touch screen events
* that should be handled normally.
* @param ev The touch screen event.
* @return boolean Return true if this event was consumed.
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
会在dispatchTouchEven执行完之后进行onTouchEvent
2.紧接着事件到达View的dispatchTouchEvent
if (onFilterTouchEventForSecurity(event)) {
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null && (mViewFlags &
ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) {
return true;
}
if (onTouchEvent(event)) {
return true;
}
}
可以判断View的onTouch()会先于View的onTouchEvent()实践处理
3.进接着事件传递到onTouchEvent()
...
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
...
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
...
可以看到设置的点击事件在onTouchEvent()之后会被调用
ViewGroup的事件传递机制
主要涉及的方法
public boolean dispatchTouchEvent(MotionEvent event)
public boolean onTouchEvent(MotionEvent event)
public boolean onInterceptTouchEvent(MotionEvent event)
试验2(在实验1的基础上,自定义一个布局ViewGroup布局)
1.写一个类CustomButton继承Button,在dispatchTouchEvent(),onTouchEvent()中对应的动作状态下打印日志
2.自定义一个ViewGroup CLinearLayout,把它作为CButton的父容器
3.在Activity中的dispatchTouchEvent(),onTouchEvent()中对应的动作状态下打印日志
4.在Activity中设置CustomButton的onTouchListener(),onClickListener(),
并在onTouchListener()中设置对应的动作状态下打印日志
最终结果如下
I/System.out: Activity__dispatchTouchEvent__DOWN
I/System.out: CLinearLayout_dispatchTouchEvent_DOWN
I/System.out: CLinearLayout_onInterceptTouchEvent_DOWN
I/System.out: CButton_dispatchTouchEvent_DOWN
I/System.out: CButton__onTouch__DOWN
I/System.out: CButton_onTouchEvent_DOWN
I/System.out: Activity__dispatchTouchEvent__UP
I/System.out: CLinearLayout_dispatchTouchEvent_UP
I/System.out: CLinearLayout_onInterceptTouchEvent_UP
I/System.out: CButton_dispatchTouchEvent_UP
I/System.out: CButton__onTouch__UP
I/System.out: CButton_onTouchEvent_UP
I/System.out: CButton__onClickListener clicked!
1.事件最先到达Activity的dispatchTouchEvent中,然后传递到CLinearLayout的dispatchTouchEvent中,然后交由onInterceptTouchEvent处理
...
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
...
拦截掉之后就交由onTouchEvent()处理。
三个处理函数与三种触摸事件交叉分析
参考“图解 Android 事件分发机制”,这篇文章先以ACTION_DOWN为主线,讨论了ACTION_DOWN事件在三个事件函数中的传递,然后再以图解的方式展示了不同情况下,ACTION_MOVE, ACTION_UP事件的相应处理。这里概述下该篇文章的要点
- Activity的dispatchTouchEvent中返回true和false都是消费事件
- View, ViewGroup中的dispatchTouchEvent返回true表示消费掉事件false表示不消耗,事件回到上一级的onTouchEvent, 返回super,事件传递到默认事件传递的下一级
- View, ViewGroup中的onTouchEvent返回true表示消费掉事件。返回false与super的效果一样,事件会层层向上onTouchEvent走
- ViewGroup通过onInterceptTouchEvent返回true,将事件传递给自己的onTouchEvent; View则在dispatchTouchEvent中调用super(调用默认实现)来传递到自己的onTouchEvent中
- 当dispatchTouchEvent在进行事件分发的时候,只有前一个事件(如ACTION_DOWN)返回true,才会收到ACTION_MOVE和ACTION_UP的事件。
总结
- Activity 中的dispatchTouchEvent()方法返回true和false都表示事件被消耗
- Android的事件在组件上的传递由上到下分别由Activity-->ViewGroup-->View
- Android的事件在方法上的传递按顺序由
dispatch* TouchEvent --> onInterceptTouchEvent --> onTouchEvent
需要特别注意的是,有时候在复杂布局中需要自定义的去处理触摸事件之间的冲突,会用到一把“宝剑”:
//由下一级控件(View,或者ViewGroup)高速父控件是否要拦截
//true: 告诉父控件不要拦截
//false: 告诉父控件要拦截
requestDisallowInterceptTouchEvent(boolean)
该方法一般在OnTouchListener或者dispatchTouchEvent()两个地方的合适位置(根据具体的业务逻辑分析)调用从优先级的角度看是这样的:
dispatchTouchEvent --> requestDisallowInterceptTouchEvent --> onInterceptTouchEvent --> onTouchEvent
- onTouch在事件分发方法dispatchTouchEvent中调用
- onClick在事件处理方法onTouchEvent中被调用
- onTouch事件要先于onClick事件执行
了解了基础之后,如果想了解常见冲突场景分析及其解决思路,可以移步Android事件冲突场景分析及一般解决思路