文章目录
前言
关于View的事件分发机制,网上已经有很多的资料,但是纸上得来终觉浅呀,所以自己动手理了一遍,并写下该文章作为记录。此文名为再总结,就是希望站在巨人的肩旁上,留下自己的一点文字。
背景知识
页面构成
在 Android 系统中,一个负责交互的页面一般用 Activity 类来实现。
一个 Activity 会持有一个 PhoneWindow,而 PhoneWindow 有持有一个 DecorView,这个 DecorView 就是整个页面的根View了,DecorView 其实是一个 ViewGroup,其内部含有为 title 和 content 两个Layout区域;
当我们调用 Activity 的 setContentView() 函数传进的 xml 布局文件,将会作为 DecorView 中的 content 区域的子 View 。
Activity 构成的一种案例:
第0层:DecorView
-第1层,第1个View:LinearLayout (screen_simple.xml)
--第2层,第1个View:ViewStub
--第2层,第2个View:FrameLayout(装载 activity_my.xml 的父容器,id 为 content)
---第3层,第1个View:LinearLayout(activity_my.xml )
----第4层,第1个View:TextView
触摸事件分类
当我们在手机屏幕上进行点击,长按或者滑动等操作,对于 Android 系统来说,就是一次事件序列,该事件序列中含有三种事件类型:
- Down
- Move
- Up
这一事件序列,会从 Activity 传递到 DecorView,再传递给 DecorView 的各个子 View,直到找到处理事件的子View,称为 target。
从 Activity 到 DecorView 的传递的源码,可以参考该链接。
用伪代码来看整体逻辑
引用 该链接 的伪代码来表示整体逻辑
ViewGroup 类的 dispatchTouchEvent 函数的伪代码:
// ViewGroup
public boolean dispatchTouchEvent(MotionEvent ev) {
// 事件是否被消费
boolean consume = false;
// 判断是否拦截事件
if ( !onInterceptTouchEvent(ev) ){
// 不拦截,调用子 View 的 dispatchTouchEvent 方法
for循环:consume = child.dispatchTouchEvent(ev);
}
// 如果拦截,或者没有子 View 消费,consume 都为 false
// 此时会调用自身的 onTouchEvent 方法
if ( !consume ) onTouchEvent(ev);
// 返回值表示事件是否被消费,
// true 即事件被消费,false 则调用父 View 的 onTouchEvent方法继续处理
return consume;
}
View 类的 dispatchTouchEvent 函数的伪代码:
// View
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;//事件是否被消费
if ( onTouch(ev) ) { //判断是否拦截事件
return true;
}
if ( onTouchEvent(ev) ) { //判断是否拦截事件
return true;
}
// 返回值表示事件是否被消费,
// true 即事件被消费,false 则调用父 View 的 onTouchEvent方法继续处理
return consume;
}
从该段伪代码中提取出:
- ViewGroup#dispatchTouchEvent
- onInterceptTouchEvent 拦截判断
- child.dispatchTouchEvent 分发子类
- View#onTouch
- View#onTouchEvent – onClick
- onTouchEvent 自处理
整体时序图
本次事件的分发,分为 7 个步骤来分析,在看源码前,先看看整体时序图:
该时序图中的7个步骤跟下面的分析时对应的。 下面进入源码环节,从根 ViewGroup 开始看:
第一,判断是否拦截
事件传到 ViewGroup 的分发函数 dispatchTouchEvent :
// 代码段一:
// ViewGroup 类
// 该段函数共3个关键点,1-1 至 1-3
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
// 置空 mFirstTouchTarget
cancelAndClearTouchTargets(ev);
// 重置 mGroupFlags:
resetTouchState();
}
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// 能进来这里,只有两种情况:
// down,mFirstTouchTarget 为空,刚刚被重置
// move/up,mFirstTouchTarget 不为空,有子 View 消费事件
// 1-1
// 当第一种情况:down 事件时,上述 resetTouchState 函数重置 mGroupFlags,
// disallowIntercept 为 false,表示允许拦截,走下面的 onInterceptTouchEvent 函数判断是否拦截
// 当第二种情况:move/up 事件时,若前一个事件传到子 View 时,
// 调用 getParent().requestDisallowInterceptTouchEvent(true) 改变了 mGroupFlags 的值;
// 则后一个事件到此处,disallowIntercept 为 true,不允许拦截,不走 onInterceptTouchEvent 函数,直接放行
// 否则,disallowIntercept 为 false,允许拦截,走 onInterceptTouchEvent 函数判断是否拦截
// 本类此处代码往后,不会改动 mGroupFlags 中的 FLAG_DISALLOW_INTERCEPT 标志位,
// 所以只有子 View 会改动该标志位了。
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
// down 事件才会进这里
// 1-2
intercepted = onInterceptTouchEvent(ev); // 默认返回false,不拦截
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// 1-3
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
// 之前的 down 事件,如果有子 View 处理,则已经赋值了 mFirstTouchTarget,使其不为空,
// 则不会走这里;
// 若 move/up 走到这里的,则 mFirstTouchTarget 为空,没有子 View 处理事件
// 表示本 ViewGroup 继续处理此次 touch 事件。
}
// 省略代码 ......
注意:
注释1-1处的 FLAG_ DISALLOW_INTERCEPT
变量,子View 可以通过 requestDisallowInterceptTouchEvent 方法,设置该变量,干预 父ViewGroup 的事件分发过程(ACTION_DOWN事件除外),而这就是我们处理滑动冲突常用的关键方法。接着看看重置 mGroupFlags 的函数和 拦截判断函数。
goto resetTouchState()
和 onInterceptTouchEvent(ev)
// 代码段1.1:
// ViewGroup.java
/**
* Resets all touch state in preparation for a new cycle.
*/
private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
// We're already in this state, assume our ancestors are too
return;
}
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
// 重置,跟 resetTouchState 函数一致
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// Pass it up to our parent
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
mGroupFlags 变量是一个 32 位 int 值,其第 20 位是 0 或者 1,就代表着 disallowIntercept 变量为 false 或者 true,在代码中通过「&」位运算来确认:
disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0。
重置则通过取反再「&」位运来实现: mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT,
这样 mGroupFlags 变量第 20 位会被置为 0。
第二,遍历子 View,逐个分发
ViewGroup#dispatchTouchEvent 函数的剩余部分:
// 代码段2:
// ViewGroup.java
// 该段函数共6个关键点,2-1 至 2-6
......
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
// ......
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex(); // always 0 for down
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
// Clean up earlier touch targets for this pointer id in case they
// have become out of sync.
removePointersFromTouchTargets(idBitsToAssign);
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
// 2-1,倒序遍历,即从最外层的 View 往内层遍历
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// 。。。
// 2-2,触摸点是否在 View 的范围内,
// 该 View 是否在播放动画,均不符合,就下一个
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
// 2-3,逐个把事件传递给子 View,若其中某个子 View 返回 true,
// 则说明处理了点击事件,进入函数体处理后,break 掉循环遍历子 View
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();
// 2-4,记录处理事件的子 View ,
// 在 addTouchTarget 函数内部,赋值 target 给 mFirstTouchTarget,
// 并返回引用给 newTouchTarget
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
// 2-5,若没有找到子 View 处理事件,则指向最新添加的子 View
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
// 到此,分发事件给子View的工作结束了
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// 2-6
// 没有子View处理事件
// No touch targets so treat this as an ordinary view.
// 注意传入的 child 参数为 null,内部会调用 super.dispatchTouchEvent(event);
// 即走 View#dispatchTouchEvent --> onTouchEvent
handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);
}
......
先看 注释2-4处 的 addTouchTarget 函数: **如果找到了处理事件的子 View,将其引用赋值给 mFirstTouchTarget** goto `addTouchTarget(child, idBitsToAssign)`
// 代码段2.1:
// ViewGroup.java
/**
* Adds a touch target for specified child to the beginning of the list.
* Assumes the target child is not already present.
*/
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
再回看注释2-3处。
第三,真正的分发事件给子 View
接着 注释2-3,goto dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)
// 代码段3:
// ViewGroup.java
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// Canceling motions is a special case. We don't need to perform any transformations
// or filtering. The important part is the action, not the contents.
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
// 子类不存在,调用父类 View(注意不是父视图)的分发逻辑,如代码段4 一致
// ViewGroup 继承 View 类,所以也具有父类的特性
handled = super.dispatchTouchEvent(event);
} else {
// 3-1,子类存在,调用子类的 dispatchTouchEvent 分发函数
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
接着注释3-1处…
第四,事件传递到子 View
接着 注释3-1处:
goto child.dispatchTouchEvent(event)
// 代码段4:
// View.java
public boolean dispatchTouchEvent(MotionEvent event) {
.....
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
// 4-1,优先 onTouch 函数
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
// 4-2,其次 onTouchEvent 函数
if (!result && onTouchEvent(event)) {
result = true;
}
}
// 。。。
return result;
}
接着注释4-2处…
第五:子 View 的 onTouchEvent 函数
接着 注释4-2处:goto onTouchEvent(event)
// 代码段5:
// View.java
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
// 5-1,只要设置点击或长按监听的其中一个,就会处理事件并返回 true
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
// 。。。
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
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)) {
// 5-2 Up 事件时,执行处理点击的逻辑
performClickInternal();
}
}
}
// 。。。
break;
case MotionEvent.ACTION_DOWN:
// 。。。
break;
case MotionEvent.ACTION_CANCEL:
// 。。。
break;
case MotionEvent.ACTION_MOVE:
// 。。。
break;
}
return true;
}
return false;
接着注释5-2…
第六,onClick 函数处理点击的逻辑
接着注释5-2:goto performClickInternal()
// 代码段6:
// View.java
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();// 接着这看
}
goto 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); // 6-1,回调
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
注释6-1处,执行了 mOnClickListener 的 onClick 函数,
就是通过 View 的函数 setOnClickListener 设置进来的,详情:
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
再回看代码段2 的 注释2-6处...
第七,没有子 View 消费事件时,自身尝试消费
接着 注释2-6处:
goto dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS)
// 代码段7:
// ViewGroup.java
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// Canceling motions is a special case. We don't need to perform any transformations
// or filtering. The important part is the action, not the contents.
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
// 7-1
// 子类不存在,调用父类 View(注意不是父视图)的分发逻辑,如代 码段5 一致
// ViewGroup 继承 View 类,所以也具有父类的特性
handled = super.dispatchTouchEvent(event);
}
// 。。。
event.setAction(oldAction);
return handled;
}
注释7-1处,因为 child == null,走的是父类的 super.dispatchTouchEvent,也就是 View#dispatchTouchEvent,而 dispatchTouchEvent 函数最终会走到 View#onTouchEvent函数。也就是说,如果没有子 View 消费事件,就让本 ViewGroup 来决定是否消费事件。
到此,基本流程就走完了。
整体流程图
我自己画的流程图有点丑,可以看考 该链接 的文章看里面的流程图,画得非常好,值得学习!
另一个流程图,引用自 该链接:
小结
ViewGroup # dispatchTouchEvent 函数的步骤细节
-
1⃣️ 重置
- 如果是 down 事件,就走重置逻辑
- 重置 目标子View,mFirstTouchTarget == null
- 重置 不拦截flag,mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT
-
2⃣️ 拦截判断,条件一:down 事件就拦截
- disallowIntercept 重置为 false,走 4⃣️ intercepted = onInterceptTouchEvent(ev),默认返回 false
-
3⃣️ 拦截判断,条件二:move/up事件,有目标子View,则拦截
- disallowIntercept 在 down 事件时重置为 false,走 4⃣️ intercepted = onInterceptTouchEvent(ev),默认返回 false
- 若上一个事件传到目标子View时,子View 调用getParent().requestDisallowInterceptTouchEvent(true),将 disallowIntercept置为 true,走 5⃣️ intercepted = false
-
6⃣️ 分发
- 如果 intercepted == false,即不拦截
- 落点判断:若在落点内,则分发
- 分发事件给子View,child.dispatchTouchEvent
- 若返回 true,则将该 child 记录为 目标子View
-
7⃣️ 尝试自消化
- 没有目标子View,mFirstTouchTarget == null
- dispatchTransformedTouchEvent(ev, canceled, null, … ) 第三个参数 child 为 null
- if( child == null ) super.dispatchTouchEvent --> View#dispatchTouchEvent --> View#onTouchEvent
- 即走 onTouchEvent 函数来确定自身是否消化这个事件
- 如果自身不消化,return false 给父容器
一次事件序列在 ViewGroup#dispatchTouchEvent 函数的过程
事件 | 步骤 | 备注 |
---|---|---|
down | 1⃣️ 2⃣️ 4⃣️ 6⃣️ | 记录消费事件的目标子View |
move | 3⃣️ 4⃣️ 6⃣️ | |
… | 3⃣️ 4⃣️ 6⃣️ | |
move | 3⃣️ 4⃣️ 6⃣️ | 目标子View中,请求不拦截 |
move | 3⃣️ 5⃣️ 6⃣️ | 之后都不走步骤4⃣️,走步骤5⃣️ intercepted = false ,直接不拦截 |
… | 3⃣️ 5⃣️ 6⃣️ | |
up | 3⃣️ 5⃣️ 6⃣️ |
- 若该 ViewGroup 没有子View,且被触摸点命中 ,则过程步骤如下:
事件 | 步骤 | 备注 |
---|---|---|
down | 1⃣️ 2⃣️ 4⃣️ 6⃣️ 7⃣️ | 没有目标子View,走 onTouchEvent 函数 ,若返回 false,不处理事件,则该ViewGroup自身不会被加入到父容器的 mFirstTouchTarget 中,后续的 move/up 事件,都会直接走7⃣️,返回 false,把事件返回父容器处理 |
move | 7⃣️ | |
… | 7⃣️ | |
up | 7⃣️ |
View # dispatchTouchEvent 函数的大致步骤
-
onTouch 函数处理
- 返回 true,函数结束
-
onTouchEvent 函数处理
- 设置了点击或长按的监听,处理事件并返回 true,函数结束
- case MotionEvent.ACTION_UP:performClickInternal
- case MotionEvent.ACTION_DOWN:checkForLongClick – CheckForLongPress#run – performLongClick
- 返回 true
- 设置了点击或长按的监听,处理事件并返回 true,函数结束