Android事件分发机制详解

/ Android必知必会 / 没有评论 / 280浏览

引言

理解透彻了Android事件分发机制,不仅面试游刃有余,最重要的是遇到事件冲突时能处理得更加得心应手.

网上有大量相关文章,不过大部分文章对于我个人而言,不是太复杂就是太简单,因此趁着五一放假,自己重新梳理了下,并做一个总结记录.

Android事件分发的本质就是将用户的触摸操作[1]通过责任链模式[2]处理后[3]传递给最终响应该事件序列的对象.

  1. 触摸操作主要包括按下(MotionEvent.ACTION_DOWN),移动(MotionEvent.ACTION_MOVE),抬起(MotionEvent.ACTION_UP),取消(MotionEvent.ACTION_CANCEL)等,这些触摸操作会封装成MotionEvent

  2. 在整个传递链上,主要有Activity,ViewGroup,View这些对象来处理事件

  3. 具体"处理"的方法主要包含ViewGroup.dispatchTouchEvent,ViewGroup.onInterceptTouchEvent,View.dispatchTouchEvent,OnTouchListener.onTouch,View.onTouchEvent,OnClickListener.onClick,OnLongClickListener.onLongClick

对Android事件分发机制的分析,实际上就是对Activity,ViewGroup,View分发事件逻辑的分析.

主流程介绍

在逐个分析之前先上个主流程图,不想看死磕源码的同学熟悉大致流程图也就足够了.

点我放大查看

整个分发路径成U形:

[一] 触摸事件从ActivitydispatchTouchEvent方法传递给ViewGroupdispatchTouchEvent;

[二] ViewGroupdispatchTouchEvent会调用onInterceptTouchEvent方法,判断是否拦截该事件,此时有两种情况:

  1. 如果返回true即拦截,则会调用super.dispatchTouchEvent方法,也就是ViewdispatchTouchEvent方法,接着会进入到[三] [四]的逻辑中.
  2. 如果返回false即不拦截,则会遍历ViewGroup的子元素,判断在触摸区域是否有子元素能处理触摸事件,有的话则调用子元素的dispatchTouchEvent,如果子元素是ViewGroup则重复[二]逻辑,否则同样会进入到[三] [四]的逻辑中.如果mOnTouchListener.onTouch()或者View.onTouchEvent返回flase,则会调用super.dispatchTouchEvent(同[二].1).

[三] 如果View没有设置OnTouchListener,直接进入到[四],否则ViewdispatchTouchEvent方法会先调用mOnTouchListener.onTouch(),此时有两种情况:

  1. 如果onTouch()返回true,则代表onTouchListener响应或消费该事件,后续事件序列都会交由其处理;
  2. 如果onTouch()返回false,则直接进入到[四].

[四] 调用ViewonTouchEvent方法,如果返回true则代表响应或消费该事件,后续事件序列都会交由其处理,而且会根据事件类型可能触发OnClickListener.onClick()OnLongClickListener.OnLongClickListener()

简化流程可以是:

Activity.dispatchTouchEvent()->ViewGroup.dispatchTouchEvent()->ViewGroup.onInterceptTouchEvent()->View.dispatchTouchEvent()->View.onTouchEvent()->ViewGroup.onTouchEvent()->Activity.onTouchEvent()

好了,现在我们再来通过源码逐个分析.

Activity事件分发

    // android.app.Activty
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            // 空实现
            onUserInteraction();
        }
        // getWindow()返回抽象类Window,唯一实现类是PhoneWindow
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        // 如果superDispatchTouchEvent逐层返回上来false,则会执行
        return onTouchEvent(ev);
    }
    // com.android.internal.policy.PhoneWindow
    public boolean superDispatchTouchEvent(MotionEvent event) {
        // mDecor是DecorView的实例,DecorView继承ViewGroup
        return mDecor.superDispatchTouchEvent(event);
    }
    // com.android.internal.policy.DecorView
    public boolean superDispatchTouchEvent(MotionEvent event) {
        // 调用ViewGroup的dispatchTouchEvent方法
        return super.dispatchTouchEvent(event);
    }

ViewGroup事件分发

ViewGroup事件分发逻辑相对而言复杂一点, 我们从dispatchTouchEvent方法开始分段分析

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

从上面代码可以看出,当触摸事件类型是MotionEvent.ACTION_DOWN或者mFirstTouchTarget不为null时会调用onInterceptTouchEvent方法是否拦截事件,从后面的代码可得知,当ViewGroup有子View消费响应事件时,mFirstTouchTarget会被赋值并指向该子元素. 因此当触发ACTION_DOWN时,如果ViewGroup拦截事件或者有子元素消费响应该事件,则接下来的事件序列(ACTION_MOVE,ACTION_UP)都不会调用onInterceptTouchEvent,因为下面条件不成立了

if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null)

当然这里还有一个需要注意的地点就是FLAG_DISALLOW_INTERCEPT标志位,可以通过调用ViewGroup.requestDisallowInterceptTouchEvent()方法来设置该标志位, 来控制是否允许调用onInterceptTouchEvent方法,一般会在子元素中调用父ViewGrouprequestDisallowInterceptTouchEvent方法,反向控制父ViewGroup拦截事件的逻辑.不过每次触发MotionEvent.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();
            }

接下来我们再看看ViewGroup不拦截事件时的逻辑(精简抽取的关键代码)

                    final float x = ev.getX(actionIndex);
                    final float y = ev.getY(actionIndex);
                    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);
                        // 判断child是否在触摸事件区域并且能接受事件
                        if (!canViewReceivePointerEvents(child)
                                || !isTransformedTouchPointInView(x, y, child, null)) {
                            ev.setTargetAccessibilityFocus(false);
                            continue;
                        }
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                            /* .....省略一些代码.....*/
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            /* .....省略一些代码.....*/
                            break;
                        }
                }

遍历子元素,并且通过canViewReceivePointerEventsisTransformedTouchPointInView判断子元素是否能接受事件, 如果存在能接受事件的子元素 child,则调用dispatchTransformedTouchEvent方法,在该方法内部会判断child是否为null,因为此时child不为null,所以会调用子元素的dispatchTouchEvent继续分发事件.

            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }

如果子元素dispatchTouchEvent返回true(具体怎么分发我们暂且不管),dispatchTransformedTouchEvent也会返回true,那么mFirstTouchTarget会通过addTouchTarget方法赋值.

如果没有子元素能接受响应该事件,比如没有子元素;或者子元素不在响应区域;再或者子元素dispatchTouchEvent返回false;则mFirstTouchTarget不会赋值,紧接着的代码调用dispatchTransformedTouchEvent方法时,childnull,通过之前的分析可以知道,此时会调用super.dispatchTouchEvent方法,从而转到ViewdispatchTouchEvent方法中.

 if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            }

View事件分发

ViewdispatchTouchEvent方法中会判断是否设置了onTouchListener,如果设置了并且onTouchListener.onTouch返回true,则ViewonTouchEvent就没机会执行了,这也意味着onTouchListener.onTouch的优先级会比View.onTouchEvent高.

            // android.view.View#dispatchTouchEvent
            /*....省略一些代码.....*/
            //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;
            }

如果没有设置onTouchListener,或者onTouchListener.onTouch返回false,则会调用View.onTouchEvent方法.

首先会获取Viewclickable状态, View不同子类的状态不同,比如Button默认是true,TextView默认是false,当然也可以通过setOnClicklistenersetLongClickListener来改变影响clickable的值.

接着会判断View的可用状态, 如果View是不可用的,则直接返回clickable,也意味着虽然View看起来是不可用(不可点击),但是它还是有可能会消费响应事件.

        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

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

当手指按下触发MotionEvent.ACTION_DOWN时,会postDelayed延迟ViewConfiguration.getLongPressTimeout()毫秒执行一个CheckForLongPress 类型的Runnable,如果延时完成后View还是press状态, 则会判断View是否设置了OnLongClickListener,如果设置了则执行OnLongClickListener.onLongClick,onLongClick的返回值会赋值给mHasPerformedLongPress, 如果mHasPerformedLongPresstrue,则当手指抬起触发ACTION_UP时,不会尝试响应点击事件.

        switch (action) {
            case MotionEvent.ACTION_UP:
                /*......省略.....*/
                if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                    if (mPerformClick == null) {
                        mPerformClick = new PerformClick();
                    }
                    if (!post(mPerformClick)) {
                        performClickInternal();
                    }

                }                /*......省略.....*/
                break;
            case MotionEvent.ACTION_DOWN:
                /*......省略.....*/
                checkForLongClick(0, x, y);
                /*......省略.....*/
                break;
        }
private final class CheckForLongPress implements Runnable {

    @Override
    public void run() {
        if ((mOriginalPressedState == isPressed()) && (mParent != null)
                && mOriginalWindowAttachCount == mWindowAttachCount) {
            // performLongClick(mX, mY)
            // ->performLongClick()
            // ->performLongClickInternal(mLongClickX, mLongClickY)
            // ->li.mOnLongClickListener.onLongClick(View.this)
            if (performLongClick(mX, mY)) {
                mHasPerformedLongPress = true;
            }
        }
    }
}

当手指抬起触发MotionEvent.ACTION_UP时,如果View没有设置onLongClickListener或者没有触发长按事件再或者onLongClickListener.onLongClick返回flase,(简单点说就是mHasPerformedLongPressfalse时),会触发点击事件,如果设置了onClickListener,会调用onClickListener.onClick方法

    private final class PerformClick implements Runnable {
        @Override
        public void run() {
            // ->performClick()
            // ->li.mOnClickListener.onClick(this)
            performClickInternal();
        }
    }