View 体系之 View 事件分发
本文原创,转载请注明出处。
欢迎关注我的 简书 ,关注我的专题 Android Class 我会长期坚持为大家收录简书上高质量的 Android 相关博文。
写在前面:
前两天我们分别总结了
View 的位置与事件:
View 的位置与事件
View 的滑动:
View 的滑动
今天我们来聊聊 View 的事件分发。相信每个人都知道 View 的事件分发实在是太重要了,它不仅仅是一个核心知识点,更是一个难点。在我初学 Android 时,View 的事件分发也前前后后看了好多次。虽然也能复述出一个大概,但是仍然有一些知识盲区。所以今天把 View 的事件分发总结出来,算是自己记下一篇学习笔记,未来复习巩固使用。如果还能为大家解决一些困惑,那就更好了。
当然关于事件分发的文章,前辈们总结了很多,有一篇我认为非常出色:
图解 Android 事件分发
这篇文章通过图解的方式,清晰直观的讲明白了事件分发的原则,本文打算对这篇文章做一些补充,补充一下这篇文章的一些关键 log,和源码分析。所以不理解事件分发的朋友可以阅读下该文。
首先大家想一下,在 Android 中谁是事件分发的掌控者和消费者?没错,是 Activity、ViewGroup、View,一个正常的 Android 应用程序他们三个肯定是存在的。而分发的事件就是 MotionEvent 对象。
关于事件分发,有三个关键的方法
dispatchTouchEvent
、onInterceptTouchEvent
、onTouchEvent
而 onInterceptTouchEvent
方法是 ViewGroup 特有的。我们在 Activity、ViewGroup、View 中分别打印这几个方法,来看看不同的返回值对事件分发的影响,来印证上文的观点,并且分析出事件分发的传递规则。
当我们不修改任何返回值,全部为默认实现时:
可以看到 ACTION_DOWN 事件的传递原则为,U 型原则,ACTION_MOVE、ACTION_UP 传递原则为距离最短原则。
分别来改变 Activity 中dispatchTouchEvent
和 onTouchEvent
的返回值,来看看事件传递的 log:
首先分别将 dispatchTouchEvent
的返回值改为 false 或者 true:
可以看到我的这次点击按钮的事件在 Activity 中的 dispatchTouchEvent
中消费掉了。
当我改变 MainActivity 中 onTouchEvent
方法的返回值时:
可以看到打印的结果与最初所有方法的默认返回值相同,这也很好理解,因为 Activity 的 onTouchEvent
方法本身就是事件 U型 传递的最后一环,不管什么返回值,反正事件都会到这里。
Activity 的看完了,再来看看 ViewGroup 的:
可以看到将 ViewGroup 的 dispatchTouchEvent 返回值改为 false 时,事件就不会再下发了,而是直接传递给 Activity 的 onTouchEvent。当 dispatchTouchEvent 返回值改为 true 时,与默认实现相同。
将 onInterceptTouchEvent
的返回值改为 true 时,事件不会再传递给 View ,而是传递给当前 ViewGroup 的 onTouchEvent。当onInterceptTouchEvent
返回值为 false 时,与默认相同。
首先明确一个概念:事件序列
就是当手指 按下–>滑动–>抬起 的这一完成过程产生的事件流为一个事件序列。
当我将 ViewGroup 的 onTouchEvent
方法的返回值改为 true 时,事件在 ViewGroup 就消费掉了,这里应该注意,onInterceptTouchEvent
如果发生了拦截,那么在一个事件序列中仅调用一次。
关于 View 这两个方法的返回值就不贴图了,与引用文章的结论一致。
一些细节
当给一个 View 设置 onTouchListener
时,它的 onTouch
方法就会回调,如果 onTouch
方法的返回值为 false,则该 View 的 onTouchEvent 方法会被调用,如果 onTouch
方法的返回值为 true,则该 View 的 onTouchEvent 方法就不会调用了,事件会直接在该 View 的 disPatchTouchEvent 中消费。另外 onClick 方法是在 onTouchEvent 方法中调用的。所以这几个方法的优先级关系为:
onTouch>onTouchEvent>onClick
一个 View 的 onTouchEvent 的返回值是与这个 View 本身的 onClick 和 onLongClick 属性相关的,只有这两个属性同时为 false 则 onTouchEvent 才会为 false,View 的 onLongClick 默认都为 false,而 onClick 属性不同,比如 button 的为 true,textview 的为 false。
事件分发的源码解析
在这部分内容中,我们看看事件分发在源码上的处理,事件最初都是在 Acitivity 中产生,然后分发给根 ViewGroup,最后再发给相应的 View。那先来看看 Activity 的 dispatchTouchEvent
方法。
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
这段代码很简单,当 ACTION_DOWN 来了的时候,回调 onUserInteraction
方法作为事件起始的回调。
然后来看看 getWindow().superDispatchTouchEvent(ev)
方法的返回值是如何的。
首先关于 Window 和 PhoneWindow 类的关系可以上面这篇我曾经总结的文章。
PhoneWindow:
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
跟进,看看 DectorView 的 superDispatchTouchEvent(event)
方法:
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
DectorView 继承自 FrameLayout,所以这里也是调用到了 ViewGroup 的 dispatchTouchEvent,事件顺利传到了 ViewGroup
来看看 ViewGroup 对事件的分发
代码比较多,我们分段来看:
// 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;
}
这段代码的意义是,判断是否要调用 onInterceptTouchEvent
方法,可以看到 if 判断的条件语句为:if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
先看第一个,当事件为 ACTION_DOWN 时,肯定会调用 onInterceptTouchEvent
,那 mFirstTouchTarget
是什么呢?由后面的代码可知,当事件不被拦截并且交给子元素处理时,mFirstTouchTarget != null
。所以当被当前 View 拦截的时候,mFirstTouchTarget == null
,ACTION_DOWN、ACTION_MOVE 事件来的时候,条件就不成立了,所以 onInterceptTouchEvent
方法也不会再次调用,这也就是为什么之前说,当此 ViewGroup 确定拦截事件的时候,onInterceptTouchEvent
之后在事件为 ACITON_DOWN 的时候调用一次。
这有一个 flag 比较重要,FLAG_DISALLOW_INTERCEPT,它的值由子 View 的 requestDisallowInterceptTouchEvent
决定,由子 View 请求父 View 不要拦截事件。当然此属性对 ACTION_DOWN 是无效的,原因是:
// 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();
}
在 dispatchTouchEvent
的开头就重置了 FLAG 的状态。
这里我们会有两个结论:
- onInterceptTouchEvent 方法并不是每次都调用,而如果事件传递进来,dispatchTouchEvent 才是每次都会调用的。
requestDisallowInterceptTouchEvent
可以干预父 View 的事件分发过程,有助于我们解决滑动冲突。
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// If there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
这段代码首先判断所有的子 View 是否具有接受事件的能力,1 没有进行动画 2 点击的位置在 View 的坐标范围内。dispatchTransformedTouchEvent
中有这样一行代码:
handled = child.dispatchTouchEvent(event);
所以到这里,就调用到了子 View 的 dispatchTouchEvent 方法。
在 addTouchTarget 方法中:
mFirstTouchTarget = target;
mFirstTouchTarget 被赋值,也就是当子 View 处理事件时,mFirstTouchTarget 不为 null.
看完了 ViewGroup 对事件分发的处理,我们来看看 View 对事件的处理吧。
/**
* Pass the touch screen motion event down to the target view, or this
* view if it is the target.
*
* @param event The motion event to be dispatched.
* @return True if the event was handled by the view, false otherwise.
*/
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
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;
}
}
return result;
}
因为 View 不会再有子 View 了,所以他的 dispatchTouchEvent 方法比较简单,首先如果这个 View 设置了 onTouchListener,并且 onTouch 方法返回值为 true 时,会进入判断条件,方法直接返回 true,就不会走到 onTouchEvent 方法里面了。所以这里也印证了我们之前的观点,也就是 onTouch 方法优先级大于 onTouchEvent。
再来看看 View 的 onTouchEvent 方法的源码:
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
可以看到这里即使 View 是 disable 的,依然可以消耗事件。
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE)
switch (action) {
case MotionEvent.ACTION_UP:
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
break;
可以看到当这个 View 的 LongClick 或者 Clickable 属性有一个为 true,就可以消耗这个事件,并且在 ACTION_UP 调用 performClick():
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;
}
所以到这里这个 View 的点击事件也响应了。
到这里,我们事件分发的源码就分析完毕了。
写在后面:
本文更多的是对上面那篇引用文章的源码补充,两篇结合起来看,对事件分发的理解就应该足够了。这几天看源码看得头疼。。。关于本文的结论总结,我准备过一阵回头温习的时候补充下,希望大家喜欢。