Android事件分发与事件处理源码分析

2023-05-16

一、前言

Android中事件分发与事件处理是一个老生常谈的问题了,自己在网上也看过很多文章,但是大部分人都只是抛出一些结论或是一些流程图或者干脆就是一些运行demo的截图等,对于这些结论和流程图是怎么来的,其实并没有解释太清楚,一段时间之后,这些结论也只是结论,至于为什么会有这样的结论脑子里一片空白,其实所有结论源码中都给了解释,今天从源码及结论两个方面进行代码梳理,方便加深记忆。

结论一:触摸事件从Activity分发下来,经过PhoneWindow、DecorView到达ViewGruop层面。

看源码之前先来简单复习一下Android页面层级结构:

Android页面层级结构

1. Activity

Activity作为四大组件之一,是平常使用度很高的一个类。如果你仔细观察会发现,这个类既不继承View又不继承ViewGroup,而是实现了Window.Callback, KeyEvent.Callback, OnCreateContextMenuListener等一堆接口,它承担的更多的是监听的工作,称为控制器更合适,可以理解为用户触摸屏幕事件就是从该类下发下来的。

2. window

window:最顶级的窗口外观基类,只有一个实现类PhoneWindow。

3.decorView

当前窗口最顶层的View,是所有View的根节点,即当前Activity视图树的根节点,其父类为FrameLayout。

用一张图描述一下各个组件的涵盖关系:

  

下面我们就从源码角度看一下,触摸屏幕事件是如何从Activity一级一级分发下去的。

二、Activity中事件分发与处理

  Activity.java
  //Activity中事件分发
  public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }
  //Activity中事件处理  
  public boolean onTouchEvent(MotionEvent event) {
       if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
       }
        return false;
    }
复制代码

在Activity的dispatchTouchEvent()中,如果getWindow().superDispatchTouchEvent(ev)返回true,方法到此就结束了,如果返回false,则会调用Activity的onTouchEvent()。在Activity的onTouchEvent()中,如果window在触摸屏幕时应该关闭,则Activity的onTouchEvent()返回true,并且finish,否则返回fasle。

PhoneWindow.java

@Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }
复制代码

在PhoneWindow中,其superDispatchTouchEvent()内部调用了DecorView的superDispatchTouchEvent()

Decorview.java

public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }
复制代码

在DecorView中,superDispatchTouchEvent()方法调用了其父类FrameLayout的dispatchTouchEvent(),FrameLayout中没有提供该方法,该方法的实际提供者是ViewGruop,到此我们知道了MotionEvent如何从Activity一步步传递到了ViewGruop中。

我们在看下其它比较常见的结论:

结论二:子View不消费down事件(onTouch()返回false或onTouchEvent()返回false),move、up事件不会再往该view分发,父View触发onTouch或OnTouchEvent事件。

结论三:子View消费down事件,后续move、up事件会直接发向该view(不考虑拦截)。

结论四:子View消费down事件,move、up不被拦截但是不消费move、up事件时,不会触发父View onTouchEvent()

结论五:子View消费down事件,move、up被拦截会收到来自父View的cancel事件,move、up会发送给父View,触发父View onTouchEvent()

下面从源码中找一下,为什么会有这样的结论。

三、ViewGruop中事件分发与处理

ViewGroup.java

 @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
       
        ......省略部分代码
        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            // Handle an initial down.  
            //每次接收到down事件,清除所有状态,最重要的是把mFirstTouchTarget置为null
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // Throw away all previous state when starting a new touch gesture.
                // The framework may have dropped the up or cancel event for the previous gesture
                // due to an app switch, ANR, or some other state change.
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }

            // Check for interception.
            //校验是否拦截touch事件
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {子view允许父view拦截
                    intercepted = onInterceptTouchEvent(ev);//是否拦截由onInterceptTouchEvent(ev)方法决定
                    ev.setAction(action); // restore action in case it was changed
                } else {//requestDisallowInterceptTouchEvent()设置为true,子view禁止父view拦截
                    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;
            }
            ......省略部分代码
            // Check for cancelation.
            final boolean canceled = resetCancelNextUpFlag(this)
                    || actionMasked == MotionEvent.ACTION_CANCEL;

            // Update list of touch targets for pointer down, if needed.
            final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
            if (!canceled && !intercepted) {

                // If the event is targeting accessibility focus we give it to the
                // view that has accessibility focus and if it does not handle it
                // we clear the flag and dispatch the event to all children as usual.
                // We are looking up the accessibility focused host to avoid keeping
                // state since these events are very rare.
                View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                        ? findChildWithAccessibilityFocus() : null;

                //down事件下发逻辑
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    final int actionIndex = ev.getActionIndex(); // always 0 for down
                    final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                            : TouchTarget.ALL_POINTER_IDS;

                    // Clean up earlier touch targets for this pointer id in case they
                    // have become out of sync.
                    removePointersFromTouchTargets(idBitsToAssign);

                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        final float x = ev.getX(actionIndex);
                        final float y = ev.getY(actionIndex);
                        // Find a child that can receive the event.
                        // Scan children from front to back.
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        //倒序遍历ViewGroup的子view,如果view所在区域处于触摸点坐标区域,
                        //则调用dispatchTransformedTouchEvent()方法
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);

                            // If there is a view that has accessibility focus we want it
                            // to get the event first and if not handled we will perform a
                            // normal dispatch. We may do a double iteration but this is
                            // safer given the timeframe.
                            if (childWithAccessibilityFocus != null) {
                                if (childWithAccessibilityFocus != child) {
                                    continue;
                                }
                                childWithAccessibilityFocus = null;
                                i = childrenCount - 1;
                            }

                            if (!child.canReceivePointerEvents()
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }

                            newTouchTarget = getTouchTarget(child);
                            if (newTouchTarget != null) {
                                // Child is already receiving touch within its bounds.
                                // Give it the new pointer in addition to the ones it is handling.
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }

                            resetCancelNextUpFlag(child);
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {//知识点二,找到了要处理touch事件的view,newTouchTarget、alreadyDispatchedToNewTouchTarget赋值
                                mLastTouchDownTime = ev.getDownTime();
                                if (preorderedList != null) {
                                    // childIndex points into presorted list, find original index
                                    for (int j = 0; j < childrenCount; j++) {
                                        if (children[childIndex] == mChildren[j]) {
                                            mLastTouchDownIndex = j;
                                            break;
                                        }
                                    }
                                } else {
                                    mLastTouchDownIndex = childIndex;
                                }
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);//找到要分发的view,mFirstTouchTarget设置值
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }

                            // The accessibility focus didn't handle the event, so clear
                            // the flag and do a normal dispatch to all children.
                            ev.setTargetAccessibilityFocus(false);
                        }
                        if (preorderedList != null) preorderedList.clear();
                    }

                    ......省略部分代码
                }
            }

            // 分发给触摸view
            if (mFirstTouchTarget == null) {
                //没有view要处理该touch事件,viewGroup自己处理,注意第三个参数为null
                //知识点三
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                //move、up事件下发逻辑
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;//上面执行过down事件,handled直接设置为true
                    } else {
                        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;
                }
            }

            // Update list of touch targets for pointer up or cancel, if needed.
            if (canceled
                    || actionMasked == MotionEvent.ACTION_UP
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                resetTouchState();//如果是up、cancel,重置触摸状态,mFirstTouchTarget设为null
            } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
                final int actionIndex = ev.getActionIndex();
                final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
                removePointersFromTouchTargets(idBitsToRemove);
            }
        }

        if (!handled && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
        }
        return handled;
    }
复制代码

源码中标识知识点一:判定条件意味着当前事件不是down事件,且viewGroup内没有子view要处理事件,intercepted变量设置为true,对move、up等事件进行拦截,这也就是结论二的原因。

知识点二、三、四调用了同一个方法:dispatchTransformedTouchEvent(),先看一下这个方法,在分析二、三、四处的作用有何不同。

   ViewGroup.java
   
   private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;

        // Canceling motions is a special case.  We don't need to perform any transformations
        // or filtering.  The important part is the action, not the contents.
        final int oldAction = event.getAction();
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {//事件类型为cancel
            event.setAction(MotionEvent.ACTION_CANCEL);
            if (child == null) {
                handled = super.dispatchTouchEvent(event);//调用父类View的dispatchTouchEvent()
            } else {
                handled = child.dispatchTouchEvent(event);//调用child.dispatchTouchEvent()、child可能为view也可能为viewGroup
            }
            event.setAction(oldAction);
            return handled;
        }

        // Calculate the number of pointers to deliver.
        final int oldPointerIdBits = event.getPointerIdBits();
        final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;

        // If for some reason we ended up in an inconsistent state where it looks like we
        // might produce a motion event with no pointers in it, then drop the event.
        if (newPointerIdBits == 0) {
            return false;
        }
        ......省略部分代码
        // Perform any necessary transformations and dispatch.
        if (child == null) {
            handled = super.dispatchTouchEvent(transformedEvent);//调用父类View的dispatchTouchEvent()
        } else {
            final float offsetX = mScrollX - child.mLeft;
            final float offsetY = mScrollY - child.mTop;
            transformedEvent.offsetLocation(offsetX, offsetY);
            if (! child.hasIdentityMatrix()) {
                transformedEvent.transform(child.getInverseMatrix());
            }

            handled = child.dispatchTouchEvent(transformedEvent);//调用child.dispatchTouchEvent()、child可能为view也可能为viewGroup
        }

        // Done.
        transformedEvent.recycle();
        return handled;
    }
复制代码

通过查阅源码,我们发现dispatchTransformedTouchEvent()的返回值分两种情况

  1. child为null,会调用ViewGroup的父类的dispatchTouchEvent()方法,即View的dispatchTouchEvent()
  2. child不为null,将触摸点坐标变换至子view坐标系,并且调用子View的dispatchTouchEvent()

知识点二如果返回true,则证明child即为消费down事件的子View,调用addTouchTarget()为mFirstTouchTarget赋值,后续的move、up事件会走知识点四,如果返回false,则证明不存在消费down事件的子View,后续的move、up会走知识点三,也就是把ViewGroup视为View,调用View的dispatchTouchEvent()。这也正是结论三的原因。

四、View中事件分发与处理

View.java

  /**
     * Pass the touch screen motion event down to the target view, or this
     * view if it is the target.
     *
     * @param event The motion event to be dispatched.
     * @return True if the event was handled by the view, false otherwise.
     */
    public boolean dispatchTouchEvent(MotionEvent event) {
        ......省略部分代码
        boolean result = false;
        ......省略部分代码
        if (onFilterTouchEventForSecurity(event)) {
            ......省略部分代码
            
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {//如果View设置了OnTouchListener()并且onTouch()回调返回true
                result = true;
            }

            if (!result && onTouchEvent(event)) {//如果onTouchEvent()返回true
                result = true;
            }
        }

        ......省略部分代码

        return result;
    }
复制代码

dispatchTouchEvent()中会先验证View是否设置过触摸监听调用SetOnTouchListener()且当前view是否已经启用且其onTouch()是否true。

  • 如果前面条件均满足,则result设置为true,跳过onTouchEvent(event)的执行,结束事件分发
  • 否则,调用onTouchEvent(event),如果onTouchEvent(event)返回true,result设置为true,结束事件分发。
View.java

 public boolean onTouchEvent(MotionEvent event) {
        ......
        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    .....
                    break;
                case MotionEvent.ACTION_DOWN:
                    ......
                    break;
                case MotionEvent.ACTION_CANCEL:
                    ......
                    break;
                case MotionEvent.ACTION_MOVE:
                    break;
            }
            return true;
        }
        return false;
    }
复制代码

只要view是可点击或悬停、长摁时提示tooltip,onTouchEvent(MotionEvent event)一定返回true,否则返回false。

如果down返回true,move或up返回false,则down后面的事件会因为mFirstTouchTarget !=null进入知识点四,导致父View无法执行知识点二,这是结论四的原因。

如果down返回true,down后续事件被父View拦截,会导致知识点四参数二为true,mFirstTouchTarget重置为null, 即子view接收到cancel事件,父View触发知识点二调用onTouch或onTouchEvent()。这是结论五的原因。

但是知识点三的代码已经走过了,即使把mFirstTouchTarget重置为null,知识点二的代码也不会走了,那么系统是在何时又触发了知识点二的执行呢?答案是:下个事件到来时,如果父View拦截的是move事件,等到第二个move到来时会触发父View的move、up;如果父View拦截的是up事件,则父View也不会执行onTouchEvent(),因为只有一个up事件。

附上验证截图兩张:

  可以看到,只拦截up事件的父View,不会触发自身onTouchEvent()的执行。

可以看到,拦截move事件的父View,会触发自身onTouchEvent()的执行。

五、调用关系梳理

在不进行事件拦截、不调用SetOnTouchListener()、没有多个父布局的前提下 触摸事件从Activity下发至View流程:

 

上图中,调用child的dispatchTouchEvent()时,如果child属于ViewGroup类型,依然会调用ViewGruop的dispatchEvent()方法,直至最后一层view视图,才会调用view.dispatchEvent()。

ViewGroup在调用dispatchEvent()进行事件分发时,程序会在分发事件给child后处于停滞状态,直到child执行完dispatchEvent()方法并返回Boolean类型数值为止,如果存在多个层级的ViewGroup,会逐层递归调用并等待结果,直到最后一层view。最后一层的view作为事件分发的终点,恰好是事件处理的起点。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Android事件分发与事件处理源码分析 的相关文章

  • 当字符串位于数组中时,如何替换字符串中的最后一个字符

    如果某个字符串以某个字符结尾arrayOf X Y Z 我想用新字符替换它A 我不知道该怎么做 我尝试过的一切都不起作用 你可以这样做 var test Some string Z if test lastOrNull in arrayOf
  • Android 自定义对话框因布局而膨胀 - 对齐问题

    我有一个自定义对话框 它是从dialog xml 中膨胀的 当我打开对话框时 它看起来如下所示 我在列表视图和它下面的 确定 按钮之间有一些空间 我想消除列表视图与其下方的按钮之间的空间 我怎样才能做到这一点 对话框 xml
  • 使用 iOS 分布式应用程序时 Google Cloud Messaging 显示“notRegistered”

    我在 iOS 应用程序上实现了 GCM 服务 我使用 PHP 在服务器上发送 GCM 当应用程序由开发配置文件签名时 它可以完美运行 也就是说 当应用程序使用 GCM 配置注册自身时 它始终返回一个正常运行的设备令牌 我可以使用令牌向设备发
  • 如何使用 ArrayAdapter

    ArrayList
  • 加载内容时在 ImageView 中使用“动画圆圈”

    我目前在我的应用程序中使用一个列表视图 可能需要一秒钟才能显示 我目前所做的是使用列表视图的 id android empty 属性来创建 正在加载 文本
  • 如何将 ThreeJS 与 PhoneGap 一起使用?

    这个探索是非常自我描述的 我已经用一个简单的 3D 立方体进行了测试 它在浏览器中运行良好 但只在模拟器中显示空白页面 有人说 Threejs 不能与 PhoneGap 一起使用 但也有人说他们使用过并且工作正常 在 Android 中 您
  • 如何计算android中位图擦除区域的百分比?

    我是安卓新手 我正在制作一个可以使用手指擦除画布上的位图的应用程序 像手指画橡皮擦之类的东西 我想计算擦除区域的百分比 例如 60 已从完整图像中擦除 请帮助我做到这一点 提前致谢 我尝试了一些方法 它总是给我 0 它不起作用 请参阅该方法
  • 为什么 Android Eclipse 不断刷新外部文件夹并花费很长时间?

    我只有一部新的 Android 手机 我一直在修补一些基本的应用程序 每当我保存任何内容时 Eclipse 的 Android 插件就会刷新外部文件夹 这让我抓狂 通常我不会介意 但当需要 10 秒才能刷新时 我开始注意到 我已经搜索过 其
  • Android Studio,工具提示消失得这么快

    我有以下问题 我想从这个工具提示中复制错误文本 但是一旦我将鼠标悬停在它上面 它就消失得如此之快 这让我发疯 我有以下 android studio 版本 我有以下设置 谢谢您的帮助 如果有人遇到这个问题 这与logcat刷新的方式有关 每
  • 没有 GUI 的 Android Activity

    我创建了一个仅从链接启动的活动 使用意图过滤器 我不希望此活动有 GUI 我只希望它启动服务并在栏中放置通知 我尝试将链接的意图过滤器放入我的服务中 但这不起作用 有没有更好的方法可以响应意图过滤器 或者我可以让我的活动没有 GUI 吗 抱
  • Eclipse Oxygen - 该项目未构建,因为其构建路径不完整

    我刚刚安装了 Eclipse Oxygen 并尝试在工作台中打开现有项目 但收到此错误 该项目未构建 因为其构建路径不完整 不能 找到 java lang Object 的类文件 修复构建路径然后尝试 建设这个项目 我尝试右键单击该项目 转
  • 在 Android 模拟器中获取互联网连接

    我有一台带有wifi连接的台式电脑 我的IP地址是192 168 12 95 网关是192 168 10 10 但是我在android模拟器中没有获得互联网连接 也就是说我无法访问internate 我也尝试过 emulator avd w
  • Facebook 好友请求 - 失踪好友

    我请求从我正在开发的 Android 应用程序中获取用户好友 从 Facebook Api V2 0 开始 我知道我应该只获取已经通过我的应用程序登录的用户好友 但是 尽管我知道用户的某些朋友已通过我的应用程序登录 但在请求该用户的朋友时
  • Firestore OncompleteListener [重复]

    这个问题在这里已经有答案了 我想看看这段代码的执行有什么错误 当我编译它时 它只返回 log 1 3 2 的值 并且我希望 log2 在 3 之前 Log d 1 antes de validar DocumentReference doc
  • 从前台服务的活动中释放内存

    我有一个带有前台服务和一项活动的应用程序 该服务可以在启动时自行启动 也可以从 Activity 中启动 我注意到当服务在启动时自行启动时 内存使用量约为 3MB 一旦我打开该 Activity 内存使用量就会跃升至约 9mB 一旦 Act
  • 屏幕方向更改后应用程序崩溃

    我有以下问题 启动后 应用程序工作正常 即使在更改屏幕方向后也是如此 应用程序尚未准备好处理方向更改 例如替代布局等 因此仅显示旋转的默认布局就可以了 但是 当我通过按后退键离开应用程序 更改方向并在再次启动应用程序后立即崩溃 崩溃后 如果
  • SQlite 获取最近的位置(带有纬度和经度)

    我的 SQLite 数据库中存储有纬度和经度的数据 我想获取距我输入的参数最近的位置 例如我当前的位置 纬度 经度等 我知道这在 MySQL 中是可能的 并且我已经做了相当多的研究 SQLite 需要一个自定义外部函数来实现半正弦公式 计算
  • 未捕获的引用错误:cordova 未定义

    这是我的 HelloPlugin js 文件 var HelloPlugin callNativeFunction function success fail resultType return cordova exec success f
  • 为什么 fork 炸弹没有使 android 崩溃?

    这是最简单的叉子炸弹 我在许多 Linux 发行版上执行了它 但它们都崩溃了 但是当我在 android 终端中执行此操作时 即使授予后也没有效果超级用户权限 有什么解释为什么它没有使 Android 系统崩溃吗 一句话 ulimit Li
  • Android:防止嗅探(例如使用 CharlesProxy)SSL 流量

    我使用 Charles 检查将我的应用程序发送到 HTTPS 的数据 我在手机上安装了 Charles CA 证书 因此我能够解密每个 SSL 流量 但我发现一些应用程序无法看到 SSL 流量 我如何将这种行为实现到我自己的应用程序中 有了

随机推荐