View的事件分发机制

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

1.MotionEvent

MotionEvent类就是记录手指接触屏幕后所产生的一系列的事件,下面看几个常用的事件的类型与含义:

事件 含义
MotionEvent.ACTION_DOWN 手指按下时触发
MotionEvent.ACTION_UP 手指抬起时触发
MotionEvent.ACTION_MOVE 手指移动时触发
MotionEvent.ACTION_CANCEL 事件被拦截时触发
MotionEvent.ACTION_OUTSIDE 手指不在控件区域时触发
  1. 点击事件: ACTION_DOWN-->ACTION_UP
  2. 滑动事件:ACTION_DOWN-->ACTION_MOVE--> ACTION_UP

2.点击事件分发的传递规则

点击事件是由三个很重要的方法来实现的,分别是dispatchTouchEvent(MotionEvent ev)、onInterceptTouchEvent(MotionEvent ev)、onTouchEvent(MotionEvent event)。

  • dispatchTouchEvent(MotionEvent ev)
    用来进行事件的分发。如果事件能够传递给当前View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent方法的影响,表示是否消耗当前事件。
  • onInterceptTouchEvent(MotionEvent ev)
    在上述方法内部调用,用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列中,此方法不会被再次调用,返回结果表示是否拦截当前事件。
  • onTouchEvent(MotionEvent event)
    在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件。
    这三个方法就是事件分发机制中的核心三个方法,也是我们下面在源码中重要去分析的三个方法。他们三者之间的关系可以概述如下 (注意这是一段伪代码,只是为了对解释三个方法关系)
    //点击事件产生后
    // 步骤1:调用dispatchTouchEvent()
    public boolean dispatchTouchEvent(MotionEvent ev) {
        boolean consume = false; //代表 是否会消费事件
        // 步骤2:判断是否拦截事件
        if (onInterceptTouchEvent(ev)) {
            // a. 若拦截,则将该事件交给当前View进行处理
            // 即调用onTouchEvent ()方法去处理点击事件
            consume = onTouchEvent(ev);
        } else {
            // b. 若不拦截,则将该事件传递到下层
            // 即 下层元素的dispatchTouchEvent()就会被调用,重复上述过程
            // 直到点击事件被最终处理为止
            consume = child.dispatchTouchEvent(ev);
        }
        // 步骤3:最终返回通知 该事件是否被消费(接收 & 处理)
        return consume;
    }

根据上面这段伪代码能够很好的理解三者的关系。当我们点击事件产生后,它的传递过程如下顺序:Activity->ViewGroup->View。即事件总是先传递给Activity,Activity传递给ViewGroup, 再由ViewGroup传递给View。顶级View接收到事件后,就会按照事件分发机制去分发事件。
对于一个根ViewGroup,点击事件产生后,首先会传递给它,这时会调用它的dispatchTouchEvent方法,如果这个ViewGroup的onInterceptTouchEvent返回true,就表示要拦截当前事件,接着事件就会交给这个ViewGroup来处理,即它的onTouchEvent方法被调用;返回false,就表示当前事件不需要被拦截,当前时间就会被继续传递给它的子元素,接着会调用子元素的dispatchTouchEvent方法,如此返回直到事件最终被处理。

为什么Activity向下分发第一个就是ViewGroup,如果我们布局中只有一个简单View控件(如TextView)呢?因为我们布局加载中的顶级View是DecorView(继承FrameLayout),他本是就是一个ViewGroup。

3.源码分析

上面我们分析了View的事件分发机制,下面我们从源码的角度去学习分析。

3.1 Activity对点击事件的分发过程

点击事件使用MotionEvent来表示,当点击事件时,事件最先传递给当前的Activity,由Activity的dispatchTouchEvent来进行事件派发,具体工作是由Activity内部的Window来完成的。Window会将事件传递给DecorView,DecorView一般就是当前界面的底层容器(即setContentView 所设置的View的父容器),通过Activity.getWindow.getDecorView()可以获得。
关于activity的构成学习可以参考
刘望舒 Android View体系(六)从源码解析Activity的构成
下面看下Activity的dispatchTouchEvent方法:

public boolean dispatchTouchEvent(MotionEvent ev) {
    // 一般事件列开始都是DOWN事件 = 按下事件,故此处基本是true
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        //当此activity在栈顶时,触屏点击按home,back,menu键等都会触发此方法
        onUserInteraction();
    }
    //注释1
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    //当一个点击事件未被Activity下任何一个View接收或处理
    //或是发生在Window边界外的触摸事件就会调用
    //注释2
    return onTouchEvent(ev);
}

分析上面的代码,从注释1处可以看出事件开始交给Activity的Window进行分发的。返回true,整个事件循环就结束了,返回false意味着没人处理,即调用Activity的onTouchEvent方法。
从注释1处点击去就是Window#superDispatchTouchEvent(),这是一个抽象方法。我们得找到它的实现类。

  //Window#superDispatchTouchEvent()
  public abstract boolean superDispatchTouchEvent(MotionEvent event);

而Window的实现类也就是PhoneWindow,我们看下PhoneWindow的superDispatchTouchEvent方法,其源码如下所示:

  // com.android.internal.policy.PhoneWindow 
 //PhoneWindow#superDispatchTouchEvent()
 @Override
 public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
 }

mDecor是PhoneWindow中一个DecorView类型的变量,DecorView代表了当前Window最顶级的View,可以看做是根View。由上代码看出,后面会执行DecorView的superDispatchTouchEvent方法,其源码如下所示:

  //DecorView#superDispatchTouchEvent()
 public boolean superDispatchTouchEvent(MotionEvent event) {
    //DecorView继承FrameLayout 那么他本是就是一个ViewGroup
    //那么这个方法最后就会调用到ViewGroup#dispatchTouchEvent()
    return super.dispatchTouchEvent(event);
}

可以看到最后Activity的分发过程最后就是将事件交给顶级DecorView(即ViewGroup)去进行事件分发。然后它又会调用ViewGroup#dispatchTouchEvent()。到这里我们就将我们的事件由Activity->ViewGroup的传递。并将返回值设置成true。表示这个事件已经被我们消耗掉了。从注释2中我们可以看出只有Window没处理触摸事件的情况下,Activity才会调用onTouchEvent方法去处理事件:

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

只有当触摸事件没有被任何的View或ViewGroup处理过的时候,Activity才会执行自己的onTouchEvent去处理触摸事件。一种典型的情形就是,当前触摸点在Window范围之外,这样Window里面所有的View都不会接收更不会处理该触摸事件,这时候我们可以重写该方法实现一些自己的逻辑处理这种情形。如果我们处理了,就返回true,否则返回false。其默认一直返回false。

3.2 ViewGroup对点击事件的发分过程

通过上面的分析,当Activity接收到触摸事件之后,会通过DectorView调用ViewGroup的dispatchTouchEvent方法。那么我们就分析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();
}

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

从上面我们可以看出,事件在开始的时候会调用resetTouchState方法清空mFirstTouchTarget 。 ViewGroup在两种情况下会判断是否要拦截当前事件:事件类型为ACTION_DOWN或者mFirstTouchTarget != null。ACTION_DOWN是按下,mFirstTouchTarget 用于保存当前ViewGroup中处理了触摸事件的子View。

下面看下ViewGroup不拦截事件,将事件向下分发,传递给子View进行处理

//对子元素进行遍历
for(int i = childrenCount - 1;i >=0;i--){
    //子view是否在做动画
    final int childIndex = getAndVerifyPreorderedIndex(
            childrenCount, i, customOrder);
    //事件坐标是否在子元素的区域内
    final View child = getAndVerifyPreorderedView(
            preorderedList, children, childIndex);

    // If there is a view that has accessibility focus we want it
    // to get the event first and if not handled we will perform a
    // normal dispatch. We may do a double iteration but this is
    // safer given the timeframe.
    //判断是否接受点击事件
    if (childWithAccessibilityFocus != null) {
        if (childWithAccessibilityFocus != child) {
            //不符合要求
            continue;
        }
        childWithAccessibilityFocus = null;
        i = childrenCount - 1;
    }

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

    resetCancelNextUpFlag(child);
    //如果有子元素,将进行事件分发
    //注释1
    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);
}

ViewGroup遍历其子View,通过子元素是否在播放动画和点击事件的坐标是否在子元素的区域内,来判断子元素是否能够接收到点击事件。对于符合点击事件的子View并调用dispatchTransformedTouchEvent()进行事件分发。当子元素处理点点击事件就会调用addTouchTarget方法对mFirstTouchTarget进行赋值,alreadyDispatchedToNewTouchTarget 设置为true,表示只要有子View处理了触摸事件,就表示当前的ViewGroup也处理了触摸事件,并且这种情况下ViewGroup不会调用从View中继承来的dispatchTouchEvent方法,从而不会触发ViewGroup的onTouchEvent方法的执行。
下面看下如何对mFirstTouchTarget进行赋值:

private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
    final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
    target.next = mFirstTouchTarget;
    mFirstTouchTarget = target;
    return target;
}

我们可以看到mFirstTouchTarget的赋值是在addTouchTarget方法中进行的,mFirstTouchTarget是否被赋值,将影响到ViewGroup对事件的拦截。

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

如果mFirstTouchTarget ==null 即表示ViewGroup没有子元素,或者是子元素处理了点击事件,下一步将会把点击事件交给View来处理。
我们从上面的注释1和注释2处可以看到,都调用了dispatchTransformedTouchEvent方法,而第三个参数中 注释2传递的是一个null。下面看下源码:

    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) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }
      ......
   }

可以可看到,如果ViewGroup有子元素同时子元素可以处理点击事件。那么就会调用子元素的child.dispatchTouchEvent方法。如果是child是ViewGroup继续上面的循环,如果子元素是View,那么就会调用View.dispatchTouchEvent方法。关于这个方法我们后面分析。如果child为空,就会调用super.dispatchTouchEvent方法,那么就会调用ViewGroup的父类,即View.dispatchTouchEvent方法,ViewGroup自己处理点击事件。最后都会默认调用onTounchEvent方法。

3.3 View对点击事件的处理过程

view对点击事件处理过程稍微简单些。这里的view是不包含ViewGroup。下面看下dispatchTouchEvent方法:

    public boolean dispatchTouchEvent(MotionEvent event) {  
        ......

        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            //如果设置了OnTouchListener,那么会在此处执行OnTouchListener的onTouch方法
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
            //触摸事件没有被OnTouchListener处理,那么就会执行View的onTouchEvent方法
            if (!result && onTouchEvent(event)) {
                //如果onTouchEvent返回了true,就表示触摸事件被View处理了,result就被设置为了true
                result = true;
            }
        }
        ......
        return result;
    }

从上面可以看出,首先会判断有没有设置onTouchListener,如果onTouchListener中的onTouch方法返回true,那么onTouchEvent方法就不会被执行,由此可见onTouchListener的优先级是高于onTouchEvent,这样做的好处是,方便在外界处理点击事件。
下面看下onTouchEvent方法,当view 是不可用的时候点击事件的处理过程,在不可用的状态下,View照样会消耗事件,只不过不会触发onClick方法。

       final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
        //首先判断当前View是不是DISABLED不可用状态
        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;
        }
        //如果View有代理会执行这个方法
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }

当然如果设置有代理,还会执行TouchDelegate的onTouchEvent方法,此处onTouchEvent的工作机制和onTouchListener的类似。
onTouchEvent对点击事件的具体处理如下:

        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    if ((viewFlags & TOOLTIP) == TOOLTIP) {
                        handleTooltipUp();
                    }
                    if (!clickable) {
                        removeTapCallback();
                        removeLongPressCallback();
                        mInContextButtonPress = false;
                        mHasPerformedLongPress = false;
                        mIgnoreNextUpEvent = false;
                        break;
                    }
                    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:
                    ......
                    break;
                case MotionEvent.ACTION_CANCEL:
                   ......
                    break;
                case MotionEvent.ACTION_MOVE:
                   ......
                    break;
            }
            return true;
        }

如果view是可以点击的,就会进入switch语句中,当ACTION_UP事件触发,就会执行performClick方法。

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

如果我们设置了点击事件就出执行OnClickListener的onClick方法。点击信息封装在ListenerInfo中,当我们为view设置点击事件的时候就会调用,可以看下setOnClickListener方法和setOnLongClickListener方法的源码:

    public void setOnClickListener(@Nullable OnClickListener l) {
        if (!isClickable()) {
            setClickable(true);
        }
        getListenerInfo().mOnClickListener = l;
    }

    public void setOnLongClickListener(@Nullable OnLongClickListener l) {
        if (!isLongClickable()) {
            setLongClickable(true);
        }
        getListenerInfo().mOnLongClickListener = l;
    }

当我们设置点击事件的时候,会通过setClickable和setLongClickable方法将view设置成可点击的。

本文学习到此,如果文章哪里有错误,欢迎指出,谢谢!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,670评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,928评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,926评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,238评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,112评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,138评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,545评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,232评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,496评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,596评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,369评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,226评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,600评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,906评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,185评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,516评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,721评论 2 335

推荐阅读更多精彩内容