前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >View·dispatchTouchEvent 源码分析(三)

View·dispatchTouchEvent 源码分析(三)

作者头像
幺鹿
发布2018-08-21 15:43:02
8890
发布2018-08-21 15:43:02
举报
文章被收录于专栏:Java呓语Java呓语

上节概述


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

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

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

俯瞰 dispatchTouchEvent 方法


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

  • 对输入的 MotionEvent 事件的校对;
  • 辅助相关功能;
  • 事件分发;
  • 返回值 handled;
代码语言:javascript
复制
@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;
}

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


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

代码语言:javascript
复制
   /**
     * 应用安全策略过滤触摸事件
     *
     * @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事件。

代码语言:javascript
复制
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的链。

代码语言:javascript
复制
    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)串联成起来,以便于后续的事件分发。

代码语言:javascript
复制
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对象设置为列表的头部。

代码语言:javascript
复制
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遍历了它 子视图们,并将它们构建成链。那么了解子视图们与构建出的串的对应关系是件重要的事情。

代码语言:javascript
复制
// 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()

代码语言:javascript
复制
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()方法能影响到事件分发的根本原因。

代码语言:javascript
复制
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
代码语言:javascript
复制
 @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、…… 接下去的内容我们下一章节在续

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2017.03.04 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 上节概述
  • 俯瞰 dispatchTouchEvent 方法
  • 过滤事件,有选择的分发事件
  • 事件分发
    • 1、判断是否是新的事件流
      • 1.1 ViewGroup#dispatchTouchEvent()
      • 1.2 ViewGroup#cancelAndClearTouchTargets()
      • 1.3 TouchTarget
      • 1.4 添加 TouchTarget
      • 1.5 谁调用了 addTouchTarget
      • 1.6 第一次事件(ACTION_DOWN)的分发过程
  • 图例说明
  • 本章总结
相关产品与服务
腾讯云代码分析
腾讯云代码分析(内部代号CodeDog)是集众多代码分析工具的云原生、分布式、高性能的代码综合分析跟踪管理平台,其主要功能是持续跟踪分析代码,观测项目代码质量,支撑团队传承代码文化。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档