View点击事件分发原理,源码解读
前言
两年前我曾经写过一篇点击事件的原理博客,在今年重新翻看的时候发现文章的结构不好,且没有总结,让人不容易理解,所以重新整理了一下再写一次。
1. 原理总结
注意:正文中虽然说的都是点击事件,实际上他并不是指我们常用语境中的onClick或者onLongClick,而是任意类型的事件,只是用点击事件来形容比较让人容易理解,实际上视图的事件分发是包括按下,抬起,移动这三个部分的。
MotionEvent.ACTION_DOWN | 按下View(所有事件的开始) |
MotionEvent.ACTION_UP | 抬起View(与DOWN对应) |
MotionEvent.ACTION_MOVE | 移动View |
MotionEvent.ACTION_CANCEL | 结束事件(非人为原因) |
再我看完点击事件分发的原理之后,我会用三个词来形容点击事件的全部原理:
-
View树:
首先我们需要知道的是,在Android中我们所写的视图代码,无论是xml还是通过代码手动添加的,其数据结构展现出来的就是一个树,一个一对多的存储关系的集合。
在代码中我们表现这个树状数据结构的方式:在ViewGroup中设置了一个子View的List,通过让最上层的父节点DevorView(他也是一个ViewGroup)—持有好几个ViewGroup,里面的ViewGroup中每个又持有多个ViewGroup,层层嵌套到最底层的View为止。
-
深度搜索优先dfs:
在用户触发任意一个点击事件的时候,我们是通过深度搜索优先的方式去寻找可以消费该点击事件的视图,从树的最深层开始处理点击事件。
这个代表了什么意思呢?如果一个ViewGroup和它的子View同时都设置了OnClickListener,那么我们在点击它们之间重合的部分时,只会触发子View的点击事件而不会触发父View的点击事件。
在代码表现这个深度搜索的方式:处理分发事件的时候,会先用for循环把所有的子View遍历,尝试调用子View的方法来处理该点击事件,只有确认所有子View都不能处理该点击事件之后,才会调用自己的点击事件处理方法。
-
逆序遍历:
触发点击事件搜索子View的时候,总有个搜索的顺序,这个顺序是逆序,也就是从最后一个添加的子View开始查找和处理点击事件。
其原理和添加VIew是相关联的,我们知道,在一个ViewGroup的两个子View中,如果这两个子View有重合的部分,那么一般而言总是后添加的视图会覆盖掉前面添加的视图的部分。
点击事件也是同理,当用户点击他们重合的部分时,一般而言用户总是希望点击到用户本身可以看见的那个视图。所以我们的点击事件分发就和添加视图的顺序相反,从最后添加的视图开始遍历。
2.1 时序图总结
为了方便我们看完源码之后以后复习方便,我先将点击事件分发的全部流程放到这里,在看的过程中有需要可以翻回来看:
2.2 流程图总结
由于时序图一般而言没有办法很好的展示我们深度优先搜索的思想,所以我额外又补充了一张流程图,这个流程图也画出了点击事件分发的原理:
2. 源码解读
2.1 Activity到ViewGroup
任何的事件源头都是从我们底层的SurfaceFlinger进程来的,他直接管理着用户可以看到的窗口,但是我们这里不用去深究那么底层的原理。只要知道从底层来的点击事件第一个触发的是Activity的DispatchTouchEvent就足够了。
public class Activity extends ContextThemeWrapper
implements LayoutInflater.Factory2,
Window.Callback, KeyEvent.Callback,
OnCreateContextMenuListener, ComponentCallbacks2,
Window.OnWindowDismissedCallback,
ContentCaptureManager.ContentCaptureClient {
/**
* 调用以处理触摸屏事件。您可以重写此方法,在将所有触摸屏事件发送到窗口之前拦截它们。
* 请确保为应该正常处理的触摸屏事件调用此实现。
*
* @param ev 点击事件本体
* @return boolean 如果事件被消费了会return true
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
// 重点是这行
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
}
点击事件就这样从Activity手上分出去了,接下来看看Window类是如何处理的,顺便一提,Window本身是一个抽象类,作为他承载的实体一般而言是PhoneWindow类。
public class PhoneWindow extends Window implements MenuBuilder.Callback {
private DecorView mDecor;
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
}
事件就这样直接转到了DecorView。
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
final Window.Callback cb = mWindow.getCallback();
return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
? cb.dispatchTouchEvent(ev) : /* 重点看这部分 */super.dispatchTouchEvent(ev);
}
}
DecorView作为最上层的特殊View,他在处理事件的时候会有特殊的窗口判断,但是一般而言是不会触发的,我们不去理他,重点看super.dispatchTouchEvent(ev),这个就代表了点击事件的真正起点。
2.2 ViewGroup
接下来就进入到我们真正的主角,ViewGroup了,我会将他的源码切成好几段一点点的说明。
事件中断
在正式开始点击事件之前,ViewGroup会通过onInterceptTouchEvent这个方法对点击事件做一个中断判断,如果被中断了就不会处理后续的流程了
onInterceptTouchEvent这个方法一般而言都是会返回false,也就是不中断。如果你有业务上的需求需要中断的话,可以返回true。这样事件就不会往下面的View分发,只会由自己进行处理。
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
// 检查事件是否被中断
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);
} else {
intercepted = false;
}
} else {
intercepted = true;
}
// 如果被拦截,就会跳过分发的流程
if (!canceled && !intercepted) {
// ...正式分发点击事件
}
// 自己处理点击事件
return handled;
}
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
&& ev.getAction() == MotionEvent.ACTION_DOWN
&& ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
&& isOnScrollbarThumb(ev.getXDispatchLocation(0), ev.getYDispatchLocation(0))) {
return true;
}
return false;
}
}
逆序搜索
开始正式处理点击事件,会先逆序遍历所有的子View,然后进行一些判断
- 该子View能否点击。canReceivePointerEvents
- 用户点击的位置是否和该View重合。isTransformedTouchPointInView
两个判断条件都符合后,就会尝试在该子View中处理点击事件
注意,子View也有可能是一个ViewGroup,所以调用子View的dispatchTouchEvent后,有可能会实际上调用的还是ViewGroup.dispatchTouchEvent。
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
// 检查事件是否被中断
final boolean intercepted;
TouchTarget newTouchTarget = null;
if (!canceled && !intercepted) {
// ...正式处理点击事件
final int childrenCount = mChildrenCount;
for (int i = childrenCount - 1; i >= 0; i--) {
// 不用关注他的原理,我们只需要他掏出了一个View即可。
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
// 对View做合法性判断,如果合法就可以继续点击
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
// 转换为点击事件,注意child这个入参我们是有传值的
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
//...处理了一些逻辑
break; //然后直接跳出循环
}
}
// 下面又开始处理其他逻辑
}
return handled;
}
/**
* 分发转换为点击事件
*/
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
// 注意child这个入参,我们此时传入的child不为空,所以走下面
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
}
}
自己处理点击事件
在遍历了所有子View都没有处理掉该事件之后,ViewGroup会尝试自己来处理该事件。
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
TouchTarget newTouchTarget = null;
if (!canceled && !intercepted) {
// ...正式处理点击事件
TouchTarget newTouchTarget = null;
for (int i = childrenCount - 1; i >= 0; i--) {
// 刚才的for循环,用来表示代码的相对位置
}
// 如果点击事件之前被子View给处理了,
// 那么代码到这里之后newTouchTarget就不为空,或者mFirstTouchTarget不为空
if (newTouchTarget == null && mFirstTouchTarget != null) {
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
// 这里和上面是连着的,如果mFirstTouchTarget== null其实就代表着点击事件没有被子View给处理
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// 处理其他逻辑
}
return handled;
}
/**
* 分发转换为点击事件
*/
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
// 注意child这个入参,我们此时传入的child为空,所以走上面
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
}
}
ViewGroup总结
要不然就是通过某个子View层层遍历,走到最深层的某个View的dispatchTouchEvent
要不然就是没有子View,调用自己的super,dispatchTouchEvent,还是View的dispatchTouchEvent
总而言之,代码就会通过这两种方式走到View这个类里面,
ViewGroup的dispatchTouchEvent这个方法的功能也很明显了:找一个可以处理该点击事件的View(可能是自己),将点击事件(TouchEvent)分发(Dispatch)给它(View)。
2.3 View
OnTouchListener
点击事件到View之后,入口还是DispatchTouchEvent,他会先检查是否有TouchListener,有的话先执行它。
这里有两个点,第一个点就是在View里面,OnClickListener和OnTouchListener是完全不同的东西。
第二个点就是OnTouchListener这个方法的return是有开发者自己控制的,换句话说,开发者可以自行控制事件是否要停在onTouch这里
public class View implements Drawable.Callback, KeyEvent.Callback,
AccessibilityEventSource {
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
// 这个if一般而言都是通过的
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
// 重点看这里
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
// 如果事件没有被onTouch处理掉,就会进入事件处理流程
if (!result && onTouchEvent(event)) {
result = true;
}
}
return result;
}
public interface OnTouchListener {
boolean onTouch(View v, MotionEvent event);
}
}
onTouchEvent
这里就是单个事件真正的处理方法,但是对于我们而言我们反而不需要太关注这个方法的处理逻辑。
第一是本文主要关注事件时如何分发到这里的。
第二是该方法无非就是对我们常用的一些逻辑,如focus,onClickListener,onLongClick等内容进行判断,有的话这个方法就会return true,没有的话就return false
public class View implements Drawable.Callback, KeyEvent.Callback,
AccessibilityEventSource {
public boolean onTouchEvent(MotionEvent event) {
final int action = event.getAction();
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
// 一堆判断之后会走到这里,我们就不看这些判断了
performClickInternal();
break;
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_CANCEL:
break;
case MotionEvent.ACTION_MOVE:
break;
}
return true;
}
return false;
}
private boolean performClickInternal() {
notifyAutofillManagerOnClick();
return performClick();
}
public boolean performClick() {
notifyAutofillManagerOnClick();
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
// 这里就是我们设置的点击事件了,OnClickListener
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
}
3. 附录:时序图uml代码
@startuml
participant Activity
participant PhoneWindow
participant DecorView
participant ViewGroup as vg
participant View as v
participant TouchListener as tl
participant "子View\nViewGroup" as cvg
activate Activity
Activity -> PhoneWindow : dispatchTouchEvent
activate PhoneWindow
PhoneWindow -> DecorView : dispatchTouchEvent
activate DecorView
DecorView -> vg : dispatchTouchEvent\n进入View树处理点击事件
activate vg
vg -> vg : onInterceptTouchEvent\n判断事件是否被拦截
activate vg
vg --> vg : return boolean
deactivate vg
alt true
vg --> vg : return\n不进行任何点击事件的处理\n流程结束
end
loop 逆序遍历子View
vg -> cvg : isTransformedTouchPointInView\n判断是否可以点击
activate cvg
cvg --> vg : return boolean\ntrue代表可以点击
deactivate cvg
alt 不能点击
vg -> vg : continue\n搜索下一个子View
else 可以点击
vg -> vg : dispatchTransformedTouchEvent\n将分发事件转变为点击事件
activate vg
vg -> cvg : dispatchTouchEvent\n重复该ViewGroup的行为,继续往下分发
activate cvg
break 点击事件被消费
cvg --> vg : return boolean\n告知点击事件是否被消费
end
deactivate cvg
deactivate vg
end
end
alt 点击事件没被消费
vg -> vg : dispatchTransformedTouchEvent\n将分发事件转变为点击事件
activate vg
vg -> v :dispatchTouchEvent
activate v
alt 该View有TouchListener
v -> tl : onTouch
activate tl
tl --> v : return boolean\n告知是否继续往下处理
deactivate tl
end
alt return true
v --> vg : return true\n告知点击事件已经被处理
else return false
v -> v :onTouched
activate v
v --> v : return boolean\n告知是否处理了点击事件
deactivate v
v --> vg : return boolean\n告知是否处理了点击事件
end
deactivate v
deactivate vg
end
vg --> DecorView : return boolean\n告知是否处理了点击事件
deactivate vg
DecorView --> PhoneWindow : return boolean\n告知是否处理了点击事件
deactivate DecorView
PhoneWindow --> Activity : return boolean\n告知是否处理了点击事件
deactivate PhoneWindow
deactivate Activity
@enduml