View—事件分发

作者: SharryChoo | 来源:发表于2018-01-23 14:45 被阅读22次

    TouchTarget

    在开始分析事件分发之前, 需要了解一下 TouchTarget 这个类的作用

    在ViewGroup的事件分发中, 它起到了不可获取的作用, 不了解其原理, 看起源码可能会导致身体不适

    下面开始分析

       /**
        * ViewGroup.TouchTarget 链表结构
        */
        private static final class TouchTarget {
            // 链表最大的长度
            private static final int MAX_RECYCLED = 32;
            // 用于控制同步的锁
            private static final Object sRecycleLock = new Object[0];
            // sRecycleBin 内部可复用实例链表表头
            // 注意 ViewGroup 中还维护着一个 mFirstTouchEvent, 它是外部记录正在响应事件 View 的链表, 响应完成之后会调用 recycler 方法, 加入 sRecycleBin 这个可复用的链表中
            private static TouchTarget sRecycleBin;
            // 内部可复用的实例链表的长度
            private static int sRecycledCount;
    
            public static final int ALL_POINTER_IDS = -1; // all ones
    
            // 当前被触摸的 View
            public View child;
            // The combined bit mask of pointer ids for all pointers captured by the target.
            // 对目标捕获的所有指针的指针id的组合位掩码
            public int pointerIdBits;
            // 链表中指向的下一个目标
            public TouchTarget next;
    
            private TouchTarget() {
            }
    
            public static TouchTarget obtain(@NonNull View child, int pointerIdBits) {
                if (child == null) {
                    throw new IllegalArgumentException("child must be non-null");
                }
    
                final TouchTarget target;
                synchronized (sRecycleLock) {
                    if (sRecycleBin == null) {
                        target = new TouchTarget();
                    } else {
                        // 从链表复用池中取出一个对象, 并重至属性值
                        target = sRecycleBin; // 将当前的表头赋给这个变量
                        sRecycleBin = target.next; // 表头移动到下个位置
                        sRecycledCount--; // 当前复用池的数量 -1 
                        target.next = null; // 将它的 next 置空
                    }
                }
                // 重新绑定数据
                target.child = child;
                target.pointerIdBits = pointerIdBits;
                return target;
            }
            
            public void recycle() {
                if (child == null) {
                    throw new IllegalStateException("already recycled once");
                }
                synchronized (sRecycleLock) {
                    if (sRecycledCount < MAX_RECYCLED) {
                        // 之前的表头变成了 next
                        next = sRecycleBin;
                        // 更新链表表头位置为自己
                        sRecycleBin = this;
                        // 复用池的数量 +1
                        sRecycledCount += 1;
                    } else {
                        next = null;
                    }
                    child = null;
                }
            }
        }
        
        
        /**
         * ViewGroup.addTouchTarget
         */
        private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
            // 1. 创建一个新的 TouchTarget 对象
            final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
            // 2. 让他链上上一个对象, 第一次的话 mFirstTouchTarget 肯定为 null
            target.next = mFirstTouchTarget;
            // 3. 更新 mFirstTouchTarget 的值
            mFirstTouchTarget = target;
            return target;
        }
        
        /**
         * ViewGroup.getTouchTarget
         */
        private TouchTarget getTouchTarget(@NonNull View child) {
            // 变量 mFirstTouchTarget 形成的链表, 寻找当前 child 对应的 TouchTarget
            for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
                if (target.child == child) {
                    return target;
                }
            }
            return null;
        }
    

    需要着重注意的两点

    1. sRecycleLock: 在 TouchTarget 内部维护的链表, 用于处理 TouchTarget 实例的复用
      • sRecycleLock 指向链表首部
    2. mFirstTouchTarget: 在 ViewGroup 中维护的链表, 用于记录当前响应事件序列的子 View (一个事件序列对应一个响应它的子View)
      • mFirstTouchTarget 指向链表首部
      • 当其绑定的 View 消费了当前的事件之后, 会调用 TouchTarget.recycle 从 mFirstTouchTarget 所在链表中移除且回收进 sRecycleLock 所在链表中

    分析 ViewGroup 对事件的分发

        /**
         * ViewGroup.dispatchTouchEvent
         * mFirstTouchTarget 是当前正在响应事件链表的表头
         */
        public boolean dispatchTouchEvent(MotionEvent ev) {
            // 1. 若为 ACTION_DOWN 则清除之前的状态, ACTION_DOWN 为整个序列事件的初始化事件
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // 在开始一个新的触摸手势时,抛弃所有以前的状态。
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }
            // 2.  判断是否需要当前 ViewGroup 拦截事件
            // 2.1 DOWN 事件会判断是否需要拦截
            // 2.2 当前事件序列已经之前有子 View 响应(mFirstTouchTarget != null) 判断是否需要拦截
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    // 调用自己的 onInterceptTouchEvent 方法, 判断是否拦截事件
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                // 没有触摸的目标, 并且不是初始化事件 ACTION_DOWN 了, 则直接将标记位设置为拦截
                intercepted = true;
            }
            // 3. 不是 ACTION_CANCEL 事件, 并且没有自己被拦截, 则开始寻找响应事件的子 View
            final boolean canceled = resetCancelNextUpFlag(this)
                    || actionMasked == MotionEvent.ACTION_CANCEL;
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
            if (!canceled && !intercepted) {
                // 只有 Down 事件才会去找寻响应事件的子 View
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    for (int i = childrenCount - 1; i >= 0; i--) {
                        final int childIndex = getAndVerifyPreorderedIndex(
                                childrenCount, i, customOrder);
                        final View child = getAndVerifyPreorderedView(
                                preorderedList, children, childIndex);
                        // 3.1 判断这个子 View 是否可以接收事件, 事件是否在它的区域内
                        if (!canViewReceivePointerEvents(child)
                                || !isTransformedTouchPointInView(x, y, child, null)) {
                            ev.setTargetAccessibilityFocus(false);
                            continue;
                        }
    
                        //... 走到这里说明在我们手指触摸的区域, 已经找到了一个子 View
    
                        // 3.2 判断当前响应事件序列的链表中, 这个 View 是否已经在响应一个事件序列了
                        // Sample: 当你一根手指按在了这个子 View 上, 令一根手指也按在了这个子View上时, 不会重新响应 Down 事件的
                        newTouchTarget = getTouchTarget(child);
                        if (newTouchTarget != null) {
                            // 若当前找到的 View 正在响应事件, 则跳出循环
                            newTouchTarget.pointerIdBits |= idBitsToAssign;
                            break;
                        }
                        // 3.3 找到的 View 没有在处理事件, 则通过 dispatchTransformedTouchEvent 将事件传递到这个子 View 中
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                            // ... 忽略部分代码, 只关注细节
                            // 3.3.1 走到这里, 说明事件成功分发给了子 View 并且被其成功消费了
                            // 3.3.2 调用 addTouchTarget 给 mFirstTouchTarget 赋值
                            // 3.3.3 如果处理掉了的话,将此 child 添加到 TouchTarget 链表的首部, 并让 mFirstTouchTarget -> 链表的首部
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            break;
                        }
                    }
    
                    // 4. 本次没找到响应该事件的 View, 但之前存在响应当前事件序列的 View (即当前 ViewGroup 的 mFirstTouchTarget 链不为 null)
                    // 例如我一开始一根手指按在了一个 Button 上, 后来我另一根手指按在了 ViewGroup 空白的地方, 那么就会走下面的逻辑
                    if (newTouchTarget == null && mFirstTouchTarget != null) {
                        // 遍历链表
                        newTouchTarget = mFirstTouchTarget;
                        while (newTouchTarget.next != null) {
                            newTouchTarget = newTouchTarget.next;
                        }
                        // while 结束后,newTouchTarget 指向了链表末尾的 TouchTarget 实例
                        newTouchTarget.pointerIdBits |= idBitsToAssign;
                    }
                }
    
            }
    
            if (mFirstTouchTarget == null) {
                // 5. mFirstTouchTarget == null 则说明没有任何子 View 消耗当前事件序列
                // 调用 dispatchTransformedTouchEvent 交由自己处理
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                // 6. 走到这里说明存在一个子 View 正在响应事件序列
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                // 遍历 mFirstTouchTarget 形成的链表中
                while (target != null) {
                    final TouchTarget next = target.next;
                    // 6.1 遍历到的 target 恰好为 newTouchTarget 直接标记为 handle, 一般 ACTION_DOWN 会进入到这个 if 里去, 因为 Down 事件的分发在上面进行
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                        // 判断是否需要向之前响应事件的子 view 发送 ACTION_CANCEL 事件
                        // 当前 ViewGroup 的父容器突然拦截了事件序列, 我们收到了 ACTION_CANCEL 事件
                        // 当前 ViewGroup 自己拦截了事件序列, 给子 View 分发 ACTION_CANCEL 事件
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        // 6.2 将事件继续分发给子 View, 用于分发 ACTION_DOWN 以外的事件
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true; // TouchTarget 链中任意一个处理了则设置 handled 为true
                        }
                        // 6.3 如果需要 cancelChild 的话,
                        // 则将此节点从 mFirstTouchTourget 所在的链表中回收到 TouchTarget.sRecycleBin 所在的链表中
                        if (cancelChild) {
                            if (predecessor == null) {
                                // 若表头需要被回收, 则移动表头到下一个位置
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target; // 访问下一个节点
                    target = next;
                }
            }
            return handle;
        }
        
        /**
         * ViewGroup.dispatchTransformedTouchEvent
         */
        private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
                View child, int desiredPointerIdBits) {
            final boolean handled;
            // ... 只关注核心部分
            if (child == null) {
                // 若子 View 为 null, 则回调当前 ViewGroup 父类的 dispatchTouchEvent 方法, 即自己处理
                handled = super.dispatchTouchEvent(transformedEvent);
            } else {
                // 若子 View 不为null, 则把事件传递给子 View 处理
                handled = child.dispatchTouchEvent(transformedEvent);
            }
            transformedEvent.recycle();
            return handled;
        }
        
        /**
         * View.dispatchTouchEvent 
         */
        public boolean dispatchTouchEvent(MotionEvent event) {
            // ... 只关注核心部分代码
            boolean result = false;
            if (onFilterTouchEventForSecurity(event)) {
                if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                    result = true;
                }
                //noinspection SimplifiableIfStatement
                ListenerInfo li = mListenerInfo;
                // 1. 先执行 onTouchListener
                if (li != null && li.mOnTouchListener != null
                        && (mViewFlags & ENABLED_MASK) == ENABLED
                        && li.mOnTouchListener.onTouch(this, event)) {
                    result = true;
                }
                // 2. onTouchListener 返回 false 会执行 onTouchEvent
                if (!result && onTouchEvent(event)) {
                    result = true;
                }
            }
            return result;
        }
    

    好了事件的分发到此出就结束了, 所有的重点还是 ViewGroup 中的 dispatchTouchEvent 这个方法中, 尤其是对 TouchTarget 链表的理解, 简单的总结一下

    ViewGroup.dispatchTouchEvent 几个关键点:

    1. 判断是否需要拦截这个事件

      • 事件为 ACTION_DOWN 会判断是否需要拦截
      • 当前 ViewGroup 已经有子 View 消费事件序列了, 即 mFirstTarget != null 会判断是否需要拦截
    2. 若为 ACTION_DOWN 事件: 找寻能够响应这个事件的 View, 即给 newTouchTarget 赋值

      • 若找到子 View 且没有处理其他事件, 则调用 dispatchTransformedTouchEvent 分发给子 View 处理这个事件, 处理成功则给 newTouchTarget 赋值且绑定这个 View, 最后将 newTouchTarget 链入表头, 更新表头 mFirstTouchTaget 的值
      • 若没找到子 View 或者子 View 正在处理其他事件
        • mFirstTouchTarget != null : newTouchTarget 为 mFirstTouchTarget 链表最后一个元素
        • mFirstTouchTarget == null: 调用 dispatchTransformedTouchEvent 自己处理
    3. 若为 ACTION_DOWN 以外的其他事件:

      • 事件被当前 ViewGroup 拦截: 给响应这个事件的子 View 分发 ACTION_CANCEL, 且将其对应的 TouchTarget 对象, 从链表中移除
      • 事件未被当前 ViewGroup 拦截: 调用 dispatchTransformedTouchEvent 正常分发

    View.dispatchTouchEvent 中的关键点:

    如果设置了 onTouchListener 并且返回了 true, 是不会回调 onTouchEvent 方法的

    具体的细节源码中的注释写的很详细, 就不再赘述了

    View 的 onTouchEvent 事件

        public boolean onTouchEvent(MotionEvent event) { // View对touch事件的默认处理逻辑
            final float x = event.getX();
            final float y = event.getY();
            final int viewFlags = mViewFlags;
            final int action = event.getAction();
    
            // 判断是否可响应点击
            final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                    || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
             // DISABLED 的状态下
            if ((viewFlags & ENABLED_MASK) == DISABLED) {
                // 如果之前是PRESSED状态则复原
                if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                    setPressed(false);
                }
                mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                // Disable 的视图依旧可以响应触摸事件, 只不过它是以无响应的方式
                // 只不过是以无响应的方式处理了
                return clickable;
            }
            // 如果有 TouchDelegate 的话,优先交给它处理
            if (mTouchDelegate != null) {
                if (mTouchDelegate.onTouchEvent(event)) { 
                    return true; // 处理了返回 true,否则接着往下走
                }
            }
            
            if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
                switch (action) {
                    case MotionEvent.ACTION_UP:
                        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                        if ((viewFlags & TOOLTIP) == TOOLTIP) {
                            handleTooltipUp();
                        }
                        // 若为不可点击的则移除相关回调, 直接结束对 ACTION_UP 的处理
                        if (!clickable) {
                            removeTapCallback();
                            removeLongPressCallback();
                            mInContextButtonPress = false;
                            mHasPerformedLongPress = false;
                            mIgnoreNextUpEvent = false;
                            break;
                        }
                        // 如果外围有可以滚动的parent的话,当按下时会设置这个标志位
                        boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                            // 设置了FocusableInTouchMode后,View在点击的时候就会
                            // 尝试requestFocus(),并将focusToken设置为true
                            boolean focusTaken = false;
                            if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                                focusTaken = requestFocus();
                            }
    
                            if (prepressed) {
                                // 确保用户能够看到按钮的Press状态
                                setPressed(true, x, y);
                            }
    
                            if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) { 
                                // 如果没有长按发生的话, 移除长按 Callback
                                removeLongPressCallback();
    
                                // focusTaken 为 false 才执行下面的对 onClick 的回调
                                // 若本次事件请求了焦点(focusTaken 为 true), 则不会执行 onClick
                                if (!focusTaken) {
                                    // 优先使用 post 去处理点击事件, 这样可以让视觉状态在 performClick 之前呈现
                                    if (mPerformClick == null) {
                                        mPerformClick = new PerformClick();
                                    }
                                     // 如果 post 失败了,则直接调用 performClick() 方法
                                    if (!post(mPerformClick)) {
                                        performClick();
                                    }
                                }
                            }
    
                            if (mUnsetPressedState == null) {
                                mUnsetPressedState = new UnsetPressedState();
                            }
    
                            if (prepressed) {
                                postDelayed(mUnsetPressedState,
                                        ViewConfiguration.getPressedStateDuration());
                            } else if (!post(mUnsetPressedState)) {
                                // If the post failed, unpress right now
                                mUnsetPressedState.run();
                            }
    
                            removeTapCallback();
                        }
                        mIgnoreNextUpEvent = false;
                        break;
    
                    case MotionEvent.ACTION_DOWN:
                        if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                            mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                        }
                        mHasPerformedLongPress = false;
    
                        if (!clickable) {
                            checkForLongClick(0, x, y);
                            break;
                        }
    
                        if (performButtonActionOnTouchDown(event)) {
                            break;
                        }
                        // 判断是否在可以滚动的容器中
                        boolean isInScrollingContainer = isInScrollingContainer();
                        if (isInScrollingContainer) {
                            mPrivateFlags |= PFLAG_PREPRESSED; // 设置 PREPRESSED 标志位
                            if (mPendingCheckForTap == null) {
                                mPendingCheckForTap = new CheckForTap();
                            }
                            mPendingCheckForTap.x = event.getX();
                            mPendingCheckForTap.y = event.getY();
                            // 延迟反馈
                            postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                        } else {
                            // 不在可以滚动的容器中, 则直接反馈
                            setPressed(true, x, y);
                            checkForLongClick(0, x, y);
                        }
                        break;
    
                    case MotionEvent.ACTION_CANCEL:
                        // 事件被父容器拦截了不再下发, 则清除回调和标记位
                        if (clickable) {
                            setPressed(false);
                        }
                        removeTapCallback();
                        removeLongPressCallback();
                        mInContextButtonPress = false;
                        mHasPerformedLongPress = false;
                        mIgnoreNextUpEvent = false;
                        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                        break;
    
                    case MotionEvent.ACTION_MOVE:
                        if (clickable) {
                            drawableHotspotChanged(x, y);
                        }
                        // 判断是否移动到了当前 View 的界外
                        if (!pointInView(x, y, mTouchSlop)) {
                            // 移除回调
                            removeTapCallback();
                            removeLongPressCallback();
                            if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                                setPressed(false);
                            }
                            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                        }
                        break;
                }
                // 只要能进来就会返回 true
                return true;
            }
    
            return false;
        }
    

    源码注释很详细, 有几个需要关注的点:

    1. 若不想让子View消耗事件, 就必须
      • clickable 为 false
      • (viewFlags & TOOLTIP) != TOOLTIP
        // 判断是否可响应点击
        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
    
    1. 如果当前 UP 事件执行了 requestFocus 则不会响应点击事件

    相关文章

      网友评论

        本文标题:View—事件分发

        本文链接:https://www.haomeiwen.com/subject/pbysaxtx.html