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,下一篇吧。

相关推荐
遇见火星2 小时前
Linux 服务可用性监控实战:端口、进程、接口怎么监控?
android·linux·运维
njsgcs2 小时前
基于memos和agentscope的ai工具和记忆调用助手
android
特立独行的猫a3 小时前
从XML到Compose的UI变革:现代(2026)Android开发指南
android·xml·ui·compose·jetpack
xiangxiongfly9153 小时前
Android 共享元素转场效果
android·动画·共享元素转场效果
我是阿亮啊3 小时前
Android 中线程和进程详解
android·线程·进程·进程间通信
我命由我123454 小时前
Android 开发问题:Duplicate class android.support.v4.app.INotificationSideChannel...
android·java·开发语言·java-ee·android studio·android-studio·android runtime
似霰4 小时前
Android 平台智能指针使用与分析
android·c++
有位神秘人4 小时前
Android中BottomSheetDialog的折叠、半展开、底部固定按钮等方案实现
android
LeeeX!4 小时前
YOLOv13全面解析与安卓平台NCNN部署实战:超图视觉重塑实时目标检测的精度与效率边界
android·深度学习·yolo·目标检测·边缘计算