Android 事件分发机制的理解

栏目: IOS · Android · 发布时间: 5年前

内容简介:在进行正文之前,我们带着以下几个问题有目的的进行,然后最后再做问题的解决。首先我们要清楚,事件分发的对象是什么?其实就 MotionEvent,这个 MotionEvent 可以有 ACTION_DOWN,ACTION_UP,ACTION_MOVE,ACTION_CANCEL 等事件类型。Activity -> Window -> ViewGroup -> View

在进行正文之前,我们带着以下几个问题有目的的进行,然后最后再做问题的解决。

  • 问题 1:activity、 ViewGroup和 View 都不消费 ACTION_DOWN,那么 ACTION_MOVE 和 ACTION_UP 事件是怎么传递的?
  • 问题 2:在 ViewGroup 中的 onTouchEvent 中消费 ACTION_DOWN 事件(onInterceptTouch 默认设置),那么 ACTION_MOVE 和 ACTION_UP 事件是怎么传递的?
  • 在一个列表中,同时对父 View 和子 View 设置点击方法,优先响应哪个?为什么会这样?
  • 为什么子 View 不消费 ACTION_DOWN,之后的所有事件都不会向下传递了。

基础认识

事件分发的对象

首先我们要清楚,事件分发的对象是什么?其实就 MotionEvent,这个 MotionEvent 可以有 ACTION_DOWN,ACTION_UP,ACTION_MOVE,ACTION_CANCEL 等事件类型。

事件(MotionEvent)分发是在哪些对象中进行的?

Activity -> Window -> ViewGroup -> View

重点关注的方法

  • dispatchTouchEvent
  • onInterceptTouchEvent
  • onTouchEvent

ACTION_DOWN 事件在 Activity、ViewGroup、View 中根据不同函数不同的返回值的走向?

关于事件的走向,我觉的以下这张图可以很清晰的看出事件的最终走向,该图来自Kelin

Android 事件分发机制的理解
注:流程图来之Kelin

以下是使用文字对事件走向的描述,帮助对流程图的理解。

方法 Activity ViewGroup View
dispatchTouchEvent (1) return false 或者是 return true,表示 Activity 已经消费
(2) return 父类的 super.dispatchTouchEvent() 表示向下传递,最后结果由下一级决定
(1) return false,表示 ViewGroup 不消费事件,返回给上一级的 ViewGroup 或者 Activity 的 onTouchEvent 进行处理;
(2) return true,表示当前 ViewGroup 已经消费了事件, 事件在此终止
(3) return super.dispatchTouchEvent(),表示事件继续,根据 onInterceptTouchEvent 判断是否自己处理事件(是否调用 onTouchEvent)
(1) return false,表示 View 不消费事件,返回给上一级的 ViewGroup onTouchEvent 进行处理;
(2) return true,表示当前 View 已经消费了事件, 事件在此终止
(3) return super.dispatchTouchEvent(),表示事件继续,并调用自己的方法 onTouchEvent
onInterceptTouchEvent 没有此方法 (1) return true,表示 ViewGroup 拦截该事件;之后 onInterceptTouchEvent 不会再被调用
(2) return false 或者 return super.onInterceptTouchEvent 表示不拦截事件,将事件交给子 view 处理。
没有此方法
onTouchEvent (1) return false,表示所有的 ViewGroup 和 View 都不消费事件,Activity 也不消费;
(2) return true,表示消费该事件, 事件在此终止
(1) return false 或者是 return super.onTouchEvent 表示不消费事件,返回给上一级的 onTouchEvent 处理
(2) return true 表示当前 ViewGroup 自己消费了, 事件在此终止 ,不会往上传也不会往下传了。
(1) return false 或者是 return super.onTouchEvent 表示不消费事件,返回给上一级的 ViewGroup 进行护理;
(2) return true 表示当前 view 进行处理事件, 事件在此终止

结论

  1. dispatchTouchEvent 和 onTouchEvent 中返回 true,ACTION_DOWN 事件就在此终止,不会再往上传也不会往下传了。
  2. dispatchTouchEvent 和 onTouchEvent 中返回 false,ACTION_DOWN 事件交给父控件的 onTouchEvent 进行处理
  3. onInterceptTouchEvent 一旦返回 true,那么 ViewGroup 之后不会再调用 onInterceptTouchEvent

事件分发源码分析

Activity

一旦事件产生,那么首先被调用的是 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 方法就会被调用,onUserInteraction 是个空的方法,当事件产生时,那么这个方法就会被调用,如果在 activity 运行的时候,我们想要知道用户和设备的交互,那么我们就可以实现这个方法。

接着 window 中的 superDispatchTouchEvent 方法被调用,事件传递到 window 中。Window 是个抽象类,他的唯一实现是 PhoneWindow。在 Window.superDispatchTouchEvent 中调用了 DecorView 的 superDispatchTouchEvent,而这个 DecorView 就是我们在 activity 中通过调用 setContentView 设置的布局的顶层 View。

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

复制代码

最后,如果 Activity 中的所有下级事件承载对象没有处理事件,最后 Activity 中的 onTouchEvent 就会被调用,当事件超出边界或者事件为 ACTION_DOWN 时,mWindow.shouldCloseOnTouch(this, event) 为 true,onTouchEvent 默认是返回 false 的。

public boolean onTouchEvent(MotionEvent event) {
        if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
        }

        return false;
    }
复制代码

onTouchEvent 方法当所有的 view 都没有消费事件的时候,activity 的 onTouchEvent() 就会被调用

我们这边看一下 getWindow() 里面的实现,其实也就是返回一个 Window 实例

public Window getWindow() {
        return mWindow;
    }
复制代码

Window(PhoneWindow)

Window 在事件分发的过程中就类似于一个中间的桥接一样,是没有做什么操作的,只是将事件传递给 DecorView 中。

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

复制代码

ViewGroup

  • 要理解 ViewGroup 对于事件的处理,我们先来个简化的代码逻辑来帮助我们理解
void dispathTouchEvent(Event event){
    boolean consume = false;
    if(OnInterceptTouchEvent(event)){
        consume = onTouchEvent(event);
    }else{
        consume = childView.dispatchTouchEvent(event);
    }
    return consume;
}

复制代码

对于一个 ViewGroup 来说,点击事件产生之后,dispatchTouchEvent 就会被调用,如果这个 ViewGroup 的 onInterceptTouchEvent 返回 true 就表示它要拦截当前的事件,接着事件就会给这个 ViewGroup 处理,即它的 onTouchEvent 会被调用;如果 ViewGroup 的 onInterceptTouchEvent 返回 false,就表示它不拦截事件,这时当前事件就会被传递给它的子 view,接着调用子元素的 dispatchTouchEvent 方法就会被调用,如此反复,直到事件被最终处理。

  • 大致流程图
Android 事件分发机制的理解
  • 我们再看看 ViewGroup.dispatchTouchEvent() 还原度比较高的源码
// 省略部分代码

            // 如果是 ACTION_DOWN 事件,重置标志位 mGroupFlags 为 非 FLAG_DISALLOW_INTERCEPT,这个标志位关系到 ViewGroup 的 onInterceptTouch 是否有效。
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }
            

            // 检查是否拦截
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                    // 处理条件为:
                    // 1. 事件为 ACTION_DOWN
                    // 2. 有下级的 View 处理事件
                    
                    // 判断拦截是否失效?mGroupFlags = FLAG_DISALLOW_INTERCEPT 时,onInterceptTouchEvent 是不会被调用的
                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 {
            
                // 非 ACTION_DOWN 事件且没有其他的下级处理该事件的时候,不会再调用 onInterceptTouchEvent
                intercepted = true;
            }

            // 正常事件分发
            // 如果 ViewGroup 决定拦截或者已经有 子 view 处理事件,那么就开始正常的事件分发流程
            if (intercepted || mFirstTouchTarget != null) {
                ev.setTargetAccessibilityFocus(false);
            }


            // 不拦截事件
            if (!canceled && !intercepted) {
                View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                        ? findChildWithAccessibilityFocus() : null;

                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    
                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                    
                        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);


                            // 找到接收该事件的子 View 上,如果找到,则直接跳出循环
                            newTouchTarget = getTouchTarget(child);
                            if (newTouchTarget != null) {
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }

                            // 判断事件是否落在子 view 上,如果是,则跳出循环
                            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;
                            }

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

                }
            }


复制代码

从以上的代码中可以看出,当事件传递到 ViewGroup 中的 dispatchTouchEvent 的时候,这次经历了以下的几个重要步骤:

  • step 1: 重置标志位 mGroupFlags,将 mGroupFlags 标志位设置为非 FLAG_DISALLOW_INTERCEPT,如果 mGroupFlags = FLAG_DISALLOW_INTERCEPT,那么 ViewGroup 将不再调用 onInterceptTouch(),默认 ViewGroup 不拦截任何事件。
  • step 2: 通过判断事件是否为 ACTION_DOWN 或 mFirstTouchTarget != null 来决定是否向下分发 ACTION_DOWN 之外的事件;
    • 若是子 View 消费 ACTION_DOWN,那么 mFirstTouchTarget 会被赋值,mFirstTouchTarget != null 不成立
    • 若是子 View 不消费 ACTION_DOWN,那么 mFirstTouchTarget 则为 null,ViewGroup 默认拦截 ACTION_DOWN 之后的所有事件,不向下传递。
  • step 3:通过 mGroupFlags 标志位判断拦截是否有效,若是 mGroupFlags = FLAG_DISALLOW_INTERCEPT,则 ViewGroup 默认不拦截任何事件。
  • step 4: 循环所有的子 view
    • (1)判断是否有子 view 已经处理改事件了,如果有则跳出循环,直接向下级子 view 分发事件。
    • (2)判断点击是否落在某个子 view;
  • step 5: 如果子 view 消费了事件,那么将 mFirstTouchTarget 进行赋值,mFirstTouchTarget(链表)。

View

View 对于事件的处理要稍微简单一点,注意这里的 View 并不包含 ViewGroup。我们先看看 dispatchTouchEvent 方法。

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 是个单独的元素,它没有子元素,因此无法向下传递事件,所以它只能自己处理。

从上面的源码中,我们可以看出 View 对点击事件的处理过程,首先会判断有没有设置 onTouchListener,如果 onTouchListener 中的 onTouch 返回了 true,那么 onTouchEvent 就不会再被调用,可见 onTouchListener 的优先级要高于 onTouchEvent,这样的处理是方便点击事件在外界进行处理。

  • View 中的 onTouchEvent 的实现
public boolean onTouchEvent(MotionEvent event) {

        
        // 不可用状态下,View 依然会消耗点击事件
        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
            
            return clickable;
        }
        
        // 如果设置了代理,那么就设置代理的方法
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }
    
    
        // 
        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                    
                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            // This is a tap, so remove the longpress check
                            removeLongPressCallback();

                            if (!focusTaken) {
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClick();
                                }
                            }
                        }

                        ....
                    }
                    break;


            }

            return true;
        }

        return false;
    }
复制代码

从上面的代码中可以看出,只要 View 的 CLICKABLE 和 LONG_CLICK 有一个为 true,那么它就会消耗这个事件,即 onTouchEvent 方法返回 true,不管它是不是 DISABLE 状态。

然后当 ACTION_UP 事件发生的时候,会触发 performClick 方法,如果 View 设置了 onClickListener,那么 performClick 方法内部就会调用它的 onClick 方法。如下所示:

public boolean 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;
        }

        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

        notifyEnterOrExitForAutoFillIfNeeded(true);

        return result;
    }
复制代码

到这里,点击事件的源码分析就结束了。

问题解答

问题 1:activity、 ViewGroup和 View 都不消费 ACTION_DOWN,那么 ACTION_MOVE 和 ACTION_UP 事件是怎么传递的?

  • 首先,如果大家都不消费 ACTION_DOWN,那么 ACTION_DOWN 的事件传递流程是这样的:
-> Activity.dispatchTouchEvent() 
-> ViewGroup1.dispatchTouchEvent() 
-> ViewGroup1.onInterceptTouchEvent()
-> view1.dispatchTouchEvent() 
-> view1.onTouchEvent() 
-> ViewGroup1.onTouchEvent() 
-> Activity.onTouchEvent();
复制代码
  • 接着,由于大家都不消费 ACTION_DOWN,对于 ACTION_MOVE 和 ACTION_UP 的事件传递是这样的
-> Activity.dispatchTouchEvent()
-> Activity.onTouchEvent();
-> 消费
复制代码
  • 完整的事件分发走向
Android 事件分发机制的理解

问题 2:在 ViewGroup 中的 onTouchEvent 中消费 ACTION_DOWN 事件(onInterceptTouch 默认设置),那么 ACTION_MOVE 和 ACTION_UP 事件是怎么传递的?

  • 首先,我们先分析一下 ACTION_DOWN 的事件走向,由于 ViewGroup 中的 onInterceptTouch 是默认设置的,那么 ACTION_DOWN 的事件最终在 ViewGroup 中的 onTouchEvent 方法中停止了,事件走向是这样的:
-> Activity.dispatchTouchEvent() 
-> ViewGroup1.dispatchTouchEvent() 
-> ViewGroup1.onInterceptTouchEvent()
-> view1.dispatchTouchEvent() 
-> view1.onTouchEvent() 
-> ViewGroup1.onTouchEvent() 
复制代码
  • 接着 ACTION_MOVE 和 ACTION_UP 的事件分发流程,之后 onInterceptTouch 和 View 中的方法都不会被调用了,事件分发如下:
-> Activity.dispatchTouchEvent() 
-> ViewGroup1.dispatchTouchEvent() 
-> ViewGroup1.onTouchEvent() 
复制代码
  • 完整的事件分发走向
Android 事件分发机制的理解

在一个列表中,同时对父 View 和子 View 设置点击方法,优先响应哪个?为什么会这样?

答案是优先响应子 view,原因很简单,如果先响应父 view,那么子 view 将永远无法响应,父 view 要优先响应事件,必须先调用 onInterceptTouchEvent 对事件进行拦截,那么事件不会再往下传递,直接交给父 view 的 onTouchEvent 处理。

为什么子 View 不消费 ACTION_DOWN,之后的所有事件都不会向下传递了。

答案是:mFirstTouchTarget。当子 view 对事件进行处理的时,那么 mFirstTouchTarget 就会被赋值,若是子 view 不对事件进行处理,那么 mFirstTouchTarget 就为 null,之后 VIewGroup 就会默认拦截所有的事件。我们可以从 dispatchTouchEvent 中找到如下代码,可以看出来,若是子 View 不处理 ACTION_DOWN,那么之后的事件也不会给到它了。

// 检查是否拦截
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                    
                // 省略和问题无关代码
            } else {
            
                // 默认拦截
                intercepted = true;
            }

复制代码

以上所述就是小编给大家介绍的《Android 事件分发机制的理解》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Introduction to Graph Theory

Introduction to Graph Theory

Douglas B. West / Prentice Hall / 2000-9-1 / USD 140.00

For undergraduate or graduate courses in Graph Theory in departments of mathematics or computer science. This text offers a comprehensive and coherent introduction to the fundamental topics of graph ......一起来看看 《Introduction to Graph Theory》 这本书的介绍吧!

SHA 加密
SHA 加密

SHA 加密工具

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具