}
}
return result;
}
如果 ViewGroup 决定拦截事件,那么事件是不是应该交给 ViewGroup 来消费,这个是我们之前经常背的八股文,具体怎么做的呢,前面我们已经分析到如果 ViewGroup 决定拦截事件那么事件最终会交给 View 的 dispatchTouchEvent
函数处理,在代码1处定义了一个变量 result 用于标记这个事件是否已经被消费处理,代码2处判断了是否定义了 mOnTouchListener
以及 onTouch
函数是否返回true , 如果这些条件满足 result = true 并且 onTouchEvent
不会被执行,否则执行 onTouchEvent ,可以看到如果消费了事件 result = true 否则就是默认 false,可以看到 dispatchTouchEvent
的返回值是由 onTouchEvent
和 onInterceptTouchEvent
综合决定的。
ViewGroup不拦截事件又是如何将事件分发给子View
if(!canceled && !intercepted){
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
if (newTouchTarget == null && childrenCount != 0){
//排序所有的子控件
final ArrayList preorderedList = buildTouchDispatchChildList();
for (int i = childrenCount - 1; i >= 0; i–) {
//获取子控件的index下标
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
//获取子控件对象
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
//在dispatchTransformedTouchEvent中执行子控件的dispatchTouchEvent方法
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)){
//创建一个 TouchTarget 节点
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
}
}
当你手指触摸到屏幕这时候ViewGroup首先接收到的是一个down事件,如果不拦截会执行到上面的代码块中,这里你会发现首先会遍历循环所有的子控件调用 dispatchTransformedTouchEvent
函数,这里的 child 入参不在是 null 了所以会执行 child.dispatchTouchEvent
,也就是子控件的 dispatchTouchEvent
方法,由子控件继续执行事件的分发,这时候如果 child 消费了事件 dispatchTouchEvent
会返回true,接着会执行 addTouchTarget
函数和将 alreadyDispatchedToNewTouchTarget
标记设置为 true,alreadyDispatchedToNewTouchTarget
标记表示是否有子控件消费了这个事件,newTouchTarget
默认为 null 也就是在有 child 消费事件后才会创建一个节点。
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
在 addTouchTarget
函数中首先将 child 封装成一个 target 对象,TouchTarget 是一个单链表的数据结构在这里 ViewGroup 中的 mFirstTouchTarget
指向“封装child的target”,从这里可以看出如果有子控件消费了事件那么 mFirstTouchTarget 必然不为 null,同时如果 mFirstTouchTarget == null
那么说明没有子控件消费事件
TouchTarget
ViewGroup 里 TouchTarget
对象可以看做在事件分发序列中第一个消费了事件的控件对象的封装,除了记录消费事件的 View 对象还具备在 move 事件到来时快速定位具体的 子View 来处理事件,当一个子View消费了事件时 dispatchTransformedTouchEvent
返回 true ,接着调用 addTouchTarget
函数新建一个 TouchTarget
节点,
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
//新建 TouchTarget 节点
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
//mFirstTouchTarget初始为null,target.next = null
target.next = mFirstTouchTarget;
//mFirstTouchTarget被赋值为了一个包装了ViewGroup的子View的(也就是当前点击事件下View层次结构下
//ViewGroup的child view)TouchTarget.
mFirstTouchTarget = target;
return target;
}
通过 TouchTarget
可以快速定位到事件序列上直至消费事件的那个View的一条链上的所有View对象,TouchTarget
可以看做是一个“伪单链表”,为啥这么说呢,因为 TouchTarget
实际上并没有连接在一起,当一个View消费了事件时这个 View 的 dispatchTouchEvent
函数返回true,所以它的父容器的 dispatchTransformedTouchEvent
的返回值也是true,也就意味这个父容器 会执行 addTouchTarget
给 mFirstTouchTarget
赋值并且 child 指向这个View,依次递归向上,所以通过 ViewGroup
的 mFirstTouchTarget
可以形成一条指向最终消费事件的“链表”,通过它的 child 字段找到下一层的 View 执行分发操作依次递归执行上面的步骤直到最终处理事件的 View。
小结:
- 在 ViewGroup 中 mFirstTouchTarget 为 null 说明没有 子View 处理事件,事件最终会交给自身处理
- 通过 mFirstTouchTarget 可以快速定位到最终消费事件的 View 对象(如果有的话)
- 如果有 子View 消费了事件那么会执行 addTouchTarget 函数将下一层的View对象包装到 TouchTarget 节点中
shouldDelayChildPressedState
//ViewGroup#shouldDelayChildPressedState
public boolean shouldDelayChildPressedState() {
return true;
}
//View#isInScrollingContainer
public boolean isInScrollingContainer() {
ViewParent p = getParent();
//遍历所有的父容器只要有一个父容器的 shouldDelayChildPressedState 返回true就判定子View
//在一个滑动容器里
while (p != null && p instanceof ViewGroup) {
if (((ViewGroup) p).shouldDelayChildPressedState()) {
return true;
}
p = p.getParent();
}
return false;
}
//View#CheckForTap
private final class CheckForTap implements Runnable {
public float x;
public float y;
@Override
public void run() {
mPrivateFlags &= ~PFLAG_PREPRESSED;
setPressed(true, x, y);
checkForLongClick(ViewConfiguration.getTapTimeout(), x, y);
}
}
//View#onTouchEvent#MotionEvent.ACTION_DOWN
public boolean onTouchEvent(MotionEvent event) {
case MotionEvent.ACTION_DOWN:
//1 检查是否在一个滑动控件里
boolean isInScrollingContainer = isInScrollingContainer();
if (isInScrollingContainer) {
//2 将状态设置为预点击
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
//3 延时100ms发送一个消息
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
}else{
//将状态设置为按下状态
setPressed(true, x, y);
//检查是否长按
checkForLongClick(0, x, y);
}
break;
}
之所以将这个函数单独领出来主要是我发现很多人不知道这个函数的用法,但实际上利用好这个函数可以在自定义容器的时候带来100ms的优化,那具体怎么操作呢?
shouldDelayChildPressedState
是ViewGroup里的一个函数,你在自定义ViewGroup的时候可以重写这个函数来告诉子View这个父容器是否是一个滑动控件,默认情况下是true,也就是说在默认情况下我们的子View都是定义在一个滑动控件里的(代码意义上的),假设这么一种场景在滑动列表控件里定义一个item,但是Android并不知道你点击的是这个item还是列表本身也就是它不知道要处理哪一个,所以在item接收到down事件的时候会将当前的状态设置为预点击,也就是在代码2处并且创建一个 CheckForTap
的任务对象,调用 postDelayed 函数在100ms后执行 CheckForTap
的run函数。
CheckForTap
在它的 run 函数里首先会将状态设置为点击状态然后检查是否长按,也就是说到这一步流程和普通的down流程一样的,但是这中间经历了100ms的延时,就是说如果你自定义了一个ViewGroup没有重写 shouldDelayChildPressedState
返回false的话都要经过100ms才能响应你的down事件,所以这里建议大家如果自定义ViewGroup的时候如果你自定义的不是一个滑动容器都要重写 shouldDelayChildPressedState
返回 false。
down之后的事件如何处理
写到这里的时候我DIY了一下,我在想如果在一个事件序列从down -> move -> up,如果我的 View 的 onTouchEvent 的 down 返回了true,这种情况下事件是怎么分发的呢,艺术探索上的解释是“如果View不消耗除 ACTION_DOWN
以外的其他事件,那么这个点击事件会消失,此时父元素的 onTouchEvent
并不会被调用,并且当前View可以持续收到后续的事件,最终这些消失的点击事件会传递给Activity处理”,接下来我来一步步的验证这个结论。
首先从 down 开始,在目前的场景下父容器是没有拦截 down 事件的,事件正常分发执行到 if (!canceled && !intercepted)
代码块中,上面的代码都有所以我就不贴代码了,既然是正常分发事件那么理所当然的会执行到 dispatchTransformedTouchEvent
函数中将事件分发给 子View 处理,由于当前在child View 的 onTouchEvent 的 down 中返回true,down事件被 child 消费了 mFirstTouchTarget != null
,alreadyDispatchedToNewTouchTarget = true
(代表这个事件已经被子View消费了) , 好,现在继续跟流程,我们来看代码
// mFirstTouchTarget == null 表示肯定没有子View消费事件,事件交给自己处理
if(mFirstTouchTarget == null){
//代码 1
handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);
}else{
//代码 2
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
//判断这个是否已经被消费
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
//代码 3
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
}
}
}
既然down事件已经被消费了代码1处的判断肯定是不合法的所以继续执行到代码2处,这里有个 TouchTarget
,看完了上面的内容我想你肯定明白了此时 TouchTarget 这个节点封装了子View的child对象,由于是 move 事件所以 alreadyDispatchedToNewTouchTarget
在 dispatchTouchEvent
函数里会被重置为 false ,这时候执行到代码3处的 dispatchTransformedTouchEvent
, 看到没有,还是熟悉的配方,在 dispatchTransformedTouchEvent
内部将事件分发给子 View,如果这个事件被消费了那么返回 true,handled 会被赋值为 true,否则就是 false,但是这里已经不会在执行当前ViewGroup的onTouchEvent函数了,事件会继续向上委托处理直至 Activity,同理,如果子View没有消费down事件那么同一事件序列中的其他事件都不会再交给它来处理,并且事件将重新交由它的父元素去处理,即父元素的onTouchEvent 会被调用,这点在代码中同样可以看出来,你不消费down事件那么在其他事件过来的时候 mFirstTouchTarget == null
最终还是执行的代码1处 ViewGroup 的 onTouchEvent。
ACTION_CANCEL 事件在什么情况下触发
如果上层 View 是一个 RecyclerView
,它收到了一个 ACTION_DOWN
事件,由于这个可能是点击事件,所以它先传递给对应 ItemView
,询问 ItemView 是否需要这个事件,然而接下来又传递过来了一个 ACTION_MOVE 事件,且移动的方向和 RecyclerView
的可滑动方向一致,所以 RecyclerView
判断这个事件是滚动事件,于是要收回事件处理权,这时候对应的 ItemView 会收到一个 ACTION_CANCEL
,并且不会再收到后续事件,可以这么理解 ItemView 消费了 ACTION_DOWN 事件所以按照之前的理解是后续的事件都会交给这个 ItemView 处理,但这里少了个前提就是子View的父容器没有拦截后续事件(比如说move事件),这里我们先设置一个大前提就是子View消费了down事件并且在父容器中拦截了move事件,看看事件流程是这么走的:
public boolean dispatchTouchEvent(MotionEvent ev) {
if(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null){
//因为子View消费了down事件所以 mFirstTouchTarget != null 成立,当move事件来的时候
// onInterceptTouchEvent 函数肯定会执行到并且返回true, intercepted = true;
intercepted = onInterceptTouchEvent(ev);
}
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while(target != null){
final TouchTarget next = target.next;
//在move事件到来的时候 alreadyDispatchedToNewTouchTarget 被重置为 false,if条件不满足
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
}else{
//代码 1 ,intercepted 为 true 所以 cancelChild = true;
final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted;
//在 dispatchTransformedTouchEvent 函数中
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if(cancelChild){
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
containue;
}
}
predecessor = target;
target = next;
}
}
小结
最后
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。
因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门
如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门**
如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!