美文网首页知识 | 解析Android知识Android开发
View 体系之 View 事件分发源码解析

View 体系之 View 事件分发源码解析

作者: MeloDev | 来源:发表于2017-02-14 15:19 被阅读418次

    View 体系之 View 事件分发源码解析

    本文原创,转载请注明出处。
    欢迎关注我的 简书 ,关注我的专题 Android Class 我会长期坚持为大家收录简书上高质量的 Android 相关博文。

    写在前面:

    前两天我们分别总结了
    View 的位置与事件:
    View 的位置与事件

    View 的滑动:
    View 的滑动

    今天我们来聊聊 View 的事件分发。相信每个人都知道 View 的事件分发实在是太重要了,它不仅仅是一个核心知识点,更是一个难点。在我初学 Android 时,View 的事件分发也前前后后看了好多次。虽然也能复述出一个大概,但是仍然有一些知识盲区。所以今天把 View 的事件分发总结出来,算是自己记下一篇学习笔记,未来复习巩固使用。如果还能为大家解决一些困惑,那就更好了。

    当然关于事件分发的文章,前辈们总结了很多,有一篇我认为非常出色:
    图解 Android 事件分发

    这篇文章通过图解的方式,清晰直观的讲明白了事件分发的原则,本文打算对这篇文章做一些补充,补充一下这篇文章的一些关键 log,和源码分析。所以不理解事件分发的朋友可以阅读下该文。

    首先大家想一下,在 Android 中谁是事件分发的掌控者和消费者?没错,是 Activity、ViewGroup、View,一个正常的 Android 应用程序他们三个肯定是存在的。而分发的事件就是 MotionEvent 对象。

    关于事件分发,有三个关键的方法

    dispatchTouchEventonInterceptTouchEventonTouchEvent

    onInterceptTouchEvent 方法是 ViewGroup 特有的。我们在 Activity、ViewGroup、View 中分别打印这几个方法,来看看不同的返回值对事件分发的影响,来印证上文的观点,并且分析出事件分发的传递规则。

    当我们不修改任何返回值,全部为默认实现时:

    这里写图片描述

    可以看到 ACTION_DOWN 事件的传递原则为,U 型原则,ACTION_MOVE、ACTION_UP 传递原则为距离最短原则。

    分别来改变 Activity 中dispatchTouchEventonTouchEvent 的返回值,来看看事件传递的 log:

    首先分别将 dispatchTouchEvent 的返回值改为 false 或者 true:

    这里写图片描述

    可以看到我的这次点击按钮的事件在 Activity 中的 dispatchTouchEvent 中消费掉了。

    当我改变 MainActivity 中 onTouchEvent 方法的返回值时:

    这里写图片描述

    可以看到打印的结果与最初所有方法的默认返回值相同,这也很好理解,因为 Activity 的 onTouchEvent 方法本身就是事件 U型 传递的最后一环,不管什么返回值,反正事件都会到这里。

    Activity 的看完了,再来看看 ViewGroup 的:

    这里写图片描述

    可以看到将 ViewGroup 的 dispatchTouchEvent 返回值改为 false 时,事件就不会再下发了,而是直接传递给 Activity 的 onTouchEvent。当 dispatchTouchEvent 返回值改为 true 时,与默认实现相同。

    这里写图片描述

    onInterceptTouchEvent 的返回值改为 true 时,事件不会再传递给 View ,而是传递给当前 ViewGroup 的 onTouchEvent。当onInterceptTouchEvent返回值为 false 时,与默认相同。

    这里写图片描述

    首先明确一个概念:事件序列

    就是当手指 按下-->滑动-->抬起 的这一完成过程产生的事件流为一个事件序列。

    当我将 ViewGroup 的 onTouchEvent 方法的返回值改为 true 时,事件在 ViewGroup 就消费掉了,这里应该注意,onInterceptTouchEvent 如果发生了拦截,那么在一个事件序列中仅调用一次。

    关于 View 这两个方法的返回值就不贴图了,与引用文章的结论一致。

    一些细节

    当给一个 View 设置 onTouchListener 时,它的 onTouch 方法就会回调,如果 onTouch 方法的返回值为 false,则该 View 的 onTouchEvent 方法会被调用,如果 onTouch 方法的返回值为 true,则该 View 的 onTouchEvent 方法就不会调用了,事件会直接在该 View 的 disPatchTouchEvent 中消费。另外 onClick 方法是在 onTouchEvent 方法中调用的。所以这几个方法的优先级关系为:

    onTouch>onTouchEvent>onClick

    一个 View 的 onTouchEvent 的返回值是与这个 View 本身的 onClick 和 onLongClick 属性相关的,只有这两个属性同时为 false 则 onTouchEvent 才会为 false,View 的 onLongClick 默认都为 false,而 onClick 属性不同,比如 button 的为 true,textview 的为 false。

    事件分发的源码解析

    在这部分内容中,我们看看事件分发在源码上的处理,事件最初都是在 Acitivity 中产生,然后分发给根 ViewGroup,最后再发给相应的 View。那先来看看 Activity 的 dispatchTouchEvent 方法。

        public boolean dispatchTouchEvent(MotionEvent ev) {
            if (ev.getAction() == MotionEvent.ACTION_DOWN) {
                onUserInteraction();
            }
            if (getWindow().superDispatchTouchEvent(ev)) {
                return true;
            }
            return onTouchEvent(ev);
        }
    

    这段代码很简单,当 ACTION_DOWN 来了的时候,回调 onUserInteraction 方法作为事件起始的回调。

    然后来看看 getWindow().superDispatchTouchEvent(ev) 方法的返回值是如何的。

    setContentView

    首先关于 Window 和 PhoneWindow 类的关系可以上面这篇我曾经总结的文章。

    PhoneWindow:

        @Override
        public boolean superDispatchTouchEvent(MotionEvent event) {
            return mDecor.superDispatchTouchEvent(event);
        }
    

    跟进,看看 DectorView 的 superDispatchTouchEvent(event) 方法:

        public boolean superDispatchTouchEvent(MotionEvent event) {
            return super.dispatchTouchEvent(event);
        }
    

    DectorView 继承自 FrameLayout,所以这里也是调用到了 ViewGroup 的 dispatchTouchEvent,事件顺利传到了 ViewGroup

    来看看 ViewGroup 对事件的分发
    代码比较多,我们分段来看:

                // Check for interception.
                final boolean intercepted;
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || mFirstTouchTarget != null) {
                    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;
                }
    

    这段代码的意义是,判断是否要调用 onInterceptTouchEvent 方法,可以看到 if 判断的条件语句为:if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
    先看第一个,当事件为 ACTION_DOWN 时,肯定会调用 onInterceptTouchEvent,那 mFirstTouchTarget 是什么呢?由后面的代码可知,当事件不被拦截并且交给子元素处理时,mFirstTouchTarget != null。所以当被当前 View 拦截的时候,mFirstTouchTarget == null,ACTION_DOWN、ACTION_MOVE 事件来的时候,条件就不成立了,所以 onInterceptTouchEvent方法也不会再次调用,这也就是为什么之前说,当此 ViewGroup 确定拦截事件的时候,onInterceptTouchEvent之后在事件为 ACITON_DOWN 的时候调用一次。

    这有一个 flag 比较重要,FLAG_DISALLOW_INTERCEPT,它的值由子 View 的 requestDisallowInterceptTouchEvent 决定,由子 View 请求父 View 不要拦截事件。当然此属性对 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();
                }
    

    dispatchTouchEvent 的开头就重置了 FLAG 的状态。

    这里我们会有两个结论:

    1. onInterceptTouchEvent 方法并不是每次都调用,而如果事件传递进来,dispatchTouchEvent 才是每次都会调用的。
    2. requestDisallowInterceptTouchEvent 可以干预父 View 的事件分发过程,有助于我们解决滑动冲突。
                        final View[] children = mChildren;
                            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 (!canViewReceivePointerEvents(child)
                                        || !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)) {
                                    // Child wants to receive touch within its bounds.
                                    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);
                                    alreadyDispatchedToNewTouchTarget = true;
                                    break;
                                }
    

    这段代码首先判断所有的子 View 是否具有接受事件的能力,1 没有进行动画 2 点击的位置在 View 的坐标范围内。dispatchTransformedTouchEvent 中有这样一行代码:

    handled = child.dispatchTouchEvent(event);
    

    所以到这里,就调用到了子 View 的 dispatchTouchEvent 方法。

    在 addTouchTarget 方法中:

     mFirstTouchTarget = target;
    

    mFirstTouchTarget 被赋值,也就是当子 View 处理事件时,mFirstTouchTarget 不为 null.

    看完了 ViewGroup 对事件分发的处理,我们来看看 View 对事件的处理吧。

        /**
         * 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)) {
                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;
        }
    

    因为 View 不会再有子 View 了,所以他的 dispatchTouchEvent 方法比较简单,首先如果这个 View 设置了 onTouchListener,并且 onTouch 方法返回值为 true 时,会进入判断条件,方法直接返回 true,就不会走到 onTouchEvent 方法里面了。所以这里也印证了我们之前的观点,也就是 onTouch 方法优先级大于 onTouchEvent。

    再来看看 View 的 onTouchEvent 方法的源码:

            if ((viewFlags & ENABLED_MASK) == DISABLED) {
                if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                    setPressed(false);
                }
                // A disabled view that is clickable still consumes the touch
                // events, it just doesn't respond to them.
                return (((viewFlags & CLICKABLE) == CLICKABLE
                        || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                        || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
            }
    

    可以看到这里即使 View 是 disable 的,依然可以消耗事件。

    if (((viewFlags & CLICKABLE) == CLICKABLE ||
                    (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                    (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE)
         switch (action) {
                    case MotionEvent.ACTION_UP:
                            if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                                // This is a tap, so remove the longpress check
                                removeLongPressCallback();
    
                                // Only perform take click actions if we were in the pressed state
                                if (!focusTaken) {
                                    // Use a Runnable and post this rather than calling
                                    // performClick directly. This lets other visual state
                                    // of the view update before click actions start.
                                    if (mPerformClick == null) {
                                        mPerformClick = new PerformClick();
                                    }
                                    if (!post(mPerformClick)) {
                                        performClick();
                                    }
                                }
                            }
                        break;
    

    可以看到当这个 View 的 LongClick 或者 Clickable 属性有一个为 true,就可以消耗这个事件,并且在 ACTION_UP 调用 performClick():

    final boolean result;
            final ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnClickListener != null) {
                playSoundEffect(SoundEffectConstants.CLICK);
                li.mOnClickListener.onClick(this);
                result = true;
            } else {
                result = false;
            }
    

    所以到这里这个 View 的点击事件也响应了。

    到这里,我们事件分发的源码就分析完毕了。

    写在后面:

    本文更多的是对上面那篇引用文章的源码补充,两篇结合起来看,对事件分发的理解就应该足够了。这几天看源码看得头疼。。。关于本文的结论总结,我准备过一阵回头温习的时候补充下,希望大家喜欢。

    相关文章

      网友评论

        本文标题:View 体系之 View 事件分发源码解析

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