美文网首页
View·dispatchTouchEvent 源码分析(三)

View·dispatchTouchEvent 源码分析(三)

作者: 幺鹿 | 来源:发表于2017-03-04 18:17 被阅读482次

    上节概述


    1、在 Activity 中的事件都会按照先交由 Activity 中处理 dispatchTouchEvent 。
    2、然后在 Activity 会将事件上抛给 DecorView 的 superDispatchTouchEvent 去处理。
    3、而实际上 superDispatchTouchEvent 方法调用的是 ViewGroup 的 dispatchTouchEvent 方法。

    所以真正意义上的 dispatchTouchEvent 的过程即将真正开始。

    查看上节文章View·从 InputEvent 到 dispatchTouchEvent 源码分析(二)

    俯瞰 dispatchTouchEvent 方法


    先从整体的角度看下 dispatchTouchEvent 方法,它提供了:

    • 对输入的 MotionEvent 事件的校对;
    • 辅助相关功能;
    • 事件分发;
    • 返回值 handled;
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        // 1、输入事件的一致性校对
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
        }
        // 2、若辅助功能已开启则正常启动事件分发,子类会捕获点击事件。
        if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
                ev.setTargetAccessibilityFocus(false);
        }
    
        boolean handled = false;
        // 3、应用安全策略过滤触摸事件
        if (onFilterTouchEventForSecurity(ev)) {
            // 4、事件分发过程
        }
    
        if (!handled && mInputEventConsistencyVerifier != null)       
        {
        // 5、输入一致性校对取消事件追踪
            mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
        }
        // 6、返回值 handled
        return handled;
    }
    

    过滤事件,有选择的分发事件


    当窗口被遮蔽时事件将会被过滤,即意味着不会执行事件分发。

       /**
         * 应用安全策略过滤触摸事件
         *
         * @param event 即将被过滤的事件对象
         * @return 当事件允许被分发时返回 TRUE,否则为FALSE。
         *
         * @see #getFilterTouchesWhenObscured
         */
    public boolean onFilterTouchEventForSecurity(MotionEvent event) {
            //noinspection RedundantIfStatement
        if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0 && (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
                // 当 Window 被遮蔽时,则事件将会被丢弃。
                return false;
        }
        return true;
    }
    

    事件分发


    因为事件分发的代码比较长,所以会按从上到下拆解代码进行分析。每段功能的代码段作为一个小节单独分析。

    1、判断是否是新的事件流

    1.1 ViewGroup#dispatchTouchEvent()

    在代码中认定 ACTION_DOWN 是一段连续事件的开始的标志,所以代码接收到 ACTION_DOWN 事件时,会立即执行重置状态操作。

    由于应用程序切换、ANR、或一些其他状态更改,框架可能会丢弃ACTION_UPACTION_CANCEL事件。

    final int action = ev.getAction();
    final int actionMasked = action & MotionEvent.ACTION_MASK;
    
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            cancelAndClearTouchTargets(ev);
            resetTouchState();
        }
    

    1.2 ViewGroup#cancelAndClearTouchTargets()

    cancelAndClearTouchTargets 方法执行了首先检查了 mFirstTouchTarget 对象是否为空。如果 mFirstTouchTarget 不为空,意味着之前的事件流未清空。如果 mFirstTouchTarget 为空,也即意味着之前无事件流,所以也就无需再次释放了。

    mFirstTouchTarget不为空为前提情况下传入的 eventnull,则会人工构建MotionEvent 对象并将事件类型设置为 ACTION_CANCEL 。有了事件对象后,接着会遍历 TouchTarget的链,执行取消事件派发。在派发完成之后,同时清空 TouchTarget的链。

        private void cancelAndClearTouchTargets(MotionEvent event) {
            if (mFirstTouchTarget != null) {
                boolean syntheticEvent = false;
                if (event == null) {
                    // 人造事件
                    final long now = SystemClock.uptimeMillis();
                    event = MotionEvent.obtain(now, now,
                            MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
                    event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
                    syntheticEvent = true;
                }
                // 遍历派发事件
                for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
                    resetCancelNextUpFlag(target.child);
                    dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
                }
                clearTouchTargets();
                // 如果是人造事件,则及时释放
                if (syntheticEvent) {
                    event.recycle();
                }
            }
        }
    

    在这里我们观察到事件的派发似乎与TouchTargetdispatchTransformedTouchEvent()都有着紧密的关联。
    取消事件的派发是通过 遍历 TouchTarget 的链,依次派发dispatchTransformedTouchEvent() 完成的。

    所以我们跟进 TouchTargetdispatchTransformedTouchEvent() 这两个方法,看看事件派发的细节具体是如何实现的。

    1.3 TouchTarget

    TouchTarget 内部提供了大小为32的可重用的链式结构,它把与触摸相关的目标( 内部就是View)串联成起来,以便于后续的事件分发。

    private static final class TouchTarget {
            private static final int MAX_RECYCLED = 32;
            private static final Object sRecycleLock = new Object[0];
            private static TouchTarget sRecycleBin;
            private static int sRecycledCount;
    
            public static final int ALL_POINTER_IDS = -1; // all ones
    
            // 可供触摸的子视图
            public View child;
    
            // 合并后的位掩码,
            // The combined bit mask of pointer ids for all pointers captured by the target.
            public int pointerIdBits;
    
            // 列表中的下一个 target
            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--;
                        target.next = null;
                    }
                }
                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 = sRecycleBin;
                        sRecycleBin = this;
                        sRecycledCount += 1;
                    } else {
                        next = null;
                    }
                    child = null;
                }
            }
        }
    

    1.4 添加 TouchTarget

    每次调用 addTouchTarget时,内部都会创建(或重用)TouchTarget对象,然后将该TouchTarget对象设置为列表的头部。

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

    1.5 谁调用了 addTouchTarget

    追述到ViewGroup#dispatchTouchEvent()方法,发现ViewGroup遍历了它 子视图们,并将它们构建成链。那么了解子视图们与构建出的串的对应关系是件重要的事情。

    // ViewGroup#dispatchTouchEvent
    final int childrenCount = mChildrenCount;
    if (newTouchTarget == null && childrenCount != 0) {
                final float x = ev.getX(actionIndex);
                final float y = ev.getY(actionIndex);
                            ...
                final View[] children = mChildren;
    
                for (int i = childrenCount - 1; i >= 0; i--) {
                                    ...
                     if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                    ...
                        mLastTouchDownX = ev.getX();
                        mLastTouchDownY = ev.getY();
                        newTouchTarget = addTouchTarget(child, idBitsToAssign);
                        alreadyDispatchedToNewTouchTarget = true;
                        break;
                      }
                                ...
                }           
    }         
    

    1.6 第一次事件(ACTION_DOWN)的分发过程

    为了不偏离事件分发的主线,我们省略对 ViewTree 的代码分析。我们观察到上述代码在处理ACTION_DOWN事件时候得到执行机会,也就是在收到ACTION_DOWN事件时,框架会遍历View[]。而View[]的初始化可追述到addView()方法。

    所以上段代码最终表达了一件事情:遍历View[],并依据 View 拦截事件的返回值构建出一条处理链。重点请看方法dispatchTransformedTouchEvent()

    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
                View child, int desiredPointerIdBits) {
    final boolean handled;
          ... 
    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 {
              ... 
            }
          ... 
    }
    

    若已无父子关系(ViewGroup 中是没有 View)则由自身处理事件分发,否则交由子视图决定是否需要分发事件。

    1.6.1 第一次事件(ACTION_DOWN)的分发过程之分发到 View

    resulttrue/false,决定 View 是否能收到后续的手势事件,看到下方有几处会影响result取值的地方。

    • onFilterTouchEventForSecurity(event)
    • (mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)
    • li.mOnTouchListener.onTouch(this, event)
    • onTouchEvent(event)

    我们也就是为什么我们常用的 onTouchEvent(event)setTouchListener()方法能影响到事件分发的根本原因。

    public boolean dispatchTouchEvent(MotionEvent event) {
            ... 
            boolean result = false;
            ...
            final int actionMasked = event.getActionMasked();
            ...
            if (onFilterTouchEventForSecurity(event)) {
                if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                    result = true;
                }
                //noinspection SimplifiableIfStatement
                ListenerInfo li = mListenerInfo;
                if (li != null && li.mOnTouchListener != null
                        && (mViewFlags & ENABLED_MASK) == ENABLED
                        && li.mOnTouchListener.onTouch(this, event)) {
                    result = true;
                }
    
                if (!result && onTouchEvent(event)) {
                    result = true;
                }
            }
            ...
            return result;
        }
    

    1.6.2 第一次事件(ACTION_DOWN)的分发过程之分发到 ViewGroup

     @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
    
        boolean handled = false;
        ...
        if (onFilterTouchEventForSecurity(ev)) {
    
                // 检查是否拦截
                final boolean intercepted;
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || mFirstTouchTarget != null) {
                    // ViewGroup 可以设置不接受拦截事件
                    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 {
                    // There are no touch targets and this action is not an initial down
                    // so this view group continues to intercept touches.
                    intercepted = true;
                }
        }
        // 因为已经被拦截,所以 ACTION_DOWN 事件不会继续向下分发。
        if (!canceled && !intercepted) {
               ... 
        }  
    
       // 因为没有执行 ACTION_DOWN 事件,所以 mFirstTouchTarget 也就没有得到初始化的机会。即最后由被拦截的 ViewGroup 自身处理的相关事件。
       if (mFirstTouchTarget == null) {
           // 没有mFirstTouchTarget,所以可以把自身当作一个普通视图。
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
            TouchTarget.ALL_POINTER_IDS);
       } else {
                  ...
       }
    }
    

    1.6.3 ACTION_DOWN 事件分发小结

    至此为止,ACTION_DOWN 事件分发的轮廓已经描绘出来。从DecorView到具体的得到焦点的 View(如果中途被拦截,那就是拦截的 View),通过ACTION_DOWN 事件确定了一个 TouchTarget 分发链,那么接续的事件都将通过这条分发链进行处理。

    图例说明

    ACTION_DOWN 事件派发 确认 TouchTarget 的链 事件派发时

    本章总结

    1、由 Stage 调用 DecorView.dispatchTouchEvent
    2、DecorView 继承 ViewGroup 且未覆写 dispatchTouchEvent 方法,所以交由 ViewGroup的dispatchTouchEvent 进行处理。
    3、首先判断是否是 ACTION_DOWN 事件,若是则展开初始化、释放等操作
    4、其次判断是否需要 ViewGroup 拦截事件

    4.1、若需要拦截,则 ViewGroup 视作 View 去处理派发。
    4.2、若不需要拦截,则遍历 ViewGroup 下的子视图,依次询问子视图是否处理 dispatchTouchEvent,并将询问的轨迹以 TouchTarget 链的形式作记录。
    4.3、直到确认到有子视图处理时,及终止 ACTION_DOWN 事件的派发。

    5、接着事件会逐层 return ,直到回朔到DecorView的实例中。接着由DecorView的实例控制mFirstTouchTarget对象展开事件分发。
    6、…… 接下去的内容我们下一章节在续

    相关文章

      网友评论

          本文标题:View·dispatchTouchEvent 源码分析(三)

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