View 事件分发机制

View 事件简介

View 事件,既 MotionEvent,是用户触摸屏幕的一系列事件。同一事件序列是从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中所产生的一系列事件,这个事件序列以 ACTION_DOWN 开始,中间含有一系列的 ACTION_MOVE,最终以 ACTION_UP 结束。

View 事件分发简述

当用户点击屏幕的时候,TouchEvent 最先传递给Activity.dispatchTouchEvent(MotionEvent),然后再调用DecorView.superDispatchTouchEvent(MotionEvent),接着直接调用super.dispatchTouchEvent(MotionEvent),即ViewGroup.dispatchTouchEvent(MotionEvent)

ViewGroup 和 View 分发事件,有三个很重要的方法:

  1. public boolean dispatchTouchEvent(MotionEvent ev):用来进行事件的分发,如果事件能够传递给当前 View,那么该方法一定会调用,返回值受当前 View 的 onTouchEvent 和下级 View 的 dispatchTouchEvent 的影响,表示是否消耗当前事件;
  2. public boolean onInterceptTouchEvent(MotionEvent ev):表示在上述方法内部调用,判断当前 View 是否拦截某个事件。如果当前 View 拦截了某个事件,那么同一个事件序列当中,此方法不会被再次调用;反之,如果是下级 View 拦截了事件,并且下级 View 没有调用public void requestDisallowInterceptTouchEvent(boolean disallowIntercept),那么当前 View 的onInterceptTouchEvent(MotionEvent ev)会被继续调用,即当前 View 依旧拥有拦截后续事件的能力;
  3. public boolean onTouchEvent(MotionEvent event):在 dispatchTouchEvent中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前 View 无法再次接收到事件。

上述三个方法的关系大致可以用下面的伪代码来表示:

public boolean dispatchTouchEvent(MotionEvent ev){
    boolean consume=false;
    if(onInterceptTouchEvent(ev)){
        consume=onTouchEvent(ev);
    }else{
        cousume=child.dispatchTouchEvent(ev);
    }
    returen consume;
}

dispatchTouchEvent搭建了事件分发的框架,一般不需要重写,自定义 View 时通常重写的是onInterceptTouchEventonTouchEvent。事件的分发有点类似有序树的查找算法,ViewGroup 就是结点,View 就是叶子。遍历到 View 的时候,就要返回了。

源码分析

ViewGroup

dispatchTouchEvent

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

当接收到 ACTION_DOWN 事件的时候,会重置一下状态(比如说重置FLAG_DISALLOW_INTERCEPT状态位,允许拦截事件),清除之前的 TouchTarget。

TouchTarget :Describes a touched view and the ids of the pointers that it has captured.

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

此段代码是判断当前 ViewGroup 是否要拦截事件,首先介绍几个变量的含义:

  • mFirstTouchTarget:touch target list 的第一项,表示上一个处理事件的 child;
  • disallowIntercept:是否允许当前 ViewGroup 拦截事件,默认是允许,其 child 可以设置为不允许;

由上述代码可知,如果 MotionEvent 不是 ACTION_DOWN 且 child 没有处理上一个事件,则 ViewGroup 会拦截下事件;否则会调用 onInterceptTouchEvent 来判断是否需要拦截事件(除非 child 不允许 view parent 拦截事件)。

            if (!canceled && !intercepted) {
                ......
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    .....
                    //当 ViewGroup 不拦截事件,且事件为 DOWN 类型,那么就要遍历其 child,寻找处理事件的 child。
                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        ......
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);
                            //能够接受事件的child:事件坐标在 View 内;View 可见或者没有在执行动画
                            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;
                            }
                            //
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // Child wants to receive touch within its bounds.
                                ......
                                //用 child 获得新的 TouchTarget,并将其添加到 Touch Target list 的第一个;
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }
                        }
                    }
                }
            }

当 ViewGroup 不拦截事件时,且事件类型为 DOWN 时,ViewGroup 会遍历其 child,寻找能接收事件的 child,然后调用dispatchTransformedTouchEvent将事件传递给 child 进行处理。具体逻辑处理可以看代码中的注释。

            // 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);
            } else {
                // Dispatch to touch targets, excluding the new touch target if we already
                // dispatched to it.  Cancel touch targets if necessary.
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;
                    ......
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        ......                    
                        }
                }
            }

如果没有 child 要处理事件,那么就ViewGroup 自身尝试处理事件;如果有,那么遍历 Touch Target list ,每一个child 都尝试处理事件。

下面让我们看一下dispatchTransformedTouchEvent的代码:

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;
        ......
        // If the number of pointers is the same and we don't need to perform any fancy
        // irreversible transformations, then we can reuse the motion event for this
        // dispatch as long as we are careful to revert any changes we make.
        // Otherwise we need to make a copy.
        //根据 pointerId、child 的偏移量对 MotionEvent 进行转换
        final MotionEvent transformedEvent;
        if (newPointerIdBits == oldPointerIdBits) {
            if (child == null || child.hasIdentityMatrix()) {
                if (child == null) {
                    handled = super.dispatchTouchEvent(event);
                } else {
                    final float offsetX = mScrollX - child.mLeft;
                    final float offsetY = mScrollY - child.mTop;
                    event.offsetLocation(offsetX, offsetY);

                    handled = child.dispatchTouchEvent(event);

                    event.offsetLocation(-offsetX, -offsetY);
                }
                return handled;
            }
            transformedEvent = MotionEvent.obtain(event);
        } else {
            transformedEvent = event.split(newPointerIdBits);
        }
          //如果 child 为null,那么调用 super.dispatchTouchEvent,即 View 的 dispatchTouchEvent 方法,
          //看能否消耗该事件,否则,事件传递给 child 进行处理
        // Perform any necessary transformations and dispatch.
        if (child == null) {
            handled = super.dispatchTouchEvent(transformedEvent);
        } else {
            ......
            handled = child.dispatchTouchEvent(transformedEvent);
        }

        // Done.
        transformedEvent.recycle();
        return handled;
    }

逻辑不复杂,主要就是对 MotionEvent 进行根据 pointerId 、child 的偏移量进行转换,然后如果 child 不为null,那么调用 child 的 dispatchTouchEvent,将事件传递给 child 进行处理,如果 child 为null,那么调用 super.dispatchTouchEvent,即viewgroup尝试处理该事件。

上面就是 ViewGroup 分发事件基本框架代码,大致可以总结如下:

  1. 如果事件是 DOWN 类型,先判断当前 ViewGroup 是否拦截该事件(通常 ViewGroup 是不会拦截 DOWN 事件,否则child 完全接受不到事件了);如果不拦截事件,那么遍历其 child,寻找处理该事件的 child。如果没有 child 处理事件,那么 ViewGroup 自行尝试处理该事件,否则 child 处理该事件,并将 mFirstTouchTarget 设置为该 child;
  2. 事件不是 DOWN,且之前的事件没有 child 进行处理(mFirstTouchTarget 为 null),那么ViewGroup 尝试处理该事件;如果之前的事件有 child 进行处理了,那么先判断 ViewGroup是否需要拦截该事件,不需要,则直接交由之前处理事件的 child 直接处理。

View

dispatchTouchEvent

不同于 ViewGroup,View 不能再向下分发事件,要么自身处理事件,要么不处理返回给 parent处理,相当于树中的叶子,所以 View 没有 onInterceptTouchEvent 方法,它的 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 是否设置了 OnTouchListener,如果是,则将事件交给它处理,否则才会调用 VIew 的 onTouchEvent 方法。可见 OnTouchListener 的优先级高于 View 的 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);
        }
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return 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)) {
                                    performClick();
                                }
                            }
                        }

                        if (mUnsetPressedState == null) {
                            mUnsetPressedState = new UnsetPressedState();
                        }

                        if (prepressed) {
                            postDelayed(mUnsetPressedState,
                                    ViewConfiguration.getPressedStateDuration());
                        } else if (!post(mUnsetPressedState)) {
                            // If the post failed, unpress right now
                            mUnsetPressedState.run();
                        }

                        removeTapCallback();
                    }
                    mIgnoreNextUpEvent = false;
                    break;

                case MotionEvent.ACTION_DOWN:
                    mHasPerformedLongPress = false;

                    if (performButtonActionOnTouchDown(event)) {
                        break;
                    }

                    // Walk up the hierarchy to determine if we're inside a scrolling container.
                    boolean isInScrollingContainer = isInScrollingContainer();

                    // For views inside a scrolling container, delay the pressed feedback for
                    // a short period in case this is a scroll.
                    if (isInScrollingContainer) {
                        mPrivateFlags |= PFLAG_PREPRESSED;
                        if (mPendingCheckForTap == null) {
                            mPendingCheckForTap = new CheckForTap();
                        }
                        mPendingCheckForTap.x = event.getX();
                        mPendingCheckForTap.y = event.getY();
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    } else {
                        // Not inside a scrolling container, so show the feedback right away
                        setPressed(true, x, y);
                        checkForLongClick(0, x, y);
                    }
                    break;

                case MotionEvent.ACTION_CANCEL:
                    setPressed(false);
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    break;

                case MotionEvent.ACTION_MOVE:
                    drawableHotspotChanged(x, y);

                    // Be lenient about moving outside of buttons
                    if (!pointInView(x, y, mTouchSlop)) {
                        // Outside button
                        removeTapCallback();
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                            // Remove any future long press/tap checks
                            removeLongPressCallback();

                            setPressed(false);
                        }
                    }
                    break;
            }

            return true;
        }

        return false;

由上可知,View 的 onTouchEvent 的处理逻辑大致如下:

  1. 只要 View 时 clickable 的,总是返回 true,即总是消耗事件;
  2. 如果 View 时 disabled 的,那么直接返回 TRUE,照样消耗事件,但是不执行相应的动作;
  3. 如果 View 设置的 Delegate,那么直接把事件交给 Delegate处理(利用这点可以将事件交给其他 View 来处理);
  4. 如果 View 自己处理事件,那么根据事件的类型处理方法如下:
  • DOWN:设置状态为 pressed,同时设置一个延时任务,用以判断 longClick;
  • MOVE:判断事件是否超过了 View 的边界,如果是,重置状态,取消 longClick 的延时任务等;
  • UP:如果状态为 pressed,那么执行 performClick,并且重置状态;
  • CANCEL:重置状态

实战分析

对于自定义 View,如何正确地分发、处理事件非常重要,下面就大致说说 ViewGroup 和 View 分别是如何重写相关方法,以实现需求。

View

对于 View,因为不需要分发事件,所以 View 一般只需要重写 onTouchEvent,然后根据事件类型分情况处理:

  • Donw:一定要返回 TRUE,否则同一序列的后续事件都不会交给这个 View 处理了;
  • Move:通常是我们处理的关键,根据它来进行相应的逻辑处理,比如说移动 View,绘画之类的;
  • Up:重置状态,资源回收等处理;

ViewGroup

对于 ViewGroup,首先是要重写 onInterceptTouchEvent:

  • Down:返回 FALSE,这样 child 才有可能接收到事件并进行处理;
  • Move:根据需要进行判断返回 TRUE or FALSE,返回 TRUE,说明 ViewGroup 要拦截事件,交由其 onTouchEvent 进行处理,FALSE 则继续给 child 进行处理;
  • Up:返回 FALSE,这样child 才有可能接收到 Up 事件,进行相应的处理;

onTouchEvent:

  • 返回TRUE,表示 ViewGroup 消耗了事件,后续事件都会交由它处理;
  • 返回 FALSE,表示 ViewGroup 不再消耗事件,但是,如果ViewGroup dispatchTouchEvent(DownEvent)的时候返回了 TRUE,即消耗了 DOWN 事件,那么就算他不消耗后续事件,后续事件依然会传递给它,而不会交给 parent 来处理,最终这些未处理的事件会在 Activity 中处理;如果没有消耗过 DOWN 事件,那么事件会直接交给 parent 来处理(因为 mFirstTouchTarget 为 null);
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容