1. 点击事件的传递规则
所谓点击事件的事件分发,其实就是对MotionEvent事件的分发过程,就是一个MotionEvent产生了以后,系统需要把这个事情传递给一个具体的view,而这个传递的过程就是分发过程。 由下面三个方法来共同完成
1.1 public boolean dispatchTouchEvent(MotionEvent ev)
这个方法用来进行事件的分发。 如果事件能够传递给当前View, 那么此方法一定会被调用, 返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent方法的影响, 表示是否消耗事件。
1.2 public boolean onInterceptTouchEvent(MotionEvent ev)
在上述方法内部调用, 用来判断是否拦截某个事件, 如果当前View拦截了某个事件,此方法就不会被再次调用, 返回结果表示是否拦截当前事件。
1.3 public boolean onTouchEvent(MotionEvent ev)
在dispatchTouchEvent方法中调用, 用来处理点击事件, 返回结果表示是否消耗当前事件, 如果不消耗, 则在同一个时间序列中, 当面View 再出无法接受到事件。
那么上述三个方法到的区别是什么呢。
对于一个跟ViewGroup来说,点击事件产生后,首先会传递给它, 此时它的dispatchTouchEvent就会被调用, 如果这个ViewGroup的onInterceptTouchEvent返回true,表示它要拦截当前事件, 接着事件就会交给这个ViewGroup处理, 就是它的onTouchEvent方法就会被调用。
但是如果这个ViewGroup的onInterceptTouchEvent返回false就表示它不拦截当前事件, 此时当前事件就会继续传递给它的子元素,接着子元素的dispatchTouchEvent就会被调用, 如此反复直到事件被最终处理。
1.4 onTouch和onTouchEvent 优先级
当一个View需要处理事件时, 如果它设置了onTouchListener,那么OnTouchListener中的onTouch方法会被回调, 这时事件如何处理还要看onTouch的返回值。 如果返回false, 则当前View的onTouchEvent方法会被调用, 如果返回true则onTouchEvent方法分不回被调用。 因此onTouch的优先级要高于onTouchEvent。
1.5 事件传输过程
如下顺序:
Activity -> Window -> View
顶级View收到事件后,就会按照事件分发机制去分发事件。
还有一种情况是, 如果一个View的onTouchEvent返回false,那么它的父容器的onTouchEvent将会被调用。 如果所有元素都不处理将会返回给activity.
打个比方给我说 一个问题由经理交给一个员工进行处理,这个员工没有能力进行处理,那么就会返回给经理。
1.6 总结
- 同一个事件序列是指 从手机接触到屏幕到手指离开屏幕。 就是以down事件开始到up事件结束。
- 正常情况下,一个事件序列只能被一个view拦截且消耗。 这一条的原因可以参考下一条, 因为一旦一个元素拦截了某个事件, 那么同一个事件序列内的所有事件都会直接交给它处理,因此同一个事件不能分别交由两个View进行同时处理。 但是可以通过onTouchEvent强行传递给其他View处理。
- 某个View一旦要决定拦截,那么这一个事件序列都只能它处理, 并且它的onInterceptTouchEvent不回再被调用。 这条也很好理解,就是说当一个View决定拦截一个事件后,那么系统会把同一个事件序列内的其他方法都直接给它处理。因此不回再调用这个View的onInterceptTtouchEvent方法。
- 某个View一旦开始处理,如果它不消耗Action+Down事件即 onTouchEvent返回了false 那么同一个事件序列中的其他事件将不回交给它处理, 并且事件将重新交给它的父容器处理。 就比如经理交代给你一件事情, 你完成不好它就不回再交代你其他事情了
- 如果view不消耗除了ActionDown意外的其他事件, 那么这个点击事件会消失,此时父元素的onTouchEvent并不会被调用, 并且当前View可以持续收到后续的事件, 最后这些消失的点击事件会传递给Activity处理。
- ViewGroup默认不拦截任何事件
- View没有onInterceptTouchEvent方法,一旦由点击事件传递给它, 那么它的onTouchEvent将会被调用,
- View的onTouchEvent默认都会消耗事件,除非它是不可点击的。
- View的enable属性不影响onTouchEvent的默认返回值。 哪怕一个View是disable状态,只要它的clickable或者longClickable由一个是true 它的ontouchEvent就会返回true。
2. 事件分发的源码分析
我们先从顶部来开始看
2.1 Activity如何传递
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
从源码我们可以直到, 事件首先事件交给Activity所属Window进行分发,如果返回true,整个事件循环就结束了。 返回false就意味着事件没人处理Activity就会去进行处理。
2.2 Window是如何传递
我们可以看到getWindow().superDispatchTouchEvent(ev)是个抽象方法, 那么我们必须要找到Window的实现类, PhoneWindow。
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
看到这段代码我们可以直到,window直接将事件交给decorView处理。
DecorView的子View就是我们setContentView时候所用到的。 因为DecorView继承自Framelayout 所有的事件将会分发给子View。
2.3 顶级View对点击事件的分发。
点击事件到达一个ViewGroup后,会调用ViewGroup的dispatchTouchEvent方法, 然后逻辑是这样的, 如果顶级ViewGroup拦截事件, 则onInterceptTouchEvent返回true, 则事件由ViewGroup处理, 这时如果ViewGroup的onTouchListener被设置 则会调用onTouch, 否则onTouchEvent会被调用。
如果都提供的话onTouch会屏蔽onTouchEvent。
如果不拦截会传给子View的dispatchTouchEvent。
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;
}
通过上面这段代码我们可以得知, ViewGroup只有在如下两种情况,才会判断是否要拦截当前事件, 事件类型为Action_Down 或者mFirstTouchTarget !=null;
mFirstTouchTarget是当事件由子元素成功处理时, mFirstTouchTarget会被赋值并指向子元素, 就是当ViewGroup不拦截事件并由自元素处理时
mFirstTouchTarget !=null
当比如其他MotionEvent事件来时, 不满足这个ActionDown条件 将导致ViewGroup的onInterceptTouchEvent不会被调用。
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev);
resetTouchState();
}
通过上面这段代码得知,如果ViewGroup决定拦截事件,则onIntercepetTouchEvent不会被调用。
所以当我们决定要提前处理所有的事件时, 要用dispatchTouchEvent 只有这个方法才会保证每次都能确保调用。
然后会去遍历所有的子元素,来判断事件是否能够接受到点击事件。 是否能够接受点击要受以下两点判断:
- 子元素是否在播动画
- 点击事件的坐标是否落到子元素的区域内。
如果某个子元素满足这个两个条件, 那么事件可以交给它来处理
2.4 View的事件分发
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
首先会判断有没有设置OnTouchListener, 如果OnTouchListener的中的onAttach返回true, 那么onTouchEvent就不会被调用, OnTouchListener的优先级高于onTouchEvent, 这样做的好处是方便在外界处理点击事件。