View 体系之 View 事件分发

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 中分别打印这几个方法,来看看不同的返回值对事件分发的影响,来印证上文的观点,并且分析出事件分发的传递规则。

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

View 体系之 View 事件分发_第1张图片

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

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

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

View 体系之 View 事件分发_第2张图片

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

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

View 体系之 View 事件分发_第3张图片

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

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

View 体系之 View 事件分发_第4张图片

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

View 体系之 View 事件分发_第5张图片

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

View 体系之 View 事件分发_第6张图片

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

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

当我将 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 的点击事件也响应了。

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

写在后面:

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

你可能感兴趣的:(Android开发)