CrazyDailyQuestion
CrazyDailyQuestion copied to clipboard
2019-8-23:根据这个场景(在ViewGroup A中嵌套了一个VewiGroup B,然后又在ViewGroup B 中嵌套了一个View)谈一下触摸事件的传递机制。
大家可以根据图片里这个流程来扩展,展开讨论:
onTouchListener、onTouchEvent、以及onClickListener谁的优先级高?
我们知道自定义View时如果有交互,避不开要重写View的onTouchEvent()
,项目开发中setOnClickListener()
的使用更是家常便饭,除此之外,偶尔也会setOnTouchListener()
自定义触摸事件,那么如果我一个自定义View重写了onTouchEvent()
,同时又setOnClickListener()
以及setOnTouchListener()
,它们的执行顺序时怎样的?一定都会执行吗?
我们看到View的dispatchTouchEvent()
中有这么一段代码:
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
可以明显看到mOnTouchListener#onTouch()
在onTouchEvent()
之前执行,并且它的返回值会影响到onTouchEvent()
是否被执行。
在看到onTouchEvent()
的ACTION_UP事件中有:
public boolean onTouchEvent(MotionEvent event) {
...
switch (action) {
case MotionEvent.ACTION_UP:
...
performClickInternal();
...
看到performClickInternal()
private boolean performClickInternal() {
// Must notify autofill manager before performing the click actions to avoid scenarios where
// the app has a click listener that changes the state of views the autofill service might
// be interested on.
notifyAutofillManagerOnClick();
return performClick();
}
public boolean performClick() {
// We still need to call this method to handle the cases where performClick() was called
// externally, instead of through performClickInternal()
notifyAutofillManagerOnClick();
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
可以找到mOnClickListener#onClick()
的执行,也就是说点击事件的回调是包含在onTouchEvent()
中的,综合看来三者的顺序就是先mOnTouchListener#onTouch()
并且返回值会影响onTouchEvent()
是否执行,而mOnClickListener#onClick()
是最后到达的,如果我们自定义View不调用super.onTouchEvent()
,那么包括点击事件在内许多View自带的事件处理就被废掉了,就好像玄幻小说里面的经脉尽断的修者,要么牛逼,要么傻逼。
ViewGroup是怎么“放过”那些不在触摸范围内的View的?
我们在开发或者使用Android应用时,通常都是很符合直觉的摸到哪个View就是哪个View,但事实上事件时从外到里传递的,我们赖以为自然的直觉其实是ViewGroup帮我们分配的,那么体现到源码中是怎样的呢?
if (actionMasked == MotionEvent.ACTION_DOWN) {
...
for (int i = childrenCount - 1; i >= 0; i--) {
...
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
...
/**
* Returns true if a child view contains the specified point when transformed
* into its coordinate space.
* Child must not be null.
* @hide
*/
protected boolean isTransformedTouchPointInView(float x, float y, View child,
PointF outLocalPoint) {
final float[] point = getTempPoint();
point[0] = x;
point[1] = y;
transformPointToViewLocal(point, child);
final boolean isInView = child.pointInView(point[0], point[1]);
if (isInView && outLocalPoint != null) {
outLocalPoint.set(point[0], point[1]);
}
return isInView;
}
可以看到当按下事件来临时isTransformedTouchPointInView()
方法校验了每个View和当前触摸的位置,如果不满足条件,就会跳过这个View的后续操作。
View 的事件分发机制
什么是事件分发? 事件分发,通俗点就是处理点击事件
的流程。MotionEvent
就是我们要分析的点击事件对象,即当一个MotionEvent
产生以后,系统需要把这个事件传递个具体的View
。
一. 点击事件的传递规则

-
分发:dispatchTouchEvent
- 如果事件传递给当前
View
- 一定会被调用。
- 如果事件传递给当前
-
拦截:onInterceptTouchEvent
- 如果当前
View
拦截了某个事件- 同一事件中这个方法不会被再次调用。
- 如果当前
-
消费:onTouchEvent
- 如果不消耗,则在同一个事件序列中
-
View
无法再次收到事件。
-
- 如果不消耗,则在同一个事件序列中
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
if (onInterceptTouchEvent(ev)){
consume = onTouchEvent(ev);
}else {
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
二. 原理分析
三. View的滑动冲突
1. 出现场景
2. 处理规则
3. 解决方式
首先分析一下Down事件。事件传递顺序是从底层一层一层往上传的,从A到B,到C,每一层View都有消费事件的机会,如果大家都不消费,又会一层一层回传回去,从C到B到A,最终到最底层的Activity消费掉事件。
从源码角度上来细看Down事件:
- 首先会调用ViewGroup A的dispatchTouchEvent
- dispatchTouchEvent中会通过onInterceptTouchEvent判断是否拦截此事件
- 如果拦截直接调用ViewGroup的onTouchEvent
- 如果不拦截就通过child.dispatchTouchEvent遍历调用每个在触摸范围内的子View,让子View自己处理是否消费事件。
- 如果子View都不消费,就调用super.dispatchTouchEvent,实际上就是调用ViewGroup的onTouchEvent.
dispatchTouchEvent主要分发事件,真正做事件处理是在onTouchEvent中,如果onTouchEvent返回true就代表view消费了这个事件,并且也将消费这组事件序列中剩余的事件。
Down事件走完流程后,会找到最终消费这个事件序列的View。后面的事件就会直接被该View接管。所以接下来的Move、Up事件,默认都是从底层一层一层传到该View。
正常的事件流程就是这样了。但是还有一种情况,就是底层的ViewGroup可能最开始不会拦截事件,将Down以及一些Move事件给子view消费,但是当ViewGroup的onInterceptTouchEvent中检测到手指滑动达到一定的条件时,就可以在onInterceptTouchEvent返回true,以拦截这个事件以及这组事件序列中剩余事件。一旦onInterceptTouchEvent返回true,该ViewGroup就默认拦截事件,不会再调用onInterceptTouchEvent,它的子 view也不能再消费事件了。此时View Group抢夺了子View的事件,子View会收到一个Cancel事件。
事件分发机制其实整体来说涉及但View
的触摸反馈还是比较简单的,只要注意一点,重写onTouchEvent
,然后明白它的核心思想就是: 是否为消费事件取决于 ACTION_DOWN
事件是否返回为true,那么我们如何获取MotionEvent
呢,这里要讲的有两个API
,第一个是 getActionMasked
,而另外一个是 getAction
,如果你不知道用哪个,用getActionMasked
就ok了。
那么ACTION_DOWN
事件具体干了一件什么事情呢?如果在滑动事件中,切换至按压状态,并注册按下计时器。当按下进入按压状态并移动ACTION_MOVE
,重绘Ripple Effect
,然后ACTION_MOVE
是设计控件移动的反馈事件,而ACTION_CANCLE
是切换抬起状态,并清除一切状态
ViewGroup
事件分发体系稍微有点复杂,相比单view
的事件体系而言,,除了重写onTouchEvent
以外,还重写了onInterceptTouchEvent
,onInterceptTouchEvent
不用在第一时间返回true,只有当事件被拦截的时候返回true就行了。那么从源码层次怎么去理解ViewGroup
的时间分发体系呢?说白点,其实自定义view我们知道,关于viewGroup的子view都是按层级遍历解析的,那么同理,viewGroup的触摸反馈流程也是由Activity.dispatchTouchEvent 触发,然后历经 window,再到view层次,具体怎么做呢,我简单说一下:
递归Activty的dispatchTouchEvent,然后在通过调用viewGroup的dispatchTouchEvent,然后去观察ViewGroup的onInterceptTouchEvent,然后层层遍历最终child dispatchTouchEvent 和 view的onTouchEvent,最后所有子view的时间体系走完之后就走Activity的onTouchEvent