深入浅出Android事件分发机制

31 阅读14分钟

本文尽可能采用白话的形式介绍Android事件分发的核心思想,经验丰富的同学可以直接移到第四节练习面试题。

Android事件分发机制是指触摸事件在View树中传递,最终找到并交给能处理该事件View的过程。

1. 基础概念

1.1. View 和 ViewGroup

  • View是Android中所有控件的基类,它代表的是一个控件。
  • ViewGroup代表一个控件组,可以嵌套多个控件,同时它本身也是一个控件,继承自View,但是它重写了View的绘制和事件分发等多个核心方法。

1.2. MotionEvent

一次完整的事件序列包含手指下按 → 手指移动[0 - n]次 → 手指抬起。

最简单的触摸动作就是点击,对应的事件序列是:手指下按 → 手指抬起,复杂一点的还可能有手指移动、多指的场景,每一个动作都被系统封装到MotionEvent对象中在View树之间流转。

事件事件含义一次事件序列中可触发次数
ACTION_DOWN手指下按,事件序列起点1
ACTION_MOVE手指移动[0, ∞]
ACTION_UP手指抬起,事件序列终点1
ACTION_POINTER_DOWN手指下按,下按前屏幕上已有手指[0, ∞]
ACTION_POINTER_UP手指抬起,抬起后屏幕上还有手指[0, ∞]
ACTION_CANCEL事件被中途拦截[0, 1]

1.3. 按压状态

按压状态分为正在按压、未按压两种状态。

ACTION_DOWN触发之后变为正在按压状态。

ACTION_UP触发之后变为取消按压状态。

可以通过xml设置这两种状态下View的背景。

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
  <item android:state_pressed="true" android:color="@color/red"/>
  <item android:state_pressed="false" android:color="@color/black"/>
</selector>

1.4. Tooltip

是指长按View时在视图附近悬浮展示一个文本提示,与OnLongClickListener互斥,它的响应优先级更低,可通过下面两种方式设置。

mBinding.viewTest.tooltipText = "测试"
或者在xml的View中
android:tooltipText="测试"

2. 事件分发起点

2.1. 页面结构

在Android项目中新建一个Activity会发现自动新增了标题栏和状态栏区域(黄色区域是xml中定义的内容)。

是因为Android会预先定义几套比较通用的模版样式,所以最终呈现给用户的视图是模版样式与开发者自定义样式的组合。整个视图的根节点是DecorView,它继承自ViewGroup。

protected ViewGroup generateLayout(DecorView decor) {
    // ...
    // 找到合适的模版样式
    if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
        layoutResource = R.layout.screen_title_icons;
    } else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0
            && (features & (1 << FEATURE_ACTION_BAR)) == 0) {
        layoutResource = R.layout.screen_progress;
    } else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {
        layoutResource = R.layout.screen_custom_title;
    } else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
        layoutResource = R.layout.screen_title;
    } else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
        layoutResource = R.layout.screen_simple_overlay_action_mode;
    } else {
        layoutResource = R.layout.screen_simple;
    }
    mDecor.startChanging();
    // 将模版的View add到DecorView中
    mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
    // 从模版View中找到专门为Activity的xml留的位置
    ViewGroup contentParent = (ViewGroup) findViewById(ID_ANDROID_CONTENT);
    return contentParent;
}

2.2. 初始分发过程

事件先由ViewRootImpl监听到,它直接传递给视图根节点DecorViewDecorView没直接处理,而是绕了一圈,给ActivityPhoneWindow拦截处理的机会,之后又重新回到了DecorView,接着才开始进行View间的事件分发流程。

3. View的分发流程

核心流程:事件最先传递给DecorView,然后使用深度优先遍历寻找愿意处理事件的View,寻找到之后将终止遍历,将结果返回给上一层。

代码角度:DFS调用dispatchTouchEvent方法分发事件,愿意处理返回true,反之为false。

3.1. View#dispatchTouchEvent

View:事件既然分发给我了,我就判断一下我要不要处理就行。

public boolean dispatchTouchEvent(MotionEvent event) {
    boolean result = false;
    // ...
    final int actionMasked = event.getActionMasked();

    // 当成true即可,校验屏幕被遮挡的情况下是否允许点击
    if (onFilterTouchEventForSecurity(event)) {
        // ⭐️ 1. 鼠标相关,无需关注,当做false
        if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
            result = true;
        }
        
        // ⭐️ 2. onTouchListener是否愿意处理事件
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
            && (mViewFlags & ENABLED_MASK) == ENABLED
        && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }

        // ⭐️ 3. onTouchEvent是否愿意处理事件
        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }

    return result;
}
  1. 与鼠标拖拽滚动条相关,手机中用不到,当成false即可。
  2. 为View设置了OnTouchListener,需要回调出去问问。
  3. onTouchEvent看它是不是要处理↓↓↓
public boolean onTouchEvent(MotionEvent event) {
    // ...
    // 是否有事件监听,有无点击事件监听 || 长按事件监听 || 设置了可上下文点击事件(手机设备上可忽略)
    final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
            || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
            || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
    // ⭐️ 4. 控件处于不可用的状态 并且 要响应点击(默认),不做实质性响应,返回true
    if ((viewFlags & ENABLED_MASK) == DISABLED
            && (mPrivateFlags4 & PFLAG4_ALLOW_CLICK_WHEN_DISABLED) == 0) {
        // ...
        return clickable;
    } 
    // ⭐️ 5. 点击事件代理,将MotionEvent交给另外一个视图处理
    if (mTouchDelegate != null) {
        if (mTouchDelegate.onTouchEvent(event)) {
            return true;
        }
    } 
    // ⭐️ 6. 如果可点击 或者 有tooltip,表示可以处理事件
    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
        switch (action) {
           // ⭐️ 7. 事件监听回调和tooltip展示流程    
        }
        return true;
    } 
    return false;
}
  1. 控件如果被设置了禁用状态,默认情况下返回值取决于是否有无事件监听,无论返回值如何都不会回调点击监听了,当然也可以通过setAllowClickWhenDisabled让View在事件分发时忽略禁用状态,继续走步骤5和6。
  2. View中还可以使用setTouchDelegate设置事件代理,将事件转发给指定的View处理。
  3. 有事件监听时由于要回调监听,所以当前View需要处理该事件;有tooltip的话,长按时需要展示小浮层所以也需要处理。
  4. 事件监听回调和tooltip展示流程单独放到3.2中。

步骤1-6中的结果,返回给上一层父ViewGroup,告诉它自己的意愿。

3.2. 事件响应

该章节讲解onTouchEvent中根据触摸事件在一定时机给于监听者单点、长按回调和展示tooltip的逻辑。

onTouchEvent源码中分别针对ACTION_DOWNACTION_UPACTION_MOVEACTION_CANCEL四种事件类别做处理,其它事件抛弃。

  • 手指按下: ACTION_DOWN
switch(action) {
        // 手指下按
    case MotionEvent.ACTION_DOWN:
        if ((viewFlags & TOOLTIP) == TOOLTIP) {
            // ⭐️ 1. ViewConfiguration.getLongPressTimeout()为400ms
            checkForLongClick(ViewConfiguration.getLongPressTimeout(), x, y);
            break;
        }

        // 是否在一个可滑动容器中
        boolean isInScrollingContainer = isInScrollingContainer();
        // ⭐️ 2.
        if (isInScrollingContainer) {
            // 设置为预按压状态
            mPrivateFlags |= PFLAG_PREPRESSED;
            if (mPendingCheckForTap == null) {
                 mPendingCheckForTap = new CheckForTap();
            }
            // ViewConfiguration.getTapTimeout()为100ms
            postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
        } else {
            // 设置按压状态
            setPressed(true, x, y);
            checkForLongClick();
        }
        break;
}

private final class CheckForTap implements Runnable {
    @Override
    public void run() {
        mPrivateFlags &= ~PFLAG_PREPRESSED;
        setPressed(true, x, y);
        // ViewConfiguration.getLongPressTimeout()为400ms
        // ViewConfiguration.getTapTimeout()为100ms
        // delay = 300ms
        final long delay =
        ViewConfiguration.getLongPressTimeout() - ViewConfiguration.getTapTimeout();
        checkForLongClick(delay, x, y, TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
    }
}
  1. 如果有tooltip,执行400ms的倒计时,倒计时完成去展示文案。
  2. 如果不在滚动视图中,立刻设置按压状态,400ms后回调长按事件监听;如果在滚动视图中,手指触摸屏幕时可能是想要滑动,所以不能立刻设置为按压状态,标记为预按压状态,先等个100ms。
    • 100ms内如果没滚动呢?那再展示按压状态,300ms后回调长按事件监听。
    • 如果滚动了呢?事件会被父滚动事件拦截掉,并发送一个ACTION_CANCEL给当前View,那时所有的倒计时和标志都会被重置。
  • 手指抬起: ACTION_UP
switch (action) {
    // 手指抬起
    case MotionEvent.ACTION_UP:
        // 1.5s后隐藏tip
        if ((viewFlags & TOOLTIP) == TOOLTIP) {
            handleTooltipUp();
            break;
        }
        // 是否是预按压状态
        boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
        // 是否是按压状态
        boolean pressed = (mPrivateFlags & PFLAG_PRESSED) != 0;
        // ⭐️ 1. 必须要是按压或者预按压状态
        if (pressed || prepressed) {
            // ...
            // ⭐️ 2. 如果之前是预按压状态,在手指抬起时,立刻转为按压状态
            if (prepressed) {
                setPressed(true, x, y);
            }

            // ⭐️ 3. 如果长按事件还没触发的话,尝试响应点击事件
            if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                removeLongPressCallback();
                // focusTaken与焦点有关,可当成false
                if (!focusTaken) {
                    if (mPerformClick == null) {
                        mPerformClick = new PerformClick();
                    }
                    // 执行setOnClickListener设置的回调
                    if (!post(mPerformClick)) {
                        performClickInternal();
                    }
                }
            }

            // 取消按压状态的回调
            if (mUnsetPressedState == null) {
                mUnsetPressedState = new UnsetPressedState();
            }

            // 如果之前是预按压状态,上面刚转为按压状态
            // 延迟64ms再取消按压状态,起码让用户瞅一眼
            if (prepressed) {
                postDelayed(
                    mUnsetPressedState,
                    ViewConfiguration.getPressedStateDuration()
                );
            } else if (!post(mUnsetPressedState)) {
                // 其它场景立刻取消按压状态
                mUnsetPressedState.run();
            }
            removeTapCallback();
        }
        break;
}
  1. 如果在抬起手指的时候发现View不是按压或者预按压状态是不会触发点击回调的(PS. 手指移动出View边界再松开是不会触发回调事件的)。
  2. 如果当前是预按压状态(滚动容器内),立刻展示按压样式,并加一点小延迟再取消按压。
  3. 如果之前没有展示过长按回调,执行点击回调
  • 手指滑动: ACTION_MOVE
switch (action) {
    case MotionEvent.ACTION_MOVE:
        // ...
        // 识别手势类别
        final int motionClassification = event.getClassification();
        // 是否是模棱两可的手势
        final boolean ambiguousGesture =
                motionClassification == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE;
        // 8dp
        int touchSlop = mTouchSlop;
        // 模棱两可的手势 && 长按事件正在倒计时
        if (ambiguousGesture && hasPendingLongPressCallback()) {
            // ⭐️ 1. 原视图上下左右各加8dp,判断点击事件是否在View区域内
            if (!pointInView(x, y, touchSlop)) {
                // ⭐️ 2. 
                // 先移除,增加延迟重新设
                removeLongPressCallback();
                // 长按超时时间往后延 400ms * 2
                long delay = (long) (
                        ViewConfiguration.getLongPressTimeout()
                                * mAmbiguousGestureMultiplier
                );
                delay -= event.getEventTime() - event.getDownTime();
                // 长按事件延迟触发
                checkForLongClick(delay,x, y);
            }
            // 变为touchSlop = 8dp * 2
            touchSlop *= mAmbiguousGestureMultiplier;
        }
        // ⭐️3. 如果View不在视图边缘16dp内
        if (!pointInView(x, y, touchSlop)) {
            removeTapCallback();
            removeLongPressCallback();
            if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
        }

        // 如果是用力按压(说明就是想长按),且有长按事件监听,立即触发
        final boolean deepPress =
                motionClassification == MotionEvent.CLASSIFICATION_DEEP_PRESS;
        if (deepPress && hasPendingLongPressCallback()) {
            removeLongPressCallback();
            checkForLongClick(0, x, y);
        }
        break;
}

MOVE事件就围绕一个热区问题:

  1. 考虑到人手指触摸屏幕的误差,即使移动出View边界了,只要在上下左右8dp扩展范围之内也可以当做在View内。
  2. 如果有长按事件并且还未执行(正在做400ms倒计时),但是手指移到了View边缘外8dp - 16dp的范围内,有些不确定用户行为的目的是不是要长按,长按倒计时从400ms改到800ms,再等等看。
  3. 如果超出了16dp,直接取消各类倒计时。
  • 事件取消: ACTION_CANCEL
switch (action) {
    // ⭐️ 1. 事件取消,比如上文提到的滑动容器接管事件序列
    case MotionEvent.ACTION_CANCEL:
        // 移除各种点击事件回调监听
        if (clickable) {
            setPressed(false);
        }
        removeTapCallback();
        removeLongPressCallback();
        mInContextButtonPress = false;
        mHasPerformedLongPress = false;
        mIgnoreNextUpEvent = false;
        mPrivateFlags3 & = ~PFLAG3_FINGER_DOWN;
        break;
}
  1. 一个正常的事件序列必须要有一个ACTION_DOWNACTION_UP的,如果中途事件被拦截了,此时View的样式可能还呈现按压样式,各种事件的倒计时还在执行,此时需要拦截者发送一个ACTION_CANCEL给当前View,起到替代ACTION_UP的作用。

3.3. ViewGroup#dispatchTouchEvent

ViewGroup:将事件先分发给孩子们,它们处理不了我再看看我要不要处理。

上文的View属于单个控件,而ViewGroup继承自View,同时内部可能包含多个View,它需要选取合适的View承接事件,所以它需要重写dispatchTouchEvent方法,方法职责是选取合适的View(子View或自身)处理事件。

// 弱化了多点触控的场景
public boolean dispatchTouchEvent(MotionEvent ev) {

    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {
         // down事件会清空mFirstTouchTarget、重置FLAG_DISALLOW_INTERCEPT
         if (actionMasked == MotionEvent.ACTION_DOWN) {
            cancelAndClearTouchTargets(ev);
            resetTouchState();
         }
        final boolean intercepted;
        // ⭐️ 3.3.1. mFirstTouchTarget
        // ⭐️ 3.3.2
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action);
            } else {
                intercepted = false;
            }
        } else {
            intercepted = true;
        }

        TouchTarget newTouchTarget = null;
        boolean alreadyDispatchedToNewTouchTarget = false;
        // 如果是cancel或者intercepted,直接去给接收事件的子View传递cancel事件
        if (!canceled && !intercepted) {
            // ⭐️ 注意只有DOWN事件才会询问子View是否愿意处理
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                // ...
                final int childrenCount = mChildrenCount;
                if (newTouchTarget == null && childrenCount != 0) {
                    // 按照子View视图层级顺序或者自定义顺序遍历
                    // 判断是否有能力处理
                    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);
                        // 判断当前子View是否能够处理 && 是否点击坐标是否在子视图范围之内
                        if (!child.canReceivePointerEvents()
                                || !isTransformedTouchPointInView(x, y, child, null)) {
                            ev.setTargetAccessibilityFocus(false);
                            continue;
                        }
                        // ...
                        // ⭐️ 3.3.3 分发事件给子视图,看它愿不愿意接收
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {				// 如果它愿意,当前序列的事件都分发给它
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            alreadyDispatchedToNewTouchTarget = true;
                            break;
                        }
                    }
                }
            }
        }

        // ⭐️ 3.3.3 没子View能处理,ViewGroup自身再尝试下是否要处理
        if (mFirstTouchTarget == null) {
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } else {
            TouchTarget predecessor = null;
            TouchTarget target = mFirstTouchTarget;
            // 将事件传递给子View处理
            // 循环是为了适配多指的情况,内有手指id判断,最多有一个子View处理
            while (target != null) {
                final TouchTarget next = target.next;
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;
                } else {
                    // 如果子View接收过事件,突然这次被父View拦截了,那么此次事件被替换为cancel传递给子View,并且mFirstTouchTarget被清空
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                            || intercepted;
                    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;
                        continue;
                    }
                }
                predecessor = target;
                target = next;
            }
        }

        // ...
    }
    return handled;
}
3.3.1. mFirstTouchTarget

每个事件都问子View太过于繁琐和耗时,记录ACTION_DOWN事件属于哪个子View处理的,接下来当前事件序列的事件都一股脑分给它。设计为链表格式是为了记录多个手指同时触摸的情况(多点触控)。

private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
    final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
    target.next = mFirstTouchTarget;
    mFirstTouchTarget = target;
    return target;
}
3.3.2. 事件拦截
  1. mFirstTouchTarget == null时,说明子View都愿意处理或者是当前是DOWN事件,还没开始分发寻找呢。
  2. disallowIntercept为false时,有'人'通过requestDisallowInterceptTouchEvent不想让ViewGroup拦截。
  3. 正常情况下都会先调下ViewGroup#onInterceptTouchEvent问下是否要拦截,拦截之后再判断下自身是否需要处理事件,不需要时也不再征求子View的意见,直接向上层返回false。见3.3.4
// down事件 || 已经交由子view处理过事件了
// 这里可以想象成,只要有可能是子View处理事件时,ViewGroup都可以尝试拦截
if (actionMasked == MotionEvent.ACTION_DOWN
    || mFirstTouchTarget != null) {
    // 默认为false,子View也有能力更改
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    if (!disallowIntercept) {
        intercepted = onInterceptTouchEvent(ev);
        ev.setAction(action); // restore action in case it was changed
    } else {
        intercepted = false;
    }
} else {
    // 这种case出现的原因是:actionMasked != MotionEvent.ACTION_DOWN && mFirstTouchTarget == null,即本身就只能ViewGroup处理
    intercepted = true;
}
3.3.3. dispatchTransformedTouchEvent

该方法是个复合方法,若传入的child不为空,就将事件分发给child,反之就将事件分发给自身。

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    final boolean handled;
    // ...
    if (child == null) {
        // 即在ViewGroup调用父类View的dispatchTouchEvent,看下自己要不要处理
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
        handled = child.dispatchTouchEvent(transformedEvent);
    }
    return handled;
}
3.3.4. 🌛标识处是比较常见的ACTION_MOVE和ACTION_UP事件分发路径,因为ACTION_DOWN时已经记录过下一层级处理者了,剩余事件直接分发即可。
public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action);
            } else {
                //🌛 step1
                intercepted = false;
            }
        } else {
            intercepted = true;
        }

        if (mFirstTouchTarget == null) {
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } else {
            //🌛 step2
            TouchTarget predecessor = null;
            TouchTarget target = mFirstTouchTarget;
            while (target != null) {
                final TouchTarget next = target.next;
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;
                } else {
                    // 分发给target子View
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                            target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                }
                predecessor = target;
                target = next;
            }
        }
    }
    return handled;
}

4. 面试场景题

  1. Q:如果没有View愿意接收事件,后续事件会是怎么分发?

    A:同序列的后续事件全部交由DecorView处理。

    ViewGroup在DOWN事件时会用深度优先遍历的方式寻找能处理的子View,找到后记录到mFirstTouchTarget中,后续事件(同一次事件序列)使用mFirstTouchTarget查到目标子View,直接分发事件,如果mFirstTouchTarget为null,会被ViewGroup拦截掉自己处理,不会再往下一层子View分发。

    MOVEUP事件传递到第一个ViewGroup,即DecorView时,会被全部拦截掉,不再往下分发。

  2. Q:parentView包含childView,手指按在childView,然后移到parentView,事件分发过程是怎么样的?

    A: 事件还是全部分发给childView。

    问题1提到ViewGroup持有mFirstTouchTargetDOWN之后的事件都是根据mFirstTouchTarget无脑分发。

  3. Q:手指按在一个View上,然后移出这个View,再移进这个View,此时松开手指,OnClickListener会回调吗?

    A:不会

    移出时View会取消按压状态,重新移入时不会重设,而View只会在按压状态才回调点击监听。

  4. Q:childView可以强制让parentView所有事件都不拦截吗?parentView可以强制拦截childView的所有事件吗?

    A:不行,可以

    childView可以通过调用requestDisallowInterceptTouchEvent(true)不让parentView拦截,但是这个标识会在DOWN 事件时被清除,所以DOWN事件可能会被拦截,如果DOWN`事件被拦截,后续其它事件更别想要了。

    而parentView可以通过重写onInterceptTouchEvent拦截到所有事件。

总结:

整体来看,DOWN事件会先分发给DecorViewDecorView通过深度优先遍历寻找到要处理该事件的View,之后事件会省略查找过程,逐层传递给该View。

希望通过这篇文章的讲解,能对大家有所帮助。如果有疑问,欢迎在评论区提问,也欢迎大家批评指正其中的不足之处。