美文网首页android 技术梳理
Android 基本功-事件分发机制

Android 基本功-事件分发机制

作者: jkwen | 来源:发表于2021-03-15 10:08 被阅读0次

背景知识

一般我们所说的事件分发是指点击事件的分发处理。其次,点击事件一般都是通过应用界面点击产生的,而界面又是 xml 布局的直观展示。xml 布局文件里,摆放着我们精心设计的布局控件,最终加载到内存里的就是一棵视图树,而 DecorView 就是树的根。

点击事件的产生会经过 Activity,ViewGroup, View 进行分发处理,这个是规定好的事件分发走向,可以强行记住。

另外,我们说的事件分发机制也可以认为就是对 3 个关键方法的使用,

  • dispatchTouchEvent
    分发点击事件
  • onInterceptTouchEvent
    拦截点击事件,这会在 dispatchTouchEvent 方法中调用,格外注意,只有类似于 LinearLayout 这种容器类控件才有这个方法,也就是说只有继承自 ViewGroup 的才有,对于特定的 View,只有处理或不处理事件的份。
  • onTouchEvent
    处理点击事件,这也会在 dispatchTouchEvent 方法中调用

这 3 个方法的入参都有一个 MotionEvent 类型的参数,这个就是点击事件。并且方法都带有 boolean 型的返回值,true 表示分发或处理成功,此时事件分发流程应该终止,false 表示仍可继续。

所以事件分发的大致逻辑可以这样来理解,接收到事件后对其进行分发操作,在分发的过程中就确定了是否拦截,不拦截就继续分发,拦截了就看看是否处理。

有了以上概念的了解,再来结合源码看看具体怎么走,我将整个流程拆为两个部分,一部分是事件分发,另一部分是事件处理。

事件分发过程

//Activity
public boolean dispatchTouchEvent(MotionEvent ev) {
    //判断事件是否分发成功
    if (getWindow().superDispatchTouchEvent(ev)) {
        //成功的话就结束了
        return true;
    }
    //分发了一遍都没有分发掉就自己处理,并返回处理结果
    return onTouchEvent(ev);
}
//getWindow() 方法获取的是窗口对象,具体实现类为 PhoneWindow
public boolean superDispatchTouchEvent(MotionEvent event) {
    //PhoneWindow 里又调用了 mDecor 的分发方法
    //mDecor 就是 DecorView 类型
    //可见接下去就开始从视图树的根节点开始分发事件了
    return mDecor.superDispatchTouchEvent(event);
}
//DecorView
public boolean superDispatchTouchEvent(MotionEvent event) {
    //调用了父类的方法
    //DecorView 继承自 FrameLayout, FrameLayout 又继承自 ViewGroup
    //FrameLayout 没有该方法,所以最终调用的是 ViewGroup 的
    return super.dispatchTouchEvent(event);
}
//ViewGroup
public boolean dispatchTouchEvent(MotionEvent ev) {
    //标记事件分发是否处理完成
    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {
        //标记是否拦截事件,注意这个只有 ViewGroup 才有的逻辑
        final boolean intercepted;
        //这里的 mFirstTouchTarget 可能需要重点关注下
        if (actionMasked == MotionEvent.ACTION_DOWN 
            || mFirstTouchTarget != null) {
            //disallowIntercept 用来表示该 ViewGroup 不具备拦截功能是否成立
            //也就是 disallowIntercept 为 true 就是不拦截,默认是 false
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                //如果可以拦截,那就要通过 onInterceptTouchEvent 方法决定是否拦截
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action);
            } else {
                intercepted = false;
            }
        } else {
            //如果不是 DOWN 事件,且 mFirstTouchTarget 为空的话就直接拦截
            intercepted = true;
        }
        final boolean canceled = resetCancelNextUpFlag(this)
             || actionMasked == MotionEvent.ACTION_CANCEL;
        //如果事件没有取消,也没有被拦截,就要继续分发
        TouchTarget newTouchTarget = null;
        if (!canceled && !intercepted) {
            removePointersFromTouchTargets(idBitsToAssign);
            final int childrenCount = mChildrenCount;
            if (newTouchTarget == null && childrenCount != 0) {
                //按 z 轴进行子 View 排序
                final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                final View[] children = mChildren;
                //遍历子 View 找到可以处理事件的 view
                for (int i = childrenCount - 1; i >= 0; i--) {
                    final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
                    final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
                    newTouchTarget = getTouchTarget(child);
                    if(dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                        newTouchTarget = addTouchTarget(child, idBitsToAssign);
                        break;
                    }
                }
            }
        }
        //这块我只能看懂个大概,如果有什么不对,可以帮忙指正
        //经过上面的遍历,如果能找到合适的处理事件的 view 就会走这里的 else 部分,
        //不然就会自己处理
        if (mFirstTouchTarget == null) {
            handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);
        } else {
            TouchTarget target = mFirstTouchTarget;
            while (target != null) {
                final TouchTarget next = target.next;
                if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                    handled = true;
                }
            }
        }
    }
}
//View
public boolean dispatchTouchEvent(MotionEvent event) {
    //同 ViewGroup 一样,在认为点击事件有效的情况下继续执行
    if (onFilterTouchEventForSecurity(event)) {
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
            && (mViewFlags & ENABLED_MASK) == ENABLED
            && li.mOnTouchListener.onTouch(this, event)) {
            //这里就触发了 onTouch 方法回调,
            //这个 onTouch 方法就是通过 setOnTouchListener 方法设置的点击事件监听器的回调,如果事件在这里被处理掉了,那么事件处理就结束了。
            result = true;
        }
        if (!result && onTouchEvent(event)) {
            //要是上面没处理掉,就会走到这里,看到了吧,onTouchEvent 就是在这里调的
            //所以这里能调的前提是前面没有把事件拦掉,那怎么不会拦掉呢?
            //要么不设置监听器,要么 onTouch 方法不要返回 true
            result = true;
        }
    }
    return result;
}

经过上面的一路分析,事件的分发到这就算是到的末端了。总结起来就是从点击事件的生成到分发经过了 Activity(PhoneWindow -> DecorView) -> ViewGroup -> View。在事件分发给合适的 View 之后,就要开始处理了。

事件处理过程

//前面我们已经将事件分发到了 ViewGroup,在 ViewGroup 会决定分发给哪个具体 View
//View,代码分析同前面一样,可以不看
public boolean dispatchTouchEvent(MotionEvent event) {
    //同 ViewGroup 一样,在认为点击事件有效的情况下继续执行
    if (onFilterTouchEventForSecurity(event)) {
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
            && (mViewFlags & ENABLED_MASK) == ENABLED
            && li.mOnTouchListener.onTouch(this, event)) {
            //这里就触发了 onTouch 方法回调,
            //这个 onTouch 方法就是通过 setOnTouchListener 方法设置的点击事件监听器的回调,如果事件在这里被处理掉了,那么事件处理就结束了。
            result = true;
        }
        if (!result && onTouchEvent(event)) {
            //要是上面没处理掉,就会走到这里,看到了吧,onTouchEvent 就是在这里调的
            //所以这里能调的前提是前面没有把事件拦掉,那怎么不会拦掉呢?
            //要么不设置监听器,要么 onTouch 方法不要返回 true
            result = true;
        }
    }
    return result;
}
public boolean onTouchEvent(MotionEvent event) {
    //onTouchEvent 里逻辑也蛮多的,来看主要的
    //简单来说会根据事件类型来做对应处理,
    //事件会被封装成 MotionEvent 对象,分为 DOWN, MOVE, UP, CANCEL 四种
    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
        switch (action) {
              case MotionEvent.ACTION_UP:
                //UP 事件里会调用下面这个方法,而这个就是我们平时对控件做点击事件响应用的最多的方法
                //对应的就是 onClick 方法回调
                //所以有时候 set 了 onTouchListener 又 set 了 onClickListener 就会发现 onClick 方法不走了。原因就在于前后顺序的问题。
                performClickInternal();
                break;
              case MotionEvent.ACTION_DOWN:
                break;
              case MotionEvent.ACTION_CANCEL:
                break;
              case MotionEvent.ACTION_MOVE:
                break;
        }
        return true;
    }
    return false;
}

其实对于具体 View 的事件分发就是看事件要不要处理。按前面在 ViewGroup 里说的,如果没有合适的 View 会处理事件的话,ViewGroup 会自己处理,但其实 ViewGroup 也继承自 View,所以它的事件处理也和具体 View 的一样。

再回退到 Activity 里,如果事件没有分发成功,那 Activity 就会自己处理

//Activity
public boolean dispatchTouchEvent(MotionEvent ev) {
    //判断事件是否分发成功
    if (getWindow().superDispatchTouchEvent(ev)) {
        //成功的话就结束了
        return true;
    }
    //分发了一遍都没有分发掉就自己处理,并返回处理结果
    return onTouchEvent(ev);
}
public boolean onTouchEvent(MotionEvent event) {
    //既然事件都没分发成功,这个事件有可能是个关闭事件或者无效的
    if(mWindow.shouldCloseOnTouch(this, event)) {
        //这里应该就是表示关闭事件,这时会销毁 Activity
        finish();
        return true;
    }
    return false;
}

到这事件处理部分也就差不多了。虽说整个过程还是涉及比较多的东西,但平时实际用的话基本就做点击响应,除非一些自定义控件需要特殊处理,那么就要考虑拦不拦截啊,各种类型的事件做什么处理啊。又或者多个控件组合使用,例如列表控件和刷新控件或者嵌套滚动之类的。

总之,事件分发机制还是必须要掌握的,毕竟是 Android 开发里天天都要打交道的东西。

参考内容

Android事件分发机制 详解攻略,您值得拥有 文章讲的很好,很是推荐

相关文章

网友评论

    本文标题:Android 基本功-事件分发机制

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