写点什么

带着问题重学 Android 事件分发,移动端内嵌 h5 页面

用户头像
Android架构
关注
发布于: 23 小时前

如上代码的 1 处,如果 child 的入参为 null ,那么执行 super.dispatchTouchEvent(transformedEvent) 即 View 的 dispatchTouchEvent


//View#dispatchTouchEventpublic boolean dispatchTouchEvent(MotionEvent event) {// 1boolean result = false;if (mInputEventConsistencyVerifier != null) {mInputEventConsistencyVerifier.onTouchEvent(event, 0);}final int actionMasked = event.getActionMasked();


if (onFilterTouchEventForSecurity(event)) {//2ListenerInfo li = mListenerInfo;if (li != null && li.mOnTouchListener != null&& (mViewFlags & ENABLED_MASK) == ENABLED&& li.mOnTouchListener.onTouch(this, event)) {result = true;}//3if (!result && onTouchEvent(event)) {result = true;}}


return result;}


如果 ViewGroup 决定拦截事件,那么事件是不是应该交给 ViewGroup 来消费,这个是我们之前经常背的八股文,具体怎么做的呢,前面我们已经分析到如果 ViewGroup 决定拦截事件那么事件最终会交给 View 的 dispatchTouchEvent 函数处理,在代码 1 处定义了一个变量 result 用于标记这个事件是否已经被消费处理,代码 2 处判断了是否定义了 mOnTouchListener 以及 onTouch 函数是否返回 true , 如果这些条件满足 result = true 并且 onTouchEvent 不会被执行,否则执行 onTouchEvent ,可以看到如果消费了事件 result = true 否则就是默认 false,可以看到 dispatchTouchEvent 的返回值是由 onTouchEventonInterceptTouchEvent 综合决定的。

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<View> 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 = nulltarget.next = mFirstTouchTarget;//mFirstTouchTarget 被赋值为了一个包装了 ViewGroup 的子 View 的(也就是当前点击事件下 View 层次结构下//ViewGroup 的 child view)TouchTarget.mFirstTouchTarget = target;return target;}


通过 TouchTarget 可以快速定位到事件序列上直至消费事件的那个 View 的一条链上的所有 View 对象,TouchTarget 可以看做是一个“伪单链表”,为啥这么说呢,因为 TouchTarget


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


际上并没有连接在一起,当一个 View 消费了事件时这个 View 的 dispatchTouchEvent 函数返回 true,所以它的父容器的 dispatchTransformedTouchEvent 的返回值也是 true,也就意味这个父容器 会执行 addTouchTargetmFirstTouchTarget 赋值并且 child 指向这个 View,依次递归向上,所以通过 ViewGroupmFirstTouchTarget 可以形成一条指向最终消费事件的“链表”,通过它的 child 字段找到下一层的 View 执行分发操作依次递归执行上面的步骤直到最终处理事件的 View。


小结:


  • 在 ViewGroup 中 mFirstTouchTarget 为 null 说明没有 子 View 处理事件,事件最终会交给自身处理

  • 通过 mFirstTouchTarget 可以快速定位到最终消费事件的 View 对象(如果有的话)

  • 如果有 子 View 消费了事件那么会执行 addTouchTarget 函数将下一层的 View 对象包装到 TouchTarget 节点中

shouldDelayChildPressedState

//ViewGroup#shouldDelayChildPressedStatepublic boolean shouldDelayChildPressedState() {return true;}


//View#isInScrollingContainerpublic 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#CheckForTapprivate final class CheckForTap implements Runnable {public float x;public float y;@Overridepublic void run() {mPrivateFlags &= ~PFLAG_PREPRESSED;setPressed(true, x, y);checkForLongClick(ViewConfiguration.getTapTimeout(), x, y);}}


//View#onTouchEvent#MotionEvent.ACTION_DOWNpublic 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){//代码 1handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);}else{//代码 2TouchTarget target = mFirstTouchTarget;while (target != null) {final TouchTarget next = target.next;//判断这个是否已经被消费 if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {handled = true;} else {//代码 3if (dispatchTransformedTouchEvent(ev, cancelChild,target.child, target.pointerIdBits)) {handled = true;}}}}

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
带着问题重学Android事件分发,移动端内嵌h5页面