Android事件分发机制

之前在面试的过程中遇到了Android事件分发的相关面试题,其实之前也看了一些源码性的东西,但是感觉脑子里对这个东西还是比较乱,但是一些面试题还是答的不太好,故此自己来总结一下。

Android事件分发机制

当我们点击屏幕之后,就产生了点击事件,这个事件被封装成了一个类:MotionEvent,之后系统将MotionEvent传递给View层级,在View层级中的传递过程就是点击事件的分发。

当我们点击事件产生之后,事件首先会传递给当前的Activity,调用Activity的dispatchTouchEvent,之后交给Activity中的PhoneWindow来处理,PhoneWindow再将事件交给DecorView,DecorView又交给根ViewGroup来处理,所以我们先来看ViewGroup中的dispatchToucheEvent.

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        ...
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                //如果是按下事件,则放弃先前所有的状态    
                //由于程序切换,ANR或者一些其他的状态改变,可能放弃上一个手势的up或者cancel事件
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }

            // 检查拦截
            final boolean intercepted;
            //如果是按下事件或者没有拦截(如果拦截mFirstTouchTarget为null)
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                //禁止ViewGroup拦截除了DOWN之外的事件
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;、
                //如果拦截了事件
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); //如果已更改请还原操作
                } else {
                    intercepted = false;
                }
            } else {
                //没有触摸目标,并且此动作不是最初的,因此继续拦截触摸
                intercepted = true;
            }
            //除按下事件的其他事件
            ...
        return handled;
    }

 

在ViewGroup中,首先判断是否是按下事件DOWN,如果是则清除之前的所有状态重新开始。接着检查是否要拦截。接着判断如果是按下事件或者mFirstTouchTarget是否为null(如果拦截了则会将mFirstTouchTarget=null,如果没有拦截则则交给子View处理,则mFirstTouchTarget!=null),之后将标志位设为之只拦截DOWN事件,之后执行onInterceptTouchEvent,如果触发的是MOVE、UP等事件则不会执行该方法。

接下来看看onInterceptTouchEvent()方法:

    public boolean onInterceptTouchEvent(MotionEvent ev) {
        //符合所有条件才会拦截
        if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
                && ev.getAction() == MotionEvent.ACTION_DOWN
                && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
                && isOnScrollbarThumb(ev.getX(), ev.getY())) {
            return true;
        }
        //默认返回false不拦截
        return false;
    }

onInterceptTouchEvent默认返沪false不进行拦截,如果你想要拦截则应该在自定义的ViewGroup中重写onInterceptTouchEvent方法。

我们来看看dispatchToiuchEvent()其他源码:

//获取到所有子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);
                    //触摸点没有在子View范围内
                   if (childWithAccessibilityFocus != null) {
     
                       if (childWithAccessibilityFocus != child) {
                                    continue;
                                }
                                childWithAccessibilityFocus = null;
                                //下一个child
                                i = childrenCount - 1;
                            }
                            //是否在子View的范围内或者是否子View是否在播放动画
                            if (!child.canReceivePointerEvents()
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }
                            //获取到符合条件的View
                            newTouchTarget = getTouchTarget(child);
                            if (newTouchTarget != null) {
     
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }

                            resetCancelNextUpFlag(child);
                            //关键代码
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                               
                                mLastTouchDownTime = ev.getDownTime();
                                if (preorderedList != null) {
                                   
                                    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;
                            }

                            ev.setTargetAccessibilityFocus(false);
                        }

通过倒序循环(从最外层View到内层View遍历)来找到符合条件的子View,首先判断子View是否能获取焦点,如果能接收到则交由子View来处理,接着判断子View是否在范围内或者是否在播放动画,如果均不符合跳过当前继续寻找下一个,一直找到合适的子View。

我们看一下注释标注的关键代码dispatchTransformedTouchEvent:

    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;

        final int oldAction = event.getAction();
        
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
            event.setAction(MotionEvent.ACTION_CANCEL);
            //如果没有子View则调用父类的dispatchTouchEvent,有子View则会调用自己的dispatchTouchEvent
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }
    }

官方注释:将运动的事件转换为特定子视图的坐标空间,过滤掉无关的point ids,并在必要时覆盖其动作,如果child为null,则将事件发给父类。

在该方法中就是判断是否有子View,有则调用子View自身的dispatchTouchEvent,没有则会调用父类的dispatchTouchEvent。

那我们就来看一看View的dispatchTouchEvent:

    public boolean dispatchTouchEvent(MotionEvent event) {
        // 如果改事件有可访问性
        if (event.isTargetAccessibilityFocus()) {
            //没有焦点或者没有虚拟后代,则不处理
            if (!isAccessibilityFocusedViewOrHost()) {
                return false;
            }
            // 我们获取到事件并开始分发
            event.setTargetAccessibilityFocus(false);
        }

        ...
        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            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;
            }
        }

        ...
    }

View中的dispatchTouchEvent最关键就是注释代码。

如果onTouchListener不为null并且onTouch方法返回true,则表示事件被消费,就不会执行onTouchEvent(event),否则会执行onTouchEvent。可以看出onTouchListener中的onTouch()优先级高于onTouchEvent();

我们来看一看onTouchEvent方法:

    public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();

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

        ...

        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    ...
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        ...
                        boolean focusTaken = false;
                        ...

                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            removeLongPressCallback();
                            if (!focusTaken) {
                               
                                if (mPerformClick == null) {
                                    //关键代码
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClickInternal();
                                }
                            }
                        }

                       ...

            return true;
        }

        return false;
    }

我们在这里主要分析switch中的ACTION_UP,只要View中的CLICKABLE和LONG_CLICKABLE有一个为true,那么onTouchEvent就会就会消耗掉这个事件。通过View的setClickable和setLongClickable来设置也可以通过View的setOnClickLisntenr和setOnLongClickListener来设置,他们会自动将View设置为CLICKABLE和LONG_CLICKABLE.

我们看一下关键代码PerformClick:

    public boolean performClick() {

        notifyAutofillManagerOnClick();

        final boolean result;
        final ListenerInfo li = mListenerInfo;
        //如果View设置了点击事件就执行onClick
        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;
    }

在performClick中如果View设置了onClickListener则会执行onClick方法。

 

总结

首先我们按下手机屏幕,产生点击事件,Activity最先收到点击事件,之后发给PhoneWindow中的DecorView中的dispatchTouchEvent来处理,也就是发送到了ViewGroup的dispatchTouchEvent,发送到ViewGroup中首先判断是否是按下事件,按下之后判断是否拦截事件,默认不拦截,拦截的话需要我们重写onInterceptor方法。之后不拦截之后继续向下执行ViewGroup中的dispatchTouchEvent,倒序循环找到合适的子View,找到之后判断如果有子View则调用子View的dispatchTouchEvent,如果ViewGroup没有子View则会调用父类的dispatchTouchEvent,之后进入View的dispatchTouchEvent,之后判断onTouchListener是否为null以及onTouchListener.ouTouch方法返回true(在这里可以看出onTouch优先级高于onTouchEvent),如果不为null且onTouch返回true标识事件被消费,否则会调用onTouchEvent方法,在onTouchEvent中只要View的CLICKABLE或者LONG_CLICKABLE有一个为true,就是点击事件以及长按事件有一个为true就会消耗掉事件,一般我们通过设置点击事件setOnClickListenere以及长按事件setOnLongClickListener。之后再ACTION_UP中调用performClick,在该方法内部中如果设置了点击事件onClick则会被执行

点击事件分发的传递规则

首先点击事件是由上而下的传递规则,当点击事件产生之后会由Activity来处理,传递给PhoneWindow,在传递给DecorView,最后传递给顶层的ViewGroup。传递给ViewGroup一般我们都考虑他是否拦截,如果拦截方法onInterceptTonchEvent返回true则不会向下传递,就交给ViewGroup的onTouchEvent来处理;如果onInterceptorTouchEvent返回false则不拦截,事件继续向下传递,传递给子View的dispatchTouchEvent(),如果View没有子View就会调用View的dispatchTouchEvent,一般情况下最终会调用View的onTouchEvent来处理事件

如果子View没有处理掉事件则会由下而上传递,点击事件传递到最底层的View时,如果该View的onTouchEvent返回true则表示处理并消费掉事件;如果返回false则表示该View不处理,继续向上传递,交给父View的onTouchEvent来处理,如果夫View的onTouchEvent返回true表示事件被消耗;如果返回false继续向上传递,如此反复

图片引用:https://www.jianshu.com/p/d3758eef1f72

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

后厂村三环十三少

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值