美文网首页
震惊,View事件分发机制竟然是这样的

震惊,View事件分发机制竟然是这样的

作者: 一只小松 | 来源:发表于2017-03-16 22:53 被阅读0次

    事件类型MotionEvent

    MotionEvent的事件有三种类型:

    • ACTION_DOWN
    • ACTION_MOVE
    • ACTION_UP

    当你点击一次屏幕的时候,整个过程中包含了一个ACTION_DOWN开始事件和多个的ACTION_MOVE以及一个ACTION_UP终止事件。当然如果没有在屏幕上滑动的也就没有ACTION_MOVE事件啦。

    事件分发机制

    通常我们的点击事件传递过程是Activity->Window->DecorView(View的事件分发机制),接下来具体介绍下这三者的事件传递过程:

    1. Activity#dispatchTouchEvent的过程

        /**
         * Called to process touch screen events.  You can override this to
         * intercept all touch screen events before they are dispatched to the
         * window.  Be sure to call this implementation for touch screen events
         * that should be handled normally.
         *
         * @param ev The touch screen event.
         *
         * @return boolean Return true if this event was consumed.
         */
        public boolean dispatchTouchEvent(MotionEvent ev) {
            if (ev.getAction() == MotionEvent.ACTION_DOWN) {
                onUserInteraction();// 该方法默认没有实现内容,子类Activity可在ACTION_DOWN发生时做特定互动
            }
            if (getWindow().superDispatchTouchEvent(ev)) {// 交由Window分发处理
                return true;
            }
            return onTouchEvent(ev);// 整个View树的onTouchEvent都返回false(不消费事件),仍然由Activity自身处理
        }
    

    dispatchTouchEvent是系统事件传递地开端,是Window.Callback的一个重要回调方法,系统将屏幕点击事件传递于此。

    2. PhoneWindow#superDispatchTouchEvent的过程

    Window是一个抽象类,superDispatchTouchEvent也是个抽象方法,PhoneWindow是其唯一实现类。

        @Override
        public boolean superDispatchTouchEvent(MotionEvent event) {
            return mDecor.superDispatchTouchEvent(event);// mDecor就是DecorView,即getWindow.getDecorView()返回的那个View
        }
    

    3. 顶级View对事件的分发过程

    我们先了解下几个会在View事件分发中使用到的方法:

    • onTouchEvent 处理点击事件
    • onInterceptTouchEvent 是否拦截处理点击事件,在ViewGroup才有
    • dispatchTouchEvent 分发事件

    以上三个方法的返回结果:

    • false,表示事件在当前view未消耗,则继续往view树下层传递;
    • true,表示事件在当前view已被消耗,则不再传递。

    一段伪代码表示以上三个方法的关系:

    public boolean dispatchTouchEvent(MotionEvent ev){
            boolean consume = false;
            if (onInterceptTouchEvent(ev)){
                    consume = onTouchEvent(ev);
            }else{
                    consume = child.dispatchTouchEvent(ev);
            }
            return consume;
    }
    

    那么可能有同学会问,不是还有onTouchListener和onClickListener吗?
    这三者的优先级是onTouchListener > onTouchEvent > onClickListener,我们可以从View源码来验证。

    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;
                if (li != null && li.mOnTouchListener != null
                        && (mViewFlags & ENABLED_MASK) == ENABLED
                        && li.mOnTouchListener.onTouch(this, event)) {// OnTouchListener优先调用(可用状态下)
                    result = true;
                }
    
                if (!result && onTouchEvent(event)) {// 若OnTouchListener返回true,则onTouchEvent被屏蔽
                    result = true;
                }
            }
    
            ....
            return result;
        }
    

    onTouchListener若存在,onTouch方法返回true则会屏蔽掉onTouchEvent。

    现在来看ViewGroup#dispatchTouchEvent,分段来说明:
    1) requestDisallowInterceptTouchEvent设置FLAG_DISALLOW_INTERCEPT状态后,将使ViewGroup无法拦截除ACTION_DOWN以外的其他点击事件。换言之,尽管子元素有优先禁用父容器的拦截功能,但是对于ACTION_DOWN事件是个特例;

                // Handle an initial down.
                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();// ACTION_DOWN事件重置了FLAG_DISALLOW_INTERCEPT标记位
                }
    

    2) 这部分代码描述当前ViewGroup是否拦截点击事件的这个逻辑

                // Check for interception.
                final boolean intercepted;
                if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {// ACTION_DOWN动作和子元素是否成功处理(null表示没有子元素处理)
                    // 若是ACTION_DOWN事件,FLAG_DISALLOW_INTERCEPT标记位会被清除,disallowIntercept为false
                    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                    if (!disallowIntercept) {// 若子元素未禁止父容器的拦截功能(ACTION_DOWN肯定会执行)
                        intercepted = onInterceptTouchEvent(ev);// ViewGroup自己决定是否拦截
                        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. 即没有子view处理时(mFirstTouchTarget 若不为空说明子元素处理成功),事件由当前ViewGroup拦截,不再调用onInterceptTouchEvent来决定是否拦截
                    intercepted = true;
                }
    

    由上源码分析,我们可得出结论:

    1. ViewGroup决定拦截事件,那后续事件将默认交由处理,不需要再调用onInterceptTouchEvent
    2. FLAG_DISALLOW_INTERCEPT标记位的作用是让ViewGroup不再拦截事件,当然前提是ViewGroup不拦截ACTION_DOWN事件,ViewGroup若要拦截,则标记位设置了也无效

    总结

    我们最后完整的理一遍整个流程:一个点击事件首先由Activity接收,Activity调用dispatchTouchEvent进行分发,优先传递给Window进行处理,如果Window不消耗该事件则再由Activity的onTouchEvent来处理;Window处理过程则直接委托给DecorView进行事件分发,这个DecorView是android.R.id.content的父View,而android.R.id.content的子View就是我们Activity的视图view。
    接下来就进入了View的事件分发过程
    如果我们的Activity视图中的顶级ViewGroup拦截事件,即onInterceptTouchEvent返回true,则事件由该层ViewGroup处理,如果设置了onTouchListener,则onTouch被调用,否则onTouchEvent会被调用,如果还有onClickListener,则onClick最后被调用;若ViewGroup不拦截,则事件传递到点击事件链上的子View,子View的dispatchTouchEvent被调起,依上如此循环,完成事件分发。

    注意点

    • 一个事件序列(开始-结束)只能被一个View拦截消耗
    • 子view一旦开始处理事件,ACTION_DOWN事件就应该返回true;若返回false,则后续事件序列不再交由处理,重新由其父元素onTouchEvent去处理
    • ViewGroup默认不拦截任何事件
    • View只要可点击的,onTouchEvent默认会消耗事件
    • 子View可以通过requestDisallowIntercceptTouchEvent干预父元素的事件分发,ACTION_DOWN事件除外

    相关文章

      网友评论

          本文标题:震惊,View事件分发机制竟然是这样的

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