Android中对于点击事件的深度梳理(二)

LinearLayout 单 Button 点击事件分发

从分析最简单的activity布局开始,假设我们的页面只有一个线性布局和一个按钮。

XML 复制代码
//layout_activity.xml
<LinearLayout
 android:layout_width="match_parent"
 android:layout_height="match_parent"
 android:gravity="center">
 <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="单击事件" />
</LinearLayout>
从 DecorView 到 LinearLayout

上篇文章可知,点击事件到了decorView从而触发了viewgroup的dispatch事件。

那么decoreview是什么,和我们的xml布局有什么关系呢

Android 中 Activity 的视图呈现遵循固定的三层嵌套层级

DecorView(Framlayout)

├─ 标题栏 / ActionBar 容器

└─ 内容容器(Framlayout)「com.android.internal.R.id.content」  

└─ 开发者布局「setContentView 加载」

由此可见,xml中的LinearLayout实际上是DecoreView的子view的子View.

Button 的点击事件触发流程

第一个动作是ACTION_DOWN,从DecorView层的ViewGroup开始分发。跟踪源码看下

先看第一部分

java 复制代码
//ViewGroup.java



public boolean dispatchTouchEvent(MotionEvent ev) {//当前例子中首先到达的ev.action为DOWN

/第一步:****判断点击事件有效性 可略过*****/
//这一部分判断我们使用的机器版本,除非是aosp开发人员指定机器编译选项,这里默认都是null
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
        }


//这里检测是否开启了无障碍服务(就是平常手机设置里面的无障碍),我们按照正常情况,即不开启,不会进入这个if
        // If the event targets the accessibility focused view and this is it, start
        // normal event dispatch. Maybe a descendant is what will handle the click.
        if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
            ev.setTargetAccessibilityFocus(false);
        }

/****判断点击事件有效性 *****/

        /***第二步****/
        boolean handled = false;
        
        if (onFilterTouchEventForSecurity(ev)) {//判断点击事件和view是否正常,通常为true
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            // Handle an initial down.
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // Throw away all previous state when starting a new touch gesture.
                // The framework may have dropped the up or cancel event for the previous gesture
                // due to an app switch, ANR, or some other state change.
                cancelAndClearTouchTargets(ev);//清空上一个点击事件维系的点击链表
                resetTouchState();//暗藏乾坤的方法,对后面影响极大
            }





            /*****第三步******/

            // Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {//根据这个属性来判断是否直接将intercept设置为false
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            }
到这里,可以分为三步
  1. 检查这个点击事件的正常性;
  2. 如果是down事件,做一些预处理。在这里resetTouchState还蛮关键,后面说。
  3. 确定当前viewgroup(这个例子中,依然是我们的decoreview)是否拦截该事件。

在第3步中,我们可以看到确定是否拦截的一个条件是disallowIntercept,代码中的逻辑如下

disallowIntercept

├─ false ===> intercept:onInterceptTouchEvent

└─true ===> intercept:false

再来看disallowIntercept的取值:

复制代码
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; 

其中
mGroupFlag默认为0。FLAG_DISALLOW_INTERCEPT = 0x80000不变

看上去disallowIntercept会永远为true

这里呢,就是第二步的resetTouchState的方法的调控作用了

java 复制代码
   /**
     * Resets all touch state in preparation for a new cycle.
     */
    private void resetTouchState() {
        clearTouchTargets();
        resetCancelNextUpFlag(this);
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;//这一句的代码很重要,表示mGroupFlags中的这个FLAG_DISALLOW_INTERCEPT所在的标志位改为0
        mNestedScrollAxes = SCROLL_AXIS_NONE;
    }

在对mGroupFlags进行值改变后,

final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;

disallowIntercept的值就会为false了。因为mGroupFlags & FLAG_DISALLOW_INTERCEPT的值必为0(这一部分不懂的话可以看「位掩码」相关的知识)。

回归上面的第三步中的intercept的取值,可见,接收了ACTION_DOWN事件的ViewGroup,其intercept的取值必然是onIntercept的返回。

java 复制代码
/***
* 除非是特定情况的鼠标点击事件,否则默认返回false
***/
public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
            && ev.getAction() == MotionEvent.ACTION_DOWN
            && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
            && isOnScrollbarThumb(ev.getX(), ev.getY())) {
        return true;
    }
    return false;
}

前三步看完,接着往下看:

这里又分了三步,承接前面的三步,这里概括为第四、第五、第六步。

java 复制代码
            // If intercepted, start normal event dispatch. Also if there is already
            // a view that is handling the gesture, do normal event dispatch.
/*****第四步(不用细看)******/            
/****设置是否无障碍服务***** 此处不进入该if*****/
            if (intercepted || mFirstTouchTarget != null) {
                ev.setTargetAccessibilityFocus(false);
            }

            // Check for cancelation./**检测动作是否被取消 结果为false****/
            final boolean canceled = resetCancelNextUpFlag(this)
                    || actionMasked == MotionEvent.ACTION_CANCEL;

            /**检测动作是否来自鼠标点击 结果为false****/            
            // Update list of touch targets for pointer down, if needed.
            final boolean isMouseEvent = ev.getSource() ==InputDevice.SOURCE_MOUSE;    
            /**检测是否为可分发给多个子view. 默认为false****/       
            final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0
                    && !isMouseEvent;
/*****第五步*******/
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
            if (!canceled && !intercepted) {//条件符合,进入)
                // If the event is targeting accessibility focus we give it to the
                // view that has accessibility focus and if it does not handle it
                // we clear the flag and dispatch the event to all children as usual.
                // We are looking up the accessibility focused host to avoid keeping
                // state since these events are very rare.
                View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                        ? findChildWithAccessibilityFocus() : null;

                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 =
                                isMouseEvent ? ev.getXCursorPosition() : ev.getX(actionIndex);
                        final float y =
                                isMouseEvent ? ev.getYCursorPosition() : 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;
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);
                            if (!child.canReceivePointerEvents()
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                continue;
                            }

                            newTouchTarget = getTouchTarget(child);
                            if (newTouchTarget != null) {
                                // Child is already receiving touch within its bounds.
                                // Give it the new pointer in addition to the ones it is handling.
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }

                            resetCancelNextUpFlag(child);
                            /*******第六步******/
                            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();
                                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();
                    }

                    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接收(不用管)
  • 第五步:这一步主要是查找承接当前点击事件的view, ViewGroup本身或者它的子View(不会蔓延到孙子辈)
  • 第六步:根据第五步找到的承接view,去执行它的点击事件的分发流程。
从第五步开始看:

这里可以简化为三小步:

1.拿到用户点击的坐标
复制代码
final float x =
        isMouseEvent ? ev.getXCursorPosition() : ev.getX(actionIndex);
final float y =
        isMouseEvent ? ev.getYCursorPosition() : ev.getY(actionIndex);
2.获取当前viewgroup的所有子view并排序
复制代码
final ArrayList<View> preorderedList = buildTouchDispatchChildList();//这里除非是自定义的viewGroup里面对特定的子View指定了顺序,返回的就是viewgroup的子View.[0]是xml里面第一个子view,[1]是第二个,依次轮推
3.对所有的子view从前到后排,这里的前和后是视觉效果上的前后。也就是说如果没有指定Z,都是在xml里面的从后到前排。
复制代码
for (int i = childrenCount - 1; i >= 0; i--) {//这里注意,是倒序排,也就是视觉上的view从前往后排。
    final int childIndex = getAndVerifyPreorderedIndex(
            childrenCount, i, customOrder);
    final View child = getAndVerifyPreorderedView(
            preorderedList, children, childIndex);
    if (!child.canReceivePointerEvents()
            || !isTransformedTouchPointInView(x, y, child, null)) {
//如果child不可见或者不可点,或者touchevent没落在子view的空间内,都跳过
        continue;
    }

//找到了第一个合格的子view
    newTouchTarget = getTouchTarget(child);
    if (newTouchTarget != null) {
        // Child is already receiving touch within its bounds.
        // Give it the new pointer in addition to the ones it is handling.
        newTouchTarget.pointerIdBits |= idBitsToAssign;
        break;
    }

    resetCancelNextUpFlag(child);

上面三步,找到了应该接收这个点击事件的子view,结合例子,当前viewgroup的实例是decoreView,我们点击了button,那么此时这个child实例应该是decoreview的直接子view,也就是id为com.android.internal.R.id.content的framelayout。

找到后执行了getTouchTarget(child)方法,构造了一个TouchTarget。鉴于mFirstTouchTarget,所以此处返回null.

复制代码
private TouchTarget getTouchTarget(@NonNull View child) {
    for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
        if (target.child == child) {
            return target;
        }
    }
    return null;
}
第六步(重点来了)
复制代码
第六步一开始执行了这句,我们跳入看看这个if语句的方法
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) 
复制代码
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) {
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }

    // Calculate the number of pointers to deliver.
    final int oldPointerIdBits = event.getPointerIdBits();
    final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;

    // If for some reason we ended up in an inconsistent state where it looks like we
    // might produce a motion event with no pointers in it, then drop the event.
    if (newPointerIdBits == 0) {
        return false;
    }

    // If the number of pointers is the same and we don't need to perform any fancy
    // irreversible transformations, then we can reuse the motion event for this
    // dispatch as long as we are careful to revert any changes we make.
    // Otherwise we need to make a copy.
    final MotionEvent transformedEvent;
    if (newPointerIdBits == oldPointerIdBits) {
        if (child == null || child.hasIdentityMatrix()) {
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                final float offsetX = mScrollX - child.mLeft;
                final float offsetY = mScrollY - child.mTop;
                event.offsetLocation(offsetX, offsetY);

                handled = child.dispatchTouchEvent(event);

                event.offsetLocation(-offsetX, -offsetY);
            }
            return handled;
        }
        transformedEvent = MotionEvent.obtain(event);
    } else {
        transformedEvent = event.split(newPointerIdBits);
    }

    // Perform any necessary transformations and dispatch.
    if (child == null) {
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
        final float offsetX = mScrollX - child.mLeft;
        final float offsetY = mScrollY - child.mTop;
        transformedEvent.offsetLocation(offsetX, offsetY);
        if (! child.hasIdentityMatrix()) {
            transformedEvent.transform(child.getInverseMatrix());
        }

        handled = child.dispatchTouchEvent(transformedEvent);
    }

    // Done.
    transformedEvent.recycle();
    return handled;
}

这里呢,可以看出,不管那种情况,只要是child不是null,那么最后返回的handle就是child.dispatchTouchEvent(transformedEvent);在实例中,就是执行id为com.android.internal.R.id.content的framelayout的dispatchTouchEvent,同样是执行viewgroup的dispatchTouchEvent嘛,按照之前的分析,又会走到child.dispatchTouchEvent(transformedEvent);

而此时的child是xml布局中的LinearLayout,也是ViewGroup也会执行这个dispatchTouchEvent,又会走到child.dispatchTouchEvent(transformedEvent);而这时的child,终于到了button。

┌─────────────────────────┐

│ 触摸事件触发(屏幕点击) │

└──────────────┬──────────┘

┌─────────────────────────┐

│ DecoreView.dispatchTouchEvent() │

│ (核心判断:child ≠ null) │

└──────────────┬──────────┘

┌──────────────────────────┐

│ com.android.internal.R.id.content

| → FrameLayout.dispatchTouchEvent()│

│ (ViewGroup类型,复用同分发逻辑;child ≠ null) │

└──────────────┬───────────┘

┌───────────────────────────┐

│ XML根布局 → LinearLayout.dispatchTouchEvent() │

│ (ViewGroup类型,复用同分发逻辑;child ≠ null) │

└──────────────┬────────────┘

┌────────────────────────────┐

│ 最终目标 → Button.dispatchTouchEvent() │

│ (View类型,无子View,分发终止,执行自身事件处理) │

└────────────────────────────┘

至于button怎么执行dispatchTouchEvent,下一篇吧。

相关推荐
千里马学框架13 小时前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台13 小时前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone14 小时前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc14 小时前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo16 小时前
Kotlin 协程中的 Job 结构化并发与取消
android
sun00770016 小时前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼17 小时前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone18 小时前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen18 小时前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone18 小时前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui