Android系列View的事件分发机制

Posted jzyhywxz

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android系列View的事件分发机制相关的知识,希望对你有一定的参考价值。

本文将介绍View的一个核心知识点:事件分发机制,了解并掌握事件分发机制是作为一个android程序员必不可少的技能。

事件和事件序列

当我们使用手机时,可以说无时无刻不在和事件打交道:当我们在手机屏幕上点击、滑动或者长按时,都会产生一系列的事件,而手机正是通过这些事件和我们进行交互的。

一个完整的事件序列从手指接触屏幕开始,到手指离开屏幕结束,这个事件序列以一个DOWN事件开头,中间是一串MOVE事件,并以一个UP事件结尾。例如:当我们点击屏幕时,事件序列为DOWN->UP;当我们在屏幕上滑动时,事件序列为DOWN->MOVE->…->MOVE->UP。

一个事件由一个MotionEvent对象表示,常见的事件类型有ACTION_DOWN、ACTION_MOVE和ACTION_UP,分别表示手指刚接触屏幕、手指在屏幕上移动和手指离开屏幕。

事件分发概述

当一个事件产生时,系统需要把这个事件传递给一个View处理,这个传递过程就是事件分发过程。事件的分发过程由3个很重要的方法共同完成:dispatchTouchEventonInterceptTouchEventonTouchEvent,其中:

  • dispatchTouchEvent方法用来分发事件,如果事件能够到达当前View,则此方法一定会被调用,返回true表示消耗了当前事件;
  • onInterceptTouchEvent方法用来判断是否拦截事件,如果当前View拦截了某个事件,则在同一事件序列中不会再调用此方法,返回true表示拦截了当前事件;
  • onTouchEvent方法用来处理事件,返回true表示消耗了当前事件,如果不消耗,则当前View不会再收到同一事件序列中的后续事件。

它们的关系可以用下面的伪代码表示:

boolean dispatchTouchEvent(MotionEvent ev) 
    boolean consume = false;
    if (onInterceptTouchEvent(ev)) 
        consume = onTouchEvent(ev);
     else 
        consume = child.dispatchTouchEvent(ev);
    
    return consume;

通过上面的伪代码,我们可以大致了解事件传递的规则:当一个事件到达父View时,此时会调用父View的dispatchTouchEvent方法,在此方法中,首先判断父View是否要拦截此事件,如果拦截就调用父View的onTouchEvent方法,否则把此事件交给其子View,并调用子View的dispatchTouchEvent方法。

事实上,当一个事件产生时,它是按照这个顺序传递的:Activity->Window->View,即事件先传递给当前的Activity,Activity再传递给Window,Window再传递给顶级View,顶级View收到事件后,再按照事件分发机制把事件传递给子View。

下面我们结合源码一起来跟踪事件的分发过程。

Activity对事件的分发过程

当一个事件发生时,会最先传递给当前的Activity,并由Activity的dispatchTouchEvent方法进行事件分发。

dispatchTouchEvent方法中,事件分发的工作是由Window对象完成的。Window对象会将事件传递给decor view(一般就是调用setContentView方法所设置的View的父容器),decor view再进一步执行事件分发。

现在来具体看下dispatchTouchEvent方法的源码:

/* Activity.dispatchTouchEvent */
boolean dispatchTouchEvent(MotionEvent ev) 
    if (ev.getAction() == MotionEvent.ACTION_DOWN) 
        onUserInteraction();
    
    if (getWindow().superDispatchTouchEvent(ev)) 
        return true;
    
    return onTouchEvent(ev);

通过源码可以知道:
1. 首先,如果事件是一个DOWN事件的话,则调用当前Activity的onUserInteraction方法。查看源码会发现,默认的onUserInteraction方法其实是一个空方法,这里可以忽略;
2. 接着,通过getWindow方法取得Window对象,并调用其superDispatchTouchEvent方法进行事件分发。这里有一个问题,就是Window类中的superDispatchTouchEvent方法是一个抽象方法,换句话说,我们需要找到相应的Window子类,实际上,getWindow方法返回的是一个PhoneWindow对象;
3. 最后,如果getWindow().superDispatchTouchEvent(ev)返回false的话,即此事件没有被消耗,则执行当前Activity的onTouchEvent方法。也就是说,如果没有找到一个View能够消耗此事件,则最终会重新交给当前的Activity处理,并且Activity默认的onTouchEvent方法总是返回false。

经过分析后发现,事件分发实际上是在第2步中进行的。那我们接着看下PhoneWindow的superDispatchTouchEvent方法:

/* PhoneWindow.superDispatchTouchEvent */
boolean superDispatchTouchEvent(MotionEvent event) 
    return mDecor.superDispatchTouchEvent(event);

可以看到,这里的superDispatchTouchEvent方法很简单,就是把事件交给decor view继续进行事件分发,那么这个decor view到底是什么呢?

其实,decor view是一个DecorView对象,它一般就是当前界面(Activity)的底层容器,换句话说,我们在Activity中调用setContentView方法所设置的View是DecorView的一个子View。由于DecorView继承自FrameLayout且是一个ViewGroup,因此事件最终会传递到View。从这里开始,事件已经传递到了顶级View(即在Activity中通过setContentView所设置的View),这一点也可以从源码看出:

/* DecorView.superDispatchTouchEvent */
boolean superDispatchTouchEvent(MotionEvent event) 
    return super.dispatchTouchEvent(event);

这里DecorView的superDispatchTouchEvent方法调用了父类的dispatchTouchEvent方法,由于DecorView是一个ViewGroup,因此事件传递给ViewGroup继续分发。

顶级View对事件的分发过程

上面分析了事件是如何从Activity经过Window又传递到顶级View的,现在我们继续跟踪事件是如何在顶级View中进行分发的。

由于源码过长,这里我们先说一下事件在顶级View中的分发过程。简单来说,如果顶级View拦截事件,即onInterceptTouchEvent方法返回true,则事件交由顶级View处理,否则事件会传给顶级View所在的点击事件链上的子View,这时子View的dispatchTouchEvent方法会被调用。这样就完成了一个层次的事件分发,接下来的传递过程也一样,如此循环就完成了整个View层次的事件分发。

GroupView是如何判断是否要拦截事件的?

下面我们按照上述的事件分发过程分析下源码,首先是GroupView是如何判断是否要拦截事件的:

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

这里我们先要意识到一点,只要intercepted被赋值为true,就意味着ViewGroup会拦截事件。那么什么情况下intercepted会被赋值为true呢?一种情况是如果事件不是DOWN事件并且没有子View能够消耗此事件,则ViewGroup一定会拦截事件;另一种情况是交由ViewGroup的onInterceptTouchEvent方法来判断,如果onInterceptTouchEvent返回true,则GroupView会拦截事件。

在这里顺便提一下mFirstTouchTarget,详细的解释到后面讲ViewGroup向子View分发事件时再说,这里只要知道mFirstTouchTarget != null表示有子View能够消耗事件,换句话说,如果ViewGroup拦截了事件,则mFirstTouchTarget最终会为null。

另外,注意到这段代码中还有一个变量disallowIntercept,它表示FLAG_DISALLOW_INTERCEPT标志位是否为1,如果此标志位为1,则ViewGroup是不能拦截事件的。在子View中,可以通过requestDisallowInterceptTouchEvent方法设置此标志位为1让父View(ViewGroup)无法拦截事件。

GroupView是如何向子View分发事件的?

从上面的分析可知,当intercepted为false时,GroupView会向子View分发事件。对应的源码如下:

/* ViewGroup.dispatchTouchEvent */
// Find a child that can receive the event.
// Scan children from front to back.
final int childrenCount = mChildrenCount;
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);

    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);
    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) 
        // Child wants to receive touch within its bounds.
        newTouchTarget = addTouchTarget(child, idBitsToAssign);
        alreadyDispatchedToNewTouchTarget = true;
        break;
    

上述代码是简化后的,方便我们看清事件分发的过程。可以看到,整个流程就是在一个for循环中寻找一个能够处理事件的子View,如果找到这样一个子View,并且这个子View不在mFirstTouchTarget链表中,就把它加到mFirstTouchTarget链表头。

为了让大家进一步理解,这里讲3个点:
1) 如何判断一个子View是否能够消耗事件?
2) newTouchTargetmFirstTouchTarget到底是什么?
3) 如果找到一个能够消耗事件的子View,接下来做了些什么?没找到又做了些什么?

========== 问题1 ==========
通过dispatchTransformedTouchEvent方法可以判断一个子View是否能够消耗事件,返回true表示可以消耗,反之不能。下面是dispatchTransformedTouchEvent方法简化后的源码:

/* ViewGroup.dispatchTransformedTouchEvent */
boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) 
    if (child == null) 
        handled = super.dispatchTouchEvent(event);
     else 
        handled = child.dispatchTouchEvent(event);
    
    return handled;

这里由于把子View作为child参数传递给dispatchTransformedTouchEvent方法,因此会调用子View的dispatchTouchEvent方法,可以看到,这里开始进行下一层次的事件分发。

========== 问题2 ==========
newTouchTargetmFirstTouchTarget都是TouchTarget类型的对象,TouchTarget就好像一个链表,可以把它想象成LinkedList,其中的ViewWrapper封装了子View和其它一些信息。TouchTarget.next指向下一个TouchTarget对象,TouchTarget.child指向子View。

当ViewGroup进行事件分发时,如果找到一个子View能够消耗事件,则把newTouchTarget“指向”它;进一步的,如果此子View不在mFirstTouchTarget链表中,则把它加到mFirstTouchTarget链表头。换句话说,newTouchTarget表示每次事件分发找到的那个能够处理事件的子View,而mFirstTouchTarget表示历史上所有能够处理事件的子View链表。

========== 问题3 ==========
显而易见,当找到一个子View来处理事件时,会记录当前的某些状态(在上述代码中删减掉了)并立即跳出循环,并且newTouchTarget会指向此子View,mFirstTouchTarget不为null。

然后看下没找到子View的情况:

/* ViewGroup.dispatchTransformedTouchEvent */
// 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);

可以看到,如果没找到能够消耗事件的子View,则把ViewGroup当作普通的View来处理事件,这也可以从dispatchTransformedTouchEvent方法里看出:由于这里的child参数为null,因此会调用父类(View)的dispatchTouchEvent方法。

========== 总结 ==========
从问题1和问题3可以看出,事件最后都会交由View的dispatchTouchEvent方法处理,因此我们也跟踪到了事件分发的最后一层:View对事件的处理。

View对事件的处理过程

接着上面的内容,现在终于到了真正处理事件的地方,我们先来看看View的dispatchTouchEvent方法:

/* View.dispatchTouchEvent */
boolean dispatchTouchEvent(MotionEvent event) 
    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对事件的处理就比较简单了,因为这里没有向下传递的过程,所以只需要自己处理事件。从上面的源码可以看出,首先会判断有没有设置OnTouchListener,如果设置了并且其onTouch方法返回true,则不会调用自身的onTouchEvent方法,这也说明了监听器的优先级比回调方法的优先级高。

接着再看看默认的onTouchEvent方法:

/* View.onTouchEvent */
boolean onTouchEvent(MotionEvent event) 
    if ((viewFlags & ENABLED_MASK) == DISABLED) 
        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:
                if (mPerformClick == null) 
                    mPerformClick = new PerformClick();
                
                if (!post(mPerformClick)) 
                    performClick();
                
                break;
        
        return true;
    

首先,注意到就算是disable状态的View一样会消耗事件;其次,如果View设置了TouchDelegate,还会执行TouchDelegate的onTouchEvent方法;最后,在UP事件发生时,会调用performClick方法。

performClick方法中,可以看到:

/* View.performClick */
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);
    return result;

如果View设置了OnClickListener,那么performClick方法内部会调用它的onClick方法。

事件分发总结

经过上述的讲解,我们已经成功的跟踪了一个事件是如何从Activity传递到每一层View的,现在再简单总结一下:

                              Activity.dispatchTouchEvent
                                           ↓
                          PhoneWindow.superDispatchTouchEvent
                                           ↓
                           DecorView.superDispatchTouchEvent
                                           ↓
                              ViewGroup.dispatchTouchEvent
                      (intercepted==false) | (intercepted==true)
                       ----------------------------------------
                       ↓                                      ↓
    ViewGroup.dispatchTransformedTouchEvent    ViewGroup.onInterceptTouchEvent
                       ↓
            View.dispatchTouchEvent
                       | (优先)
           -------------------------
           ↓                       ↓
   View.onTouchEvent    OnTouchListener.onTouch
           ↓
   View.performClick
           ↓
OnClickListener.onClick
  1. 事件首先传递给当前的Activity,Activity会委托Window进行事件分发;
  2. 接着Window又把事件传递给DecorView,DecorView会把事件传递给通过setContentView方法设置的View,此时事件进入顶级View;
  3. 通常顶级View是一个ViewGroup,此时会调用其dispatchTouchEvent方法进行事件分发。如果ViewGroup拦截事件,则事件不会传递给子View,ViewGroup自己消耗事件;如果ViewGroup不拦截事件,则在ViewGroup的所有子View中找到一个能够消耗事件的子View,并把事件传递给它;
  4. 当事件到达View(不是ViewGroup)时,此时事件不会再向下传递,View只要负责处理事件即可;
  5. 如果一个ViewGroup的所有子View都不能消耗事件,则此事件会重新交给ViewGroup处理,如果此ViewGroup也不能消耗事件,则继续把事件传递给它的父View处理,依次类推,如果所有View层次都不能消耗事件,则事件最终会回到当前的Activity,并交由Activity处理。

以下是一些关于事件分发的结论,方便大家理解整个事件传递机制:
1. 同一个事件序列是指从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在此过程中会产生一系列事件,这个事件序列以DOWN事件开头,中间是一串MOVE事件,并且以UP事件结尾;
2. 当某个View一旦决定拦截事件时,则这个事件序列都只能交给它处理,并且它的onInterceptTouchEvent方法只会调用一次。换句话说,当一个View开始拦截事件后,那么系统会把同一个事件序列的后续事件都交给它处理,因此不必再调用onInterceptTouchEvent方法了;
3. 某个View一旦开始处理事件,如果它不消耗DOWN事件(其onTouchEvent方法返回了false),则同一事件序列中的后续事件都不会再交给它处理,并且事件将重新交给它的父View去处理(即调用父View的onTouchEvent方法)。这就好比上级交给下级做一件事,如果这件事没做好,那么短期内上级不会再把事件交给这个下级做了;
4. 如果View不消耗除DOWN事件以外的其他事件,那么这个事件序列会消失,此时不会调用父View的onTouchEvent方法;虽然当前View可以继续接受同一事件序列的后续事件,但最终这些事件会交给Activity处理;
5. ViewGroup默认不拦截任何事件,Android源码中的ViewGroup的onInterceptTouchEvent方法默认返回false;
6. View(不是GroupView)没有onInterceptTouchEvent方法,一旦有事件传递给它,就会调用它的onTouchEvent方法;
7. View的onTouchEvent方法默认都会消耗事件,哪怕它是disable的,只要它有一个clickable为true,onTouchEvent方法就返回true;
8. 事件传递是由外向内的,即事件总是先传递给父View,再传递给子View,通过requestDisallowInterceptTouchEvent方法可以在子View中干预父View的事件拦截,但DOWN事件除外。

关注微信公众号,有更多精彩文章哦(o゚▽゚)o

以上是关于Android系列View的事件分发机制的主要内容,如果未能解决你的问题,请参考以下文章

Android6.0 ViewGroup/View 事件分发机制详解

Android事件分发机制理解

Android View 事件分发机制

android-----事件分发机制测试系列

Android 事件分发事件分发源码分析 ( ViewGroup 事件传递机制 四 | View 事件传递机制 )

Android开发-分析ViewGroupView的事件分发机制结合职责链模式