Android 事件传递机制总结

36 篇文章 0 订阅

Android 事件传递机制总结

Android View虽然不是四大组件,但是其重要程度堪比四大组件。初级工程师到中高级工程师,这些都是很重要的,因为事件分发机制面试也会经常被提及,如果我们能get到要领,并跟面试官深入的交流一下,那么一定会让面试官对我们印象深刻,发放offer。 就为了这个我们刨根问底深入学习一下事件传递机制,也是很值得的。

下面我们就从以下几个部分分析一下事件传递机制:

  1. Activity View 树的结构构成
  2. View 的事件传递过程分析
  3. ViewGroup的事件传递过程分析
  4. 事件传递机制流程图
  5. 总结
  6. 参考资料

前言

在我们研究事件传递机制之前,先了解一下MotionEvent。

因为我们对屏幕的点击,滑动,抬起等一系的动作都是由一个一个MotionEvent对象组成的。
根据不同动作,主要有以下三种事件类型:

  • 1.ACTION_DOWN:手指刚接触屏幕,按下去的那一瞬间产生该事件
  • 2.ACTION_MOVE:手指在屏幕上移动时候产生该事件
  • 3.ACTION_UP:手指从屏幕上松开的瞬间产生该事件

从 ACTION_DOWN 开始到 ACTION_UP 结束我们称为一个事件序列。

正常情况下,我们日常的各种操作, 最终呈现在MotionEvent上来讲无外乎下面两种。

1.点击后抬起,也就是单击操作:ACTION_DOWN -> ACTION_UP 当然也包括 ACTION_DOWN -> duration -> ACTION_UP longclick

2.点击后再滑动一段距离,再抬起:ACTION_DOWN -> ACTION_MOVE -> … -> ACTION_MOVE -> ACTION_UP

看一下日志,

点击事件日志

1. Activity View 树的结构构成

说事件传递流程之前我们还需要了解一下,从手指触碰到屏幕起, 事件是如何从最外层的Activity 传递至了指定的View之上。当然这前提是对Activity , View树结构的充分了解。
放一张图,
activity view 结构图

我们可以看到最外层是Activity , 里边是window ,关联的是Window的唯一实现类PhoneWindow, PhoneView内部装载了DecorView, 而DecorView是继承自FrameLayout的,同时FrameLayout是继承自ViewGroup 继承自View 一路下来的 。 所以我们需要进一步了解一下结构是如何的。事件是为什么这样的。

下边我就粘一些主要的代码来简单说明一下上边的这个图和一段话 。

    final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window, ActivityConfigCallback activityConfigCallback) {
        attachBaseContext(context);

        mFragments.attachHost(null /*parent*/);
        //  在Activity 类的attach方法中 mWindow 赋值为PhoneWindow
        mWindow = new PhoneWindow(this, window, activityConfigCallback);
        mWindow.setWindowControllerCallback(this);
        mWindow.setCallback(this);
        mWindow.setOnWindowDismissedCallback(this);
        mWindow.getLayoutInflater().setPrivateFactory(this);

ContentView方法 通过layoutresid进行设置ContentView

  @Override
    public void setContentView(int layoutResID) {
        // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
        // decor, when theme attributes and the like are crystalized. Do not check the feature
        // before this happens.
        if (mContentParent == null) {
            /// 在这里将DecorView和Activity关联在了一起
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                    getContext());
            transitionTo(newScene);
        } else {
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }
    

装载DecorView

  private void installDecor() {
        mForceDecorInstall = false;
        if (mDecor == null) {
            mDecor = generateDecor(-1);
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            }
        } else {
            mDecor.setWindow(this);
        }
        if (mContentParent == null) {
            mContentParent = generateLayout(mDecor);
        }

generateLayout的源码就不再粘了, 主要作用就是根据不同的情况加载不同的布局给DecorView

下边看一下生成DecorView

  protected DecorView generateDecor(int featureId) {
        // System process doesn't have application context and in that case we need to directly use
        // the context we have. Otherwise we want the application context, so we don't cling to the
        // activity.
        Context context;
        if (mUseDecorContext) {
            Context applicationContext = getContext().getApplicationContext();
            if (applicationContext == null) {
                context = getContext();
            } else {
                context = new DecorContext(applicationContext, getContext().getResources());
                if (mTheme != -1) {
                    context.setTheme(mTheme);
                }
            }
        } else {
            context = getContext();
        }
        /// 在这里创建了DecorView 
        return new DecorView(context, featureId, this, getAttributes());
    }
    

通过上边的源码分析大概了解Activity View树的一个结构和关联关系。 下边继续研究View和ViewGroup的事件传递

2. View 的事件传递过程分析

我们查看一下View中的事件分发代码, 看一下dispatchTouchEvent方法

    /**
     * Pass the touch screen motion event down to the target view, or this
     * view if it is the target.
     
     * MotionEvent事件从父View传递进来, 进入dispatch方法 进行分发。  返回的结果就是是否处理该事件。 
     
     * @param event The motion event to be dispatched.
     * @return True if the event was handled by the view, false otherwise.
     * 返回结果, 
     * true 则该view 处理了这个事件, 否则返回false
     */
    public boolean dispatchTouchEvent(MotionEvent event) {
        // If the event should be handled by accessibility focus first.
        if (event.isTargetAccessibilityFocus()) {
            // We don't have focus or no virtual descendant has it, do not handle the event.
            if (!isAccessibilityFocusedViewOrHost()) {
                return false;
            }
            // We have focus and got the event, then use normal event dispatch.
            event.setTargetAccessibilityFocus(false);
        }

        boolean result = false;

        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }

        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Defensive cleanup for new gesture
            
            stopNestedScroll();
        }
        //如果窗口没有被遮盖
        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            //  这句很有意思 , if 的条件语句里有四个 条件 , 用& 符号连接。& 符号连接的if语句 是从左往右执行的, 一旦有一个false, 后边就不会再执行了。 
            //  所以, 当监听信息为null , 或者 onTouchListener 没有设置, 或者 view 状态的enable状态为disable, 还有 设置了onTouchListener,但其onTouch事件返回的是false时 都消耗不了该事件。  
             换句话说, 默认状态, 一个没有设置监听,或者设置了监听没有修改onTouch返回值的,都不可以消耗该事件。 还有onTouch事件处于if & 语句的最后一个条件语句里, 但是比onTouchEvent靠前, 所以 执行顺序也在onTouchEvent之前。 
            if (    li != null 
                    && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

             当前边没有消耗该事件,并且onTouchEvent执行的结果为true的时候才会处理该事件。 
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }

        if (!result && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        }

        // Clean up after nested scrolls if this is the end of a gesture;
        // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
        // of the gesture.
        if (actionMasked == MotionEvent.ACTION_UP ||
                actionMasked == MotionEvent.ACTION_CANCEL ||
                (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
            stopNestedScroll();
        }

        return result;
    }

下边我们瞜一眼onTouchEvent方法 , 代码比较长, 就不全粘了, 找到ACTION_UP 中的performClick ,

  public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();
        //1.如果View是设置成不可用的(DISABLED)仍然会消费点击事件
        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);
        }
        ...
        //2.CLICKABLE 和LONG_CLICKABLE只要有一个为true就消费这个事件
        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        // take focus if we don't have it already and we should in
                        // touch mode.
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }

                        if (prepressed) {
                            // The button is being released before we actually
                            // showed it as pressed.  Make it show the pressed
                            // state now (before scheduling the click) to ensure
                            // the user sees it.
                            setPressed(true, x, y);
                        }

                        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)) {
                                    //在ACTION_UP方法发生时会触发performClick()方法
                                    performClick();
                                }
                            }
                        }
                        ...
                    break;
            }
            ...
            return true;
        }
        return false;
    }

继续看一下performClick方法

    public boolean performClick() {
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            /// 播放click 按下效果声音
            playSoundEffect(SoundEffectConstants.CLICK);
            /// 执行oNClick方法
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
         当然 如果onClickListener 没有设置为null的话 , 返回值就是false , 也就是不消耗事件。 
            result = false;
        }
        /// 这是无障碍事件的相关代码
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

        notifyEnterOrExitForAutoFillIfNeeded(true);

        return result;
    }

小结:

对于View而言,

  • 如果设置了onTouchListener,并且View是可用的, 那么OnTouchListener方法中的onTouch方法会被回调。
    如果onTouch方法返回true,则onTouchEvent方法不会被调用(onClick事件是在onTouchEvent中调用)
    所以三者优先级是onTouch->onTouchEvent->onClick

  • View 的onTouchEvent 方法默认都会消费掉事件(返回true)
    除非它是不可点击的(clickable和longClickable同时为false),View的longClickable默认为false,clickable需要区分情况,

如Button的clickable默认为true,而TextView, ImageView 的clickable默认为false。

3. ViewGroup的事件传递过程分析

下面我们把ViewGroup的dispatchTouchEvent 的主要逻辑粘在这里,提取的伪代码如下

public boolean dispatchTouchEvent(MotionEvent ev) {
        boolean consume = false;//事件是否被消费
        if (onInterceptTouchEvent(ev)){//调用onInterceptTouchEvent判断是否拦截事件
            consume = onTouchEvent(ev);//如果拦截则调用自身的onTouchEvent方法
        }else{
            consume = child.dispatchTouchEvent(ev);//不拦截调用子View的dispatchTouchEvent方法
        }
        return consume;//返回值表示事件是否被消费,true事件终止,false调用父View的onTouchEvent方法
    }

public boolean dispatchTouchEvent(MotionEvent event)

通过方法名我们不难猜测,它就是事件分发的重要方法。
那么很明显,如果一个MotionEvent传递给了View,那么dispatchTouchEvent方法一定会被调用!

返回值:表示是否消费了当前事件。可能是View本身的onTouchEvent方法消费,也可能是子View的dispatchTouchEvent方法中消费。

  • 返回true表示事件被消费,本次的事件终止。
  • 返回false表示View以及子View均没有消费事件,将调用父View的onTouchEvent方法

public boolean onInterceptTouchEvent(MotionEvent ev)
事件拦截,当一个ViewGroup在接到MotionEvent事件序列时候,首先会调用此方法判断是否需要拦截。

这是ViewGroup特有的方法,View并没有该方法 这里先
返回值:是否拦截事件传递。

  • 返回true表示拦截了事件,那么事件将不再向下分发而是调用View本身的onTouchEvent方法。
  • 返回false表示不做拦截,事件将向下分发到子View的dispatchTouchEvent方法。

public boolean onTouchEvent(MotionEvent ev)
真正对MotionEvent进行处理或者说消费的方法。在dispatchTouchEvent进行调用。
返回值:是否消费该事件。

  • 返回true表示事件被消费,本次的事件终止。
  • 返回false表示事件没有被消费,将调用父View的onTouchEvent方法

继续, 我们一点一点研究源码。

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

子View可以通过requestDisallowInterceptTouchEvent方法干预父View的事件分发过程(ACTION_DOWN事件除外)

当ViewGroup决定拦截事件后,后续事件将默认交给它处理并且不会再调用onInterceptTouchEvent方法来判断是否拦截。
子View可以通过设置FLAG_DISALLOW_INTERCEPT标志位来不让ViewGroup拦截除ACTION_DOWN以外的事件。

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

所以我们onInterceptTouchEvent并非每次都会被调用。
如果要处理所有的点击事件那么需要选择dispatchTouchEvent方法
而FLAG_DISALLOW_INTERCEPT标志位可以帮助我们去有效的处理滑动冲突

    public boolean onInterceptTouchEvent(MotionEvent ev) {
        /// 判断这么几种情况的时候, 是需要拦截该事件,并且交于当前ViewGroup处理该事件。 
        if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
                && ev.getAction() == MotionEvent.ACTION_DOWN
                && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
                /// 当事件的坐标位置处于滚动条上的时候 
                && isOnScrollbarThumb(ev.getX(), ev.getY())) {
            return true;
        }
        return false;
    }

 //判断1,View可见并且没有播放动画。2,点击事件的坐标落在View的范围内
    //如果上述两个条件有一项不满足则continue继续循环下一个View
    if (!canViewReceivePointerEvents(child)
            || !isTransformedTouchPointInView(x, y, child, null)) {
        ev.setTargetAccessibilityFocus(false);
        continue;
    }
        
  //如果有子View处理即newTouchTarget 不为null则跳出循环。
    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;
    }
        
   //dispatchTransformedTouchEvent第三个参数child这里不为null
    //实际调用的是child的dispatchTouchEvent方法  调用方法获取该childview是否消费该事件。 
    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();
        //当child处理了点击事件,那么会设置mFirstTouchTarget 在addTouchTarget被赋值
        newTouchTarget = addTouchTarget(child, idBitsToAssign);
        alreadyDispatchedToNewTouchTarget = true;
        //子View处理了事件,然后就跳出了for循环
        break;
    }

分发过程首先需要遍历ViewGroup的所有子View,可以接收点击事件的View需要满足下面条件。

1.如果View可见并且没有播放动画canViewReceivePointerEvents方法判断

    /**
     * Returns true if a child view can receive pointer events.
     * @hide
     */
    private static boolean canViewReceivePointerEvents(@NonNull View child) {
        return (child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                || child.getAnimation() != null;
    }

2.点击事件的坐标落在View的范围内isTransformedTouchPointInView方法判断

   /**
     * Returns true if a child view contains the specified point when transformed
     * into its coordinate space.
     * Child must not be null.
     * @hide
     */
    protected boolean isTransformedTouchPointInView(float x, float y, View child,
            PointF outLocalPoint) {
        final float[] point = getTempPoint();
        point[0] = x;
        point[1] = y;
        transformPointToViewLocal(point, child);
        //调用View的pointInView方法进行判断坐标点是否在View内
        final boolean isInView = child.pointInView(point[0], point[1]);
        if (isInView && outLocalPoint != null) {
            outLocalPoint.set(point[0], point[1]);
        }
        return isInView;
    }

接着看后面的代码newTouchTarget = getTouchTarget(child);

   /**
     * Gets the touch target for specified child view.
     * Returns null if not found.
     */
    private TouchTarget getTouchTarget(@NonNull View child) {
        for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
            if (target.child == child) {
                return target;
            }
        }
        return null;
    }

如果mFirstTouchTarget ==null , 则传递null 给dispatchTransformedTouchEvent , 当child为null,handled = super.dispatchTouchEvent(event);
所以此时将调用View的dispatchTouchEvent方法,点击事件给了View,也就是不消耗该事件, 层层往上调用。

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

 /**
     * Transforms a motion event into the coordinate space of a particular child view,
     * filters out irrelevant pointer ids, and overrides its action if necessary.
     * If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.
     * 
     */
    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;

        // Canceling motions is a special case.  We don't need to perform any transformations
        // or filtering.  The important part is the action, not the contents.
        final int oldAction = event.getAction();
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
            event.setAction(MotionEvent.ACTION_CANCEL);
            if (child == null) {
            //如果ViewGroup里面没有子控件就交给自己处理(就是一个纯粹的View)
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }
    }

如果它返回为true,那么就break跳出循环,如果返回为false则继续遍历下一个子View。
dispatchTransformedTouchEvent方法可以看到这样的关键逻辑
这里child是我们遍历传入的子View此时不为null,则调用了child.dispatchTouchEvent(event);
我们子View的dispatchTouchEvent方法返回true,表示子View处理了事件,那么我们一直提到的,mFirstTouchTarget 会被赋值

ViewGroup并没有子View或者子View处理了事件,但是子View的dispatchTouchEvent返回了false(一般是子View的onTouchEvent方法返回false)

    /**
     * Adds a touch target for specified child to the beginning of the list.
     * Assumes the target child is not already present.
     * mFirstTouchTarget 就是在addTouchTarget中被赋值,target 添加到list的头部, 这是个链表 。 
     */
    private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        / next
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        / 这个是头
        return target;
    }

TouchTarget 是个 放在View 上的多指触控的ids , 范围是0~31

小结:
ViewGroup会遍历所有子View去寻找能够处理点击事件的子View(可见,没有播放动画,点击事件坐标落在子View内部)最终调用子View的dispatchTouchEvent方法处理事件

如果当子View处理了事件则mFirstTouchTarget 被赋值,并终止子View的遍历,

如果ViewGroup并没有子View或者子View处理了事件,但是子View的dispatchTouchEvent返回了false(一般是子View的onTouchEvent方法返回false)那么ViewGroup会去处理这个事件(本质调用View的dispatchTouchEvent去处理)

4. 事件传递机制流程图

事件传递流程图

5. 总结

ViewGroup是View的子类,也就是说ViewGroup本身就是一个View,但是它可以包含子View(当然子View也可能是一个ViewGroup),所以不难理解,上面所展示的伪代码表示的是ViewGroup 处理事件分发的流程。而View本身是不存在分发,所以也没有拦截方法(onInterceptTouchEvent),它只能在onTouchEvent方法中进行处理消费或者不消费。

6. 参考资料

  1. https://www.jianshu.com/p/238d1b753e65

  2. https://www.jianshu.com/p/fc0590afb1bf

  3. Android开发艺术探索 View事件传递机制

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值