Android知识点5--View事件分发

148 阅读15分钟

Android支持多指触控,一个完整的事件序列:

DOWN ... MOVE ... POINTER_DOWN ... MOVE .... POINTER_UP ... MOVE .... UP 从 DOWN -> POINTER_DOWN 称为焦点事件,在MotionEvent中用PointerId描述

5.1. ViewGroup 的事件分发

public abstract class ViewGroup extends View implements ViewParent, ViewManager {
    
    // TouchTarget 描述一个正在响应事件的 View
    // 一个 View 可以响应多个焦点事件
    // mFirstTouchTarget 为链表头
    private TouchTarget mFirstTouchTarget;
    
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        ......
        boolean handled = false;
        // 过滤不安全的事件
        if (onFilterTouchEventForSecurity(ev)) {
            // 获取事件的类型
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;
            // 若为 ACTION_DOWN, 则说明是一个全新的事件序列
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                cancelAndClearTouchTargets(ev);   // 清空所有 TouchTarget
                resetTouchState();                // 清空所有触摸状态
            }
            // 1. 处理事件拦截
            final boolean intercepted;
            // 1.1 若为初始事件 或 当前容器存在子 View 正在消费事件, 则尝试拦截
            if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
                // 1.1.1 判断当前 ViewGroup 的 Flag 中是否设置了 不允许拦截事件
                // 通过 ViewGroup.requestDisallowInterceptTouchEvent 进行设置, 常用于内部拦截法, 由子 View 控制容器的行为
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    // 若允许拦截, 则调用 onInterceptTouchEvent, 尝试进行拦截
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action);
                } else {
                    // 不允许拦截, 则直接将标记为置为 false
                    intercepted = false;
                }
            } else {
                // 1.2 若非初始事件, 并且没有子 View 响应事件中的焦点, 则自己拦截下来由自己处理
                // 自己处理不代表一定能消费, 这点要区分开来
                intercepted = true;
            }
            ......
            // 判断当前容器是否被取消了响应事件
            final boolean canceled = resetCancelNextUpFlag(this)
                    || actionMasked == MotionEvent.ACTION_CANCEL;
            ......
            // 判断是否需要拆分 MotionEvent
            final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
            TouchTarget newTouchTarget = null;                // 描述响应的目标
            boolean alreadyDispatchedToNewTouchTarget = false;// 描述这个事件是否分发给了新的 TouchTarget
            if (!canceled && !intercepted) {
                ......
                // 2. 若为 ACTION_DOWN 或者 ACTION_POINTER_DOWN, 则说明当前事件序列出现了一个新的焦点, 则找寻该焦点的处理者
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { 
                    // 2.1 获取焦点的索引(第一个手指的 down 为 0, 第二个手指的 down 为 1)
                    final int actionIndex = ev.getActionIndex(); // always 0 for down
                    // 将焦点的索引序号映射成二进制位, 用于后续保存在 TouchTarget 的 pointerIdBits 中
                    // 0 -> 1, 1 -> 10, 2 -> 100
                    final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex) : TouchTarget.ALL_POINTER_IDS;
                    // 清理 TouchTarget 中对历史遗留的 idBitsToAssign 的缓存
                    removePointersFromTouchTargets(idBitsToAssign);
                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        // 2.2 获取事件在当前容器中的相对位置
                        final float x = ev.getX(actionIndex);
                        final float y = ev.getY(actionIndex);
                        // 获取当前容器前序遍历的 View 序列(即层级从高到低排序)
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        // 2.3 遍历子 View, 找寻可以响应事件的目标
                        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);
                            ......
                            // 2.3.1 判断这个子 View 是否在响应事件的区域
                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ......
                                // 若不在可响应的区域, 则继续遍历下一个子 View
                                continue;
                            }
                            // 2.3.2 判断获取这个子 View, 是否已经响应了序列中的一个焦点
                            newTouchTarget = getTouchTarget(child);
                            if (newTouchTarget != null) {
                                // 若这个 View 已经响应了一个焦点, 则在它的 TouchTarget.pointerIdBits 添加新焦点的索引
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                // 则直接结束查找, 直接进行后续的分发操作
                                break;
                            }
                            ......
                            // 2.3.3 调用 dispatchTransformedTouchEvent 尝试将这个焦点分发给这个 View 处理
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                ......
                                // 子 View 成功的消费了这个事件, 则将这个 View 封装成 TouchTarget 链到表头
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                // 表示这个事件在找寻新的响应目标时已经消费了
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }
                            ......
                        }
                        ......
                    }
                    // 2.4 若没有找到可以响应的子 View, 则交由最早的 TouchTarget 处理
                    if (newTouchTarget == null && mFirstTouchTarget != null) {
                        newTouchTarget = mFirstTouchTarget;
                        while (newTouchTarget.next != null) {
                            newTouchTarget = newTouchTarget.next;
                        }
                        // 在其 pointerIdBits 保存焦点的 ID 
                        newTouchTarget.pointerIdBits |= idBitsToAssign;
                    }
                }
            }
            // 3. 执行事件分发
            // 3.1 没有任何子 View 可以响应事件序列, 则交由自己处理
            if (mFirstTouchTarget == null) {
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                // 3.2 若存在子 View 响应事件序列, 则将这个事件分发下去
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;
                    // 3.2.1 alreadyDispatchedToNewTouchTarget 为 true 并且 target 为我们上面新找到的响应目标时, 跳过这次分发
                    // 因为在查找焦点处理者的过程中, 已经分发给这个 View 了
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                        // 3.2.2 处理还未消耗这个事件的子 View
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;// 判断是否需要给 View 发送 CANCEL 事件, view 设置了 PFLAG_CANCEL_NEXT_UP_EVENT 这个 Flag, 或者这个事件被该容器拦截了, 那么将会给子 View 发送 Cancel 事件
                        // 3.2.2 调用 dispatchTransformedTouchEvent 将事件分发给子 View 
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        // 3.2.3 若给这个 View 分发了 cancel 事件, 则说明它已经无法响应事件序列的焦点了, 因此将它从响应链表中移除
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }
            } 
            // 4. 清理失效的 TouchTarget
            // 4.1 若事件序列取消 或 整个事件 UP 了, 则移除所有的 TouchTarget
            if (canceled || actionMasked == MotionEvent.ACTION_UP || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                resetTouchState();
            } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
                // 4.2 若事件序列的一个焦点 UP 了, 则移除响应这个焦点的 TouchTarget
                final int actionIndex = ev.getActionIndex();
                final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
                removePointersFromTouchTargets(idBitsToRemove);
            }
        }
        ......
        return handled;
    }
    
}

总结一下:

  • 拦截事件
    • 若为 起始事件 ACTION_DOWN/ACTION_POINTER_DOWN 或 存在子View正在消费事件,则调用onInterceptTouchEvent尝试拦截
      • 若设置了FLAG_DISALLOW_INTERCEPT 则不进行拦截操作,直接分发给子View
    • 若非 起始事件 并且没有子View响应事件,则进行拦截操作
  • 寻找起始事件的处理者
    • 通过 canViewReceivePointerEvents 和 isTransformedTouchPointInView 找寻区域内的子View
      • 若子View已经响应了一个焦点事件序列,则将这个事件序列的id添加到这个View对应的TouchTarget的pointerIdBits中
      • 若子View当前未响应事件,则调用dispatchTransformedTouchEvent尝试让子View处理
        • 处理成功,则将这个View构建成TouchTarget保存起来
    • 若无处理者,则交由mFirstTouchTarget处理
  • 执行事件分发
    • 若无响应目标,则ViewGroup自行处理
    • 若存在响应目标,则遍历TouchTarget链表,将事件分发给所有的TouchTarget

当产生一个新的事件焦点时,如何找到响应它的子View呢?

5.1.1. 寻找事件的响应目标

在ViewGoup的dispatchTouchEvent中,我们知道判断一个View是否可以响应事件主要有两个方法:canViewReceivePointerEvents 和 isTransformedTouchPointInView

canViewReceivePointerEvents : 根据View 的状态判断是否可以接受事件

public abstract class ViewGroup extends View implements ViewParent, ViewManager {
    
    private static boolean canViewReceivePointerEvents(@NonNull View child) {
        // Condition1: View 为 Visible
        // Condition2: 当前 View 设置了动画
        return (child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                || child.getAnimation() != null;
    }
    
}

isTransformedTouchPointInView : 判断事件的相对坐标是否落在子View的宽高之内

public abstract class ViewGroup extends View implements ViewParent, ViewManager {
    
    protected boolean isTransformedTouchPointInView(float x, float y, View child,
            PointF outLocalPoint) {
        // 1. 获取一个坐标数组, 数据为事件在当前 ViewGroup 中的相对坐标 x, y 值
        final float[] point = getTempPoint();
        point[0] = x;
        point[1] = y;
        // 2. 调用了 transformPointToViewLocal, 将坐标转为 Child 的相对坐标
        transformPointToViewLocal(point, child);
        // 3. 调用了 View.pointInView 判断坐标是否落在了 View 中
        final boolean isInView = child.pointInView(point[0], point[1]);
        // 若在子 View 中, 则尝试输出到 outLocalPoint 中, dispatchTouchEvent 中传入的为 null
        if (isInView && outLocalPoint != null) {
            outLocalPoint.set(point[0], point[1]);
        }
        return isInView;
    }
    
     public void transformPointToViewLocal(float[] point, View child) {
        // 2.1 将 point 转为 View 的相对坐标
        point[0] += mScrollX - child.mLeft;
        point[1] += mScrollY - child.mTop;
        // 2.2 若 View 设置了 Matrix 变化, 则通过 Matrix 来映射这个坐标
        if (!child.hasIdentityMatrix()) {
            child.getInverseMatrix().mapPoints(point);
        }
    }
    
}

public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {
    
    final boolean pointInView(float localX, float localY) {
        return pointInView(localX, localY, 0);
    }
    
    public boolean pointInView(float localX, float localY, float slop) {
        // 很简单, 即判断是否落在 View 的区域内, 小于 View 的宽度和高度
        // slop 为当前 Android 系统能够识别出来的手指区域的大小
        return localX >= -slop && localY >= -slop && localX < ((mRight - mLeft) + slop) &&
                localY < ((mBottom - mTop) + slop);
    }
    
}

在transformPointToViewLocal方法中,会调用 child.getInverseMatrix().mapPoints(point) 来映射一次坐标,为什么呢?

  • 因为我们在执行属性动画的时候,有的时候会进行View的transition ,scale 等操作,这些操作并不会改变View的原始坐标,但会改变它内部的RenderNode的Matrix,进行坐标映射,是为了让View 在变化后的区域依旧可以响应事件流,这就是为什么属性动画后View依旧可以响应点击事件的原因。

在子View判断完是否可以响应事件后,看一下ViewGroup事件流是如何分发的?

5.2. 将事件分发给子View

在ViewGroup.dispatchTouchEvent中,我们知道最后它会遍历TouchTarget链表,逐个调用dispatchTransformedTouchEvent方法,将一个事件分发给该事件所在序列的所有焦点处理者,是怎么做到的?

public abstract class ViewGroup extends View implements ViewParent, ViewManager {
    
    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;
        // 获取事件的动作
        final int oldAction = event.getAction();
        // 1. 处理 Cancel 操作
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
            // 1.1 将事件的 Action 强行改为 ACTION_CANCEL
            event.setAction(MotionEvent.ACTION_CANCEL);
            // 1.2 ACTION_CANCEL 的分发操作
            if (child == null) {
                // 1.2.1 自己处理
                handled = super.dispatchTouchEvent(event);
            } else {
                // 1.2.2 分发给子 View 
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }
        
        // 2. 判断这个 child 是否可以响应该事件
        // 2.1 获取这个事件所在序列的所有焦点
        final int oldPointerIdBits = event.getPointerIdBits();
        // 2.2 desiredPointerIdBits 描述这个 child 能够处理的焦点
        final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits; 
        // 2.3 若他们之间没有交集, 则说明出现了异常情况, 这个 child 所响应的并非是这个事件序列中的焦点
        if (newPointerIdBits == 0) {
            return false;
        }
        
        // 3. 将事件分发给子 View 
        final MotionEvent transformedEvent;
        // 3.1 若这个子 View 能够处理这个事件序列的所有焦点, 则直接进行分发操作
        if (newPointerIdBits == oldPointerIdBits) {
            // 若不存子 View, 或者子 View 设置 Matrix 变幻, 则走下面的分支
            if (child == null || child.hasIdentityMatrix()) {
                if (child == null) {
                    // 3.1.1 自己处理
                    handled = super.dispatchTouchEvent(event);
                } else {
                    // 3.1.2 分发给子 View
                    ......
                    handled = child.dispatchTouchEvent(event);
                    ......
                }
                return handled;
            }
            transformedEvent = MotionEvent.obtain(event);
        } else {
            // 3.2 若这个子 View 只能够处理事件序列中部分的焦点, 则调用 MotionEvent.split 进行焦点分割
            transformedEvent = event.split(newPointerIdBits);
        }

        if (child == null) {
            // 3.2.1 不存在子 View 则自己处理
            handled = super.dispatchTouchEvent(transformedEvent);
        } else {
            // 3.2.2 将进行焦点分割后的事件, 分发给子 View
            ......
            handled = child.dispatchTouchEvent(transformedEvent);
        }
        ......
        return handled;
    }
    
}

来看一下TouchTarget的实现


// 遍历mFirstTouchTarget链表,检测每个节点的child是不是方法参数中的child
// 参数中的child是要分派触摸事件的子View
private TouchTarget getTouchTarget(View child) {
    for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
        if (target.child == child) {
            return target;
        }
    }
    return null;
}

private static final class TouchTarget {

    public View child;

    // 可以理解为当前已按下的手指数
    public int pointerIdBits;

    public TouchTarget next;

    ......
}

在dispatchTransformedTouchEvent返回了true以后,会调用addTouchTarget方法

private TouchTarget addTouchTarget(View child, int pointerIdBits) {
    final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
    target.next = mFirstTouchTarget;
    mFirstTouchTarget = target;
    return target;
}

第一次分派事件给子View之后,会把子View和pointerIdBits记录到mFirstTouchTarget中。 当新手指按下时,会先从链表mFirstTouchTarget中查找有没有(该子View)的节点

  • 有的话更新这个节点的pointerIdBits,也就是刷新当前手指按下的数量
  • 没有的话,说明这个子View还没有手指按下过

在dispatchTransformedTouchEvent中检测到是多指按下时,会把事件拆分,最后分派拆分(调用split方法把ACTION_POINTER_DOWN改为ACTION_DOWN)后的事件给子View。分派事件后,又把这个子View和手指数量记录到链表中。

可以看到,只要View能够处理当前MotionEvent所在事件序列中的一个焦点,便会分发给它,分发之前会调用MotionEvent.split将事件分割成为View能够处理的焦点事件,并将其分发下去。

5.2.0.1. MotionEvent事件

image.png

当两个及以上的手指触摸屏幕时,会产生多个触摸事件传递给ViewGroup,改MotionEvent中除了 存储事件类型和坐标位置等信息外,还会保存一组触摸点信息。当触摸点落在ViewGroup的不同child上时,需要对MotionEvent进行事件拆分,再将拆分后的事件派发给对应的child。

MotionEvent的action为int型,高8位存储触摸点索引集合,低8位才是存储动作类型。

image.png 可以看出,索引值是会相对变化的,而ID值保持不变。

MotionEvent.split 方法主要根据出入的idBits调整事件的Action

image.png

  • 触摸点3按下时,ViewGroup会收到 ACTION_POINTER_DOWN 事件,该触摸点是ChildB感兴趣的,对于ChildB来说是一个全新的事件序列,因为派发给childB时,需要将类型调整ACTION_DOWN。而对于childA 来说,并不是它感兴趣的,因此在派发给childA时,调整为ACTION_MOVE 。
  • 当触摸点2抬起时,ViewGroup会收到 ACTION_POINTER_UP 事件,改事件是childA感兴趣的,但是childA上仍有触摸点1,因此派发给childA 的事件类型依旧是ACTION_DOWN_UP。而派发给childB时,调整为ACTION_MOVE。

事件拆分是为了在多点触摸情况下更准确的将事件传递给子View,在派发过程中,ViewGroup不会原样把MotionEvent派发给子View,而是根据落于子View上的触摸点,调整MotionEvent中的事件类型和触摸点信息后生成新的MotionEvent副本,再用这个MotionEvent副本派发给对应的子View。

E/TAG: --------------------dispatch begin----------------------
E/TAG: ViewGroup PointerId is: 0, eventRawX 531.7157, eventRawY 747.5201
E/TAG: Child PointerId is: 1, eventRawX 467.7539, eventRawY 1387.9688
E/TAG: Child PointerId is: 0, eventRawX 531.7157, eventRawY 747.5201
E/TAG: --------------------dispatch begin----------------------
E/TAG: ViewGroup PointerId is: 0, eventRawX 534.09283, eventRawY 744.18854
E/TAG: Child PointerId is: 1, eventRawX 467.7539, eventRawY 1387.9688
E/TAG: Child PointerId is: 0, eventRawX 534.09283, eventRawY 744.18854

从日志可以看出,ViewGroup中的一个事件的确会分发给这个事件序列所有的焦点处理者,可以看到MotionEvent.split方法,不仅仅分割了焦点,并且还巧妙的将触摸事件转换到了焦点处理View对应的区域,Google这里处理的原因可能是为了保证触摸过程中事件的连续性,以实现更好的交互效果。

5.3. View的事件分发

public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {
    
    public boolean dispatchTouchEvent(MotionEvent event) {
        ......
        boolean result = false;
        ......
        // 获取 Mask 后的 action
        final int actionMasked = event.getActionMasked();
        // 若为 DOWN, 则停止 Scroll 操作
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Defensive cleanup for new gesture
            stopNestedScroll();
        }
        // 处理事件
        if (onFilterTouchEventForSecurity(event)) {
            // 1. 若是拖拽滚动条, 则优先将事件交给 handleScrollBarDragging 方法消耗
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            // 2. 若 ListenerInfo 中设置了 OnTouchListener, 则尝试将其交给 mOnTouchListener 消耗
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
            // 3. 若上面的操作没有将 result 位置 true, 则交给 onTouchEvent 消耗
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }
        ......
        return result;
    }
               
}

View的dispatchTouchEvent主要操作:

  • 优先将事件交给 ScrollBar 处理
    • 成功消费则将 result = true
  • 次优先将事件交给OnTouchListener处理
    • 成功消费则将 result = true
  • 最后将事件交给onTouchEvent处理

5.3.1. View 的 OnTouchEvent 处理事件

public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {
            
    public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();
        // 判断当前 View 是否是可点击的
        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

        ......
        // 若设置了代理, 则交由代理处理
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }
        // 执行事件处理
        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                // 1. 处理按压事件
                case MotionEvent.ACTION_DOWN:
                    ......
                    // 处理 View 在可滑动容器中的按压事件
                    if (isInScrollingContainer) {
                        ......
                    } else {
                        // 处理在非滑动容器中的按压事件
                        // 1.1 设置为 Pressed 状态
                        setPressed(true, x, y);
                        // 1.2 尝试添加一个长按事件
                        checkForLongClick(0, x, y);
                    }
                    break;
                // 2. 处理移动事件
                case MotionEvent.ACTION_MOVE:
                    ......
                    // 处理若移动到了 View 之外的情况
                    if (!pointInView(x, y, mTouchSlop)) {
                        // 移除 TapCallback
                        removeTapCallback();
                        // 移除长按的 Callback
                        removeLongPressCallback();
                        // 清除按压状态
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                            setPressed(false);
                        }
                        ......
                    }
                    break;
                // 3. 处理 UP 事件
                case MotionEvent.ACTION_UP:
                    // 3.1 若置为不可点击了, 则移除相关回调, 重置相关 Flag
                    if (!clickable) {
                        removeTapCallback();
                        removeLongPressCallback();
                        ......
                        break;
                    }
                    // 3.2 判断 UP 时, View 是否处于按压状态
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        ......
                        // 3.2.1 若没有触发长按事件, 则处理点击事件
                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            removeLongPressCallback();// 移除长按回调
                            // 处理点击事件
                            if (!focusTaken) {
                                // 创建一个事件处理器
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                // 发送到 MessageQueue 中执行
                                if (!post(mPerformClick)) {
                                    performClickInternal();
                                }
                            }
                        }
                        // 清除按压状态
                        ......
                    }
                    ......
                    break;
                // 4. 处理取消事件
                case MotionEvent.ACTION_CANCEL:
                    // 移除回调, 重置标记位
                    if (clickable) {
                        setPressed(false);
                    }
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    break;
            }
            return true;
        }
        return false;
    }
                
}

onTouchEvent 事件处理:

  • ACTION_DOWN
    • 将View设置为按压状态
    • 添加了一个长按监听器
  • ACTION_MOVE
    • 若坐标在View的范围之外,则移除相关回调,清除按压状态
  • ACTION_UP
    • 若长按事件没有相应,则处理View的点击事件
    • 移除按压状态
  • ACTION_CANCEL
    • 移除相关回调,清除按压状态

5.4. View事件处理总结

5.4.1. ViewGroup 事件分发总结

  • 拦截事件
    • 若为 起始事件 ACTION_DOWN/ACTION_POINTER_DOWN 或 存在子View正在消费事件,则调用onInterceptTouchEvent尝试拦截
      • 若设置了FLAG_DISALLOW_INTERCEPT 则不进行拦截操作,直接分发给子View
    • 若非 起始事件 并且没有子View响应事件,则进行拦截操作
  • 寻找起始事件的处理者
    • 通过 canViewReceivePointerEvents 和 isTransformedTouchPointInView 找寻区域内的子View
      • 若子View已经响应了一个焦点事件序列,则将这个事件序列的id添加到这个View对应的TouchTarget的pointerIdBits中
      • 若子View当前未响应事件,则调用dispatchTransformedTouchEvent尝试让子View处理
        • 处理成功,则将这个View构建成TouchTarget保存起来
    • 若无处理者,则交由mFirstTouchTarget处理
  • 执行事件分发
    • 若无响应目标,则ViewGroup自行处理
    • 若存在响应目标,则遍历TouchTarget链表,将事件分发给所有的TouchTarget

5.4.2. View 事件分发总结

  • 优先将事件交给 ScrollBar 处理
    • 成功消费则将 result = true
  • 次优先将事件交给OnTouchListener处理
    • 成功消费则将 result = true
  • 最后将事件交给onTouchEvent处

属性动画不会改变View真正位置,它改变的是View硬件渲染结点 mRenderNode的矩阵数据

  • RenderNode是View硬件渲染机制中用来捕获View渲染动作的
  • ViewGroup的dispatchTouchTarget在寻找事件位置所在区域的子View时,会计算View矩阵映射后的坐标,因此事件分发天然就支持属性动画变换的

为什么ViewGroup会将一个事件分发给所有的TouchTarget,只分发给响应位置的TouchTarget不行?

  • 当一个手指按在屏幕上进行滑动时,另一个手指也按上去,此时他们都是连续的触摸事件
  • 但事件分发一次只能分发一个事件,为了保证所有View响应的连续性,分发时会调用MotionEvent.split方法,巧妙的将触摸事件转换到了焦点处理View对应的区域,以实现更好的交互效果。

5.5. WMS处理部分

image.png