Android 源码详解:View的事件分发机制

Posted 鸽一门

tags:

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

(在此声明:以下内容为阅读任玉刚老师的《android开发艺术探索》中结合自身理解融合而成)

问题:
1. 点击事件传递过程中,若所有View不消费此事件,结果?1
2. Window是如何传递事件给ViewGroup?2
3. 子元素是否可以干预父元素的分发过程?How?3
4. onInterceptTouchEvent 方法是否在每次判断点击事件都会被调用?3
5. ViewGroup是如何将点击事件传递给子View处理?
6. 若给某个View设置了OnTouchListeneronTouchEventOnClickListener,三者之间的优先级如何?

关于View的事件分发机制,之前写过一篇介绍主要是理论部分:http://blog.csdn.net/itermeng/article/details/52242224

回顾上面的问题,如果你有疑惑的地方,也许应该从源码的角度去解决,单纯的理论部分无法很好的解决疑惑,分析源码更加受益匪浅:




1. Activity 对点击事件的分发过程

1.1 逻辑简介

当一个点击操作(MotionEvent)发生时,根据事件传递机制,最先接收的是 Activity,由Activity 的 dispatchTouchEvent 来进行事件派发,实质是Activity内部的 Window 完成的。Window会将事件传递给 DecorView(指当前界面的底层容器,即setContentView设置的View的父容器),通过
Activity.getWindow.getDecorView() 可以获得。



1.2 Activity的dispatchTouchEvent

因此,首先从ActivitydispatchTouchEvent 方法入手:

//Activity.java

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

以上代码可证实一开始说的:首先事件交给Activity 附属的 Window 进行分发:
1). 如果返回true,整个事件循环就结束了。
2). 返回false,意味着此点击事件一直传递下去都没有View消费掉(即所有ViewonTouchEvent 都返回了 false), 那么ActivityonTouchEvent 就会被调用。

————————————————–【第1个问题解决】———————————————






2. Window对点击事件的分发过程

第一个问题解决后,此点击事件已经传递到Window中,继续往下深究,解决第二个问题:Window是如何传递事件给ViewGroup?

public abstract boolean superDispatchTouchEvent(MotionEvent event);

通过源码可知Window是个抽象类,而它的 superDispatchTouchEvent 也是个抽象方法, 所以必须要找到Window的实现类:



2.1 Window的实现类 :PhoneWindow

其实是 PhoneWindow ,从Window的源码中可以看出,在Window的说明中:

Window类可以控制顶级View的外观和行为策略,它的唯一实现位于 android.policy.PhoneWindow 中,当你要实例化这个 
Window类的时候,你并不知道它的细节,因为这个类会被重构,
只有一个工厂方法可以使用。尽管这看起来有些模糊,不过可以看一下 android.policy.PhoneWindow这个类。

所以,接下来查看 PhoneWindow 是如何处理点击事件的:

PhoneWindow.java
public boolean superDispatchEvent(MotionEvent event)
    return mDecor.superDispatchTouchEvent(event);

由此可得知,PhoneWindow直接将事件传递给了DecorView,来查看DecorView



2.2 DecorView

//This is the top-level view of the wondow,containing the window decor
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker
    private DecorView mDecor;

    @Override
    public final View getDecorView()
        if(mDecor == null)
            installDecor();
        
        return mDecor;
    

看完以上代码,首先了解mDecor 是什么?

平常学习中,通过
((ViewGroup))getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0);
这种方式可以获取Activity所设置的View,而这个mDecor 就是:
getWindow().getDecorView()
返回的View,所以通过 setContentView 方法设置的View是它的一个子View

所以目前点击事件传递到了 DecorView,由于它继承于FrameLayout而且是个父View,所以最终事件会传递给View代表此时点击事件已经传递给顶级View了!(这里对于顶级View的两种解释:在Activity中通过setContentView所设置的View 或者 叫做根View

————————————————–【第2个问题解决】———————————————






3. 顶级View对点击事件的分发过程

3.1 逻辑简介

目前为止,第二个问题也解决掉,点击事件已经传递到 根View ,关于接下来的事件分发机制方法的优先级调用,之前一篇博客分析过:
http://blog.csdn.net/itermeng/article/details/52242224

优先级排序:OnTouchListener >onTouchEvent >OnClickListener

这里简单回顾:点击事件达到顶级View(一般是个ViewGroup)后,会调用ViewGroup的dispatchTouchEvent 方法,此方法结果首先取决于onInterceptTouchEvent

1). 若方法返回true,事件由ViewGroup处理,然后按照方法的优先级,若ViewGroup设置有mOnTouchListener ,则onTouch会被调用,否则调用onTouchEvent.(若都有设置,因为优先级的原因,onTouchEvent事件会被屏蔽掉!)最后如果在onTouchEvent中设置有mOnClickListeneronClick才会被调用。

2). 若方法返回false,代表顶级ViewGroup不拦截此事件,事件会依次传递给子View,这时子ViewdispatchTouchEvent会被调用,根据返回结果如此循环,直至点击事件被某个View消费掉!



3.2 ViewGroup 的 dispatchTouchEvent 方法

以上就是简述事件分发机制的逻辑理论,现在从源码的角度来证明以上观点!首先查看一开始ViewGroupdispatchTouchEvent 方法:

//check for interception

final boolean interception;
if(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null)
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    if(!disallowIntercept)
        //ViewGroup是否拦截此点击事件由方法返回值决定
        intercepted = onInterceptTouchEvent(ev);
        ev.setAction(action); //restore action in case it was changed
     else 
        //ViewGroup不拦截此点击事件
        intercepted = false;    
    
 else 
    //ViewGroup拦截此点击事件
    //There are no touch targets and this action is not an initial down
    //so this view group continues to intercept touches.
    intercepted = true;

以上代码可以看出,ViewGroup在如下两种情况会判断是否要拦截当前点击事件:
1). 点击事件类型为ACTION_DOWN
2). mFirstTouchTarget != null

来解释一下第二种情况:从后面的代码逻辑可得,当点击事件成功被ViewGroup的子元素处理后,会被赋值指向子元素(即mFirstTouchTarget != null 代表ViewGroup不拦截此点击事件,反之则反)。

一旦ViewGroup拦截此点击事件,mFirstTouchTarget != null条件不成立。那么当ACTION_MOVEACTION_UP事件到来,最外层的if判断为false即不会调用ViewGroup的 onInterceptTouchEvent方法,并且同一序列的其它时间都默认交给它处理!

也就是说 某个View一旦开始处理事件,若它不消耗ACTION_DOWN 事件(即onTouchEvent返回了 false),那么同一事件序列的其它事件都不会交给它来处理,并交由父元素处理!



3.3 requestDisallowInterceptTouchEvent

不过,有一种特殊情况:FLAG_DISALLOW_INTERCEPT 标记位,它是通过 requestDisallowInterceptTouchEvent 方法来设置,一般用于子View中。此标记一旦设置后,ViewGroup将无法拦截除了ACTION_DOWN以外的点击事件。为何是除了ACTION_DOWN以外?

因为ViewGroup在分发点击事件时,若是ACTION_DOWN 就会重置标记位,将导致之前设置的标记位无效。因此,当面对ACTION_DOWN事件时,ViewGroup总是会调用自己的onInterceptTouchEvent方法来询问自己是否要拦截事件。

接下来从源码角度证明以上观点:

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

    cancelAndClearTouchTargets(ev);
    resetTouchState();

以上代码证明了观点:ViewGroupACTION_DOWN 事件时重置状态的操作,在resetTouchState 方法中重置标记位,因此子View调用requestDisallowInterceptTouchEvent 方法并不能影响对 ACTION_DOWN 事件的处理。

以上可证明书中结论中的两个观点:

(3)某个View一旦决定拦截,那么这一个事件序列都只能由它来处理
(如果时间序列可以传到它),并且它的 onInterceptTouchEvent
不会再被调用。
(11)事件传递过程是由外向内的,即事件总是先传递给父元素,
再由父元素分发给子View,通过requestDisallowInterceptTouchEvent  
方法可以在子元素中干预父元素的分发过程,
但是ACTION_DOWN 事件除外。

最后总结:
onInterceptTouchEvent不是每次事件都会被调用,如果想提前处理所有的点击事件,要选择dispatchTouchEvent 方法,只有这个方法可以保证每次都会被调用,当然前提是事件能够传递到当前的ViewGroup

————————————————–【第3,4个问题解决】———————————————



3.4 ViewGroup不拦截事件,分发机制

final View[] children = mChildren;
for(int i = childrenCount -1; i>=0; i--)
    //......
    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;
    

以上代码主要逻辑:首先遍历ViewGroup的所有子元素,然后判断是否能够接收到点击事件,这取决于两点:
1). 子元素是否在播动画
2). 点击事件的坐标是否落在子元素的区域内。
若满足以上两点,则点击事件会传递给这个子View来处理。

回到第五个问题:它是如何将点击事件传递给子View处理?

关键是最外层 if 判断中的 dispatchTransformedTouchEvent方法,查看其方法中重要部分:

//dispatchTransformedTouchEvent 方法

    if(child == null)
        handle = super.dispatchTouchEvent(Event);
    else
        handle = child.dispatchTouchEvent(Event);
    

其实dispatchTransformedTouchEvent 实质就是调用子元素的 dispatchTouchEvent方法,所以以上代码中如果child传递的不是null,便会直接调用dispatchTouchEvent,这样点击事件便交由子元素处理,完成一轮事件分发

————————————————–【第5个问题解决】———————————————



3.5 mFirstTouchTarget

回到以上步骤,如果子元素的dispatchTouchEvent 返回true,这时先不考虑点击事件在这个子View内部的分发,回到ViewGroupdispatchTouchEvent 方法中,此时 mFirstTouchTarget 会被赋值同时跳出for 循环 :

newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;

以上代码完成了 mFirstTouchTarget 的赋值并终止对子元素的遍历。在遍历的过程中,如果有子元素的dispatchTouchEvent 返回trueViewGroup就会把此点击事件传递给子View,此时也将停止循环。对接以上逻辑。

但其实mFirstTouchTarget 真正的赋值过程是在 addTouchTarget 内部完成的,来查看此方法:

private TouchTarget addTouchTarget(View child , int pointerIdBits)
    TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
    target.next = mFirstTouchTarget;
    mFirstTouchTarget = target;
    return target;

从此方法可看出,mFirstTouchTarget 其实是一种单链表结构。mFirstTouchTarget 是否被赋值,直接影响到ViewGroup 对事件的拦截策略。比如在3.2 环节介绍的,若mFirstTouchTargetnull,那么ViewGroup就默认拦截同一序列的点击事件!

如果遍历所有的子元素后,传递的点击事件并没有被合适地处理掉,原因有两种:
1). ViewGroup中没有子元素。
2). 子元素处理了点击事件,可是在与ViewGroup联系的 dispatchTouchEvent方法中返回了false(一般是因为子元素在onTouchEvent中返回了false)。

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

由上可得,在以上两种情况下,ViewGroup会自己处理点击事件,这也证明了书中第4条结论:

4)某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件
(onTouchEvent返回了false),那么同一事件序列的其它事件不会交给它处理,重新将事件交由父元素处理,即父元素的onTouchEvent被调用。

以上,ViewGroup的事件传递机制以分析完,现下结束点击事件传递到View上,它的传递机制:






4. View 对点击事件的分发过程

注意这里的View不包含 ViewGroupView对于点击事件的处理相较于ViewGroup容易些,因为它是一个单独的元素,没有子元素无法向下传递事件,所以它只能自己处理事件。

4.1 View 的dispatchTouchEvent

同第三点分析ViewGroup一样,首先分析ViewdispatchTouchEvent 方法:

public boolean dispatchTouchEvent(MotionEvent event)
    boolean result = false;
    ......
    if(onFilterTouchEventForSecurity(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;
        
    
    ...
    return result;

由以上代码可得View对点击事件的处理过程:(最外层 if方法体中)
首先 内层中第一个if 中判断有没有设置 onTouchListener

如果onTouchListener中的 onTouch方法返回true那么result设置为true,则第二个if 中语句不会执行(onTouchEvent不会被执行)。即证实方法体中优先级排序,这样的好处是方便在外界处理点击事件。


4.2 onTouchEvent

接下来查看 onTouchEvent 的实现,先看View处于不可用状态下点击事件的处理过程:

//onTouchEvent 方法体中

if((viewFlags & ENABLED_MASK) == DISABLED)
    if(event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0)
        setPressed(false);
    
    // A disabled view that is clickable still consume the touch
    //events,it just doesn't respond to them.

    return (((viewFlags & CLICKABLE) == CLICKABLE || (((viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));

如上所示,不可用状态下的View照样会消耗点击事件,尽管它看起来不可用。

接下来查看onTouchEvent 对点击时间的具体处理:

//onTouchEvent 方法体中

if((viewFlags & CLICKABLE) == CLICKABLE ||
    (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE))
        switch(event.getAction())
            boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
            if((mPrivateFlags & PFLAG_PREPRESSED)!= 0 || prepressed )
            ...
            if(!mHasPerformedLongPress)
                removeLongPressCallback();
                if(!focusTaken)
                    if(mPerformClick == null)
                        mPerformClick = new PerformClick();
                    
                    if(!post(mPerformClick ))
                        performClick();
                    
                
            
            ...
        
        break;
    
    ...

由上可知,只要ViewCLICKABLELONG_CLICKABLE属性有一个为true,那么它会消费此事件,即 onTouchEvent方法返回true,不管它是否是DISABLE状态。证实了书中结论的8、9、10点:、

8View的 onTouchevent 默认都会消耗事件(返回true),
除非它是不可点击的(clickable 和 longClickable同时为false)。
View的longClickable属性默认都为false,clickable属性要分情况:
比如Button的为trueTextView的为false.
9)View的enable属性不影响 onTouchEvent的默认返回值。
哪怕一个View是disable状态,只要它的clickable 或者 longClickable
有一个为true,那么它的onTouchEvent方法返回值为true
(10)onClick会发生的前提是当前View是可点击的,
并且它收到了down、up事件

证实了以上几点结论后,回到上面代码中,当ACTION_UP事件发生时,会触发performClick方法,查看具体实现:

//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);
    return result;

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

所以这也更加确定在讲解方法中优先级时:OnClickListener的优先级最低,在onTouch(如果View没有设置OnTouchListener)方法中才会考虑到OnClickListener

以上为止,已经从源码的角度证明完分发机制中调用那3个方法的优先级了。

————————————————–【第6个问题解决】———————————————



4.3 setClickable 和 setLongClickable

接下来补充一个小细节,之前说过View 的long_clickable的属性值默认为false,而clickable属性值根据不同的View而定。其实通过setClickablesetLongClickable可以分别改变两者属性。

注意:在给view设置 setOnClickListener 会自动将view 的 CLICKABLE 设为true,setOnLongClickListener 亦然,查看其源码:

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


public void setOnLongClickListener(OnLongClickListener l)
    if(!isLongClickable())
        setLongClickable(true);
    
    getListenerInfo().mOnLongClickListener = 1;





以上部分阅读完,再次回顾一开始提出的问题,相信应该已经解决了,而View的事件分发机制应该也掌握好了,推荐任玉刚老师的《Android开发艺术探索》。

希望对你有帮助 :)

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

Android Touch事件分发处理机制详解

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

详解Android View 中的事件分发机制

详解Android View 中的事件分发机制

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

源码阅读分析 - View的Touch事件分发