本文基于api30, 学习源码中的触摸事件。
本文学习单点触摸,不包含多点触摸,不包含嵌套滑动
首先我们看View
类,作为视图树的叶节点,View
只有处理逻辑,没有分发逻辑
View
touch事件入口是dispatchTouchEvent
,这里只关注主要逻辑:
public boolean dispatchTouchEvent(MotionEvent event) {
...
boolean result = false;
...
if (onFilterTouchEventForSecurity(event)) {
//一般onFilterTouchEventForSecurity会返回true
...
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
//OnTouchListener#onTouch事件处理
&& li.mOnTouchListener.onTouch(this, event)) {
//OnTouchListener#onTouch返回true,则最终结果为true
result = true;
}
//只有OnTouchListener#onTouch返回false的情况下,才会调用onTouchEvent方法
if (!result && onTouchEvent(event)) {
//onTouchEvent返回true,则最终结果为true
result = true;
}
...
return result;
}
onTouchEvent
不是我们分析的重点。只需要知道,click、longClick
事件是在onTouchEvent
中处理的即可
也就是说,如果OnTouchListener#onTouch
返回true,那么将不会产生click、longClick
事件
ViewGroup
ViewGroup除了本身的事件处理外,还负责事件分发。事件分发需要区分是否是MotionEvent.ACTION_DOWN
事件。下面依次分析
MotionEvent.ACTION_DOWN事件分发流程
依然从入口dispatchTouchEvent
开始,该方法内的逻辑可以分为三部分:检查是否拦截、事件分发逻辑、事件处理逻辑
列出主要代码:
public boolean dispatchTouchEvent(MotionEvent ev) {
...
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
//一般onFilterTouchEventForSecurity会返回true
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
if (actionMasked == MotionEvent.ACTION_DOWN) {
//这里清除一些标志位,如通过调用requestDisallowInterceptTouchEvent方法设置的标志位
cancelAndClearTouchTargets(ev);
resetTouchState();
}
//一、检查是否需要被拦截
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//单点触摸,Down事件一定走这里
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
//单点Down事件一定走这里,因为上面已经清空了FLAG_DISALLOW_INTERCEPT标志位
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
...
}
} else {
...
}
...
//二、事件分发逻辑
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
//事件分发,如果intercepted为true,则不会分发
...
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
//这个if才是真正的Down事件分发
...
if (newTouchTarget == null && childrenCount != 0) {
//有子View,进行分发
...
for (int i = childrenCount - 1; i >= 0; i--) {
//对子View进行遍历,查找第一个消费Down事件的子View
...
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
//有子View消费了Down事件
...
//保存消费Down事件的子View到mFirstTouchTarget、newTouchTarget
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
//已经找到目标,退出for循环
break;
}//if
...
}//for
...
}//if(newTouchTarget == null && childrenCount != 0)
...
}//if(actionMasked == MotionEvent.ACTION_DOWN
...
}//if (!canceled && !intercepted)
//三、事件处理逻辑
if (mFirstTouchTarget == null) {
//没有需要处理事件的子View,则自己处理
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
//有处理事件的子View
...
TouchTarget target = mFirstTouchTarget;
while (target != null) {
//单点触摸,只循环一次
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
//alreadyDispatchedToNewTouchTarget 在上面被设置为true,并且mFirstTouchTarget==newTouchTarget
//Down事件在分发的逻辑中已经被处理,这里不再额外处理
handled = true;
} else {
...
}
...
}//while
}//else
...
}//if (onFilterTouchEventForSecurity(ev))
...
return handled;
}
备注几个用到的方法
addTouchTarget
保存消费Down事件的子View
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
//链表是为了处理多点触摸事件
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
事件的分发和处理逻辑都会调用到dispatchTransformedTouchEvent
,Down事件中,该方法重要逻辑如下:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
...
if (child == null) {
//自身处理
handled = super.dispatchTouchEvent(transformedEvent);
} else {
//分发逻辑中,child不为空
...
handled = child.dispatchTouchEvent(transformedEvent);
}
...
return handled;
}
可以看到,该方法会调用child.dispatchTouchEvent
判断子VIew是否需要消费Down事件。可以理解为该方法是真正的事件处理
非Down事件处理
这里以Move事件为例(其他事件区别不大)
从上面的分析可以知道,只有Down事件才有分发逻辑。
若有子View消费了Down事件,即表示该子View对触摸事件感兴趣,那么会将该子View保存到mFirstTouchTarget
中
非Down事件会直接交给自身或者mFirstTouchTarget
处理,不再遍历子View进行分发
这样非Down事件就只剩下了检查是否拦截、事件处理逻辑,如下:
public boolean dispatchTouchEvent(MotionEvent ev) {
...
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
//一般onFilterTouchEventForSecurity会返回true
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
//一、检查是否需要被拦截
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//有需要处理触摸事件的子View
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
//子View没有调用requestDisallowInterceptTouchEvent
//是否拦截取决于onInterceptTouchEvent
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
//子View有调用requestDisallowInterceptTouchEvent
//则不拦截
intercepted = false;
}
} else {
//没有需要处理触摸事件的子View
//则拦截
intercepted = true;
}
...//不再分发
//二、事件处理逻辑
if (mFirstTouchTarget == null) {
//没有需要处理事件的子View,则自己处理
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
//有处理事件的子View
...
TouchTarget target = mFirstTouchTarget;
while (target != null) {
//单点触摸,只循环一次
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
//alreadyDispatchedToNewTouchTarget始终为false,不再走这里
...
} else {
//若拦截,则cancelChild 为true
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
//后面分析这个方法
handled = true;
}
if (cancelChild) {
//若被拦截,走这里
if (predecessor == null) {
//将mFirstTouchTarget置为空
//单点触摸,next为空
mFirstTouchTarget = next;
} else {
...
}
...
}
}
...
}//else
...
}//while
}//else
...
}//if (onFilterTouchEventForSecurity(ev))
...
return handled;
}
非Down事件的事件处理dispatchTransformedTouchEvent
如下:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
//被拦截时,cancel为true
//将事件类型修改为ACTION_CANCEL后下发处理
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
//还原事件的原本类型
event.setAction(oldAction);
return handled;
}
//未被拦截时,cancel为false,继续执行
...
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
...
handled = child.dispatchTouchEvent(transformedEvent);
}
...
return handled
}
以上便是触摸事件的分发、处理逻辑。总结如下:
- Down事件,有拦截、分发、处理逻辑
- 拦截逻辑:一般不拦截Down事件,否则子View始终收不到触摸事件
- 分发逻辑:查找对触摸事件感兴趣的子View,若找到,保存在
mFirstTouchTarget
中 - 处理逻辑:
-
mFirstTouchTarget
不为空:已经在分发时通过dispatchTransformedTouchEvent
交给子View处理过了; - 否则通过
dispatchTransformedTouchEvent
交给自身的super.dispatchTouchEvent
处理
-
- 非Down事件,有拦截、处理逻辑
- 拦截逻辑:
-
mFirstTouchTarget
为空:拦截 -
mFirstTouchTarget
不为空,且子View调用parent#requestDisallowInterceptTouchEvent(true)
:不拦截 - 否则由自身的
onInterceptTouchEvent
决定是否拦截
-
- 处理逻辑:
-
mFirstTouchTarget
为空:通过dispatchTransformedTouchEvent
交给自身的super.dispatchTouchEvent
处理 -
mFirstTouchTarget
不为空,且不拦截:通过dispatchTransformedTouchEvent
交给子View处理 -
mFirstTouchTarget
不为空,且拦截:将事件类型临时修改为Cancel,交给子View处理,同时mFirstTouchTarget
被置为空!
-
- 拦截逻辑:
- 如果子View处理了Down事件,那么可能不会收到Move事件(比如被parent拦截)。但一定会收到Up和Cancel其中一个事件。
冲突解决
这里的冲突一般是指Move事件
View的OnTouchListener#onTouch返回true时,onClick不会执行,也可以算作是事件冲突
解决Move事件的冲突,可以分为两种方式:
- 子View处理
- 父View处理
子View处理
即通过调用parent#requestDisallowInterceptTouchEvent(true)
方法,来允许或者禁止parent拦截Move事件
android原生widget,基本都是子View处理
父View处理
即在onInterceptTouchEvent
方法中,决定何时拦截或不拦截Move事件
以上就是两种解决Move事件冲突的方式,不论哪一种,一旦Move事件被父View处理,那么子View就再也收不到了
想要解决这个问题,就需要使用android提供的嵌套滑动方案了
完