面试必备:Android事件分发机制

文章中出现的源码均基于8.0java

前言

事件分发机制不只仅是核心知识点更是难点,而且仍是View的一大难题滑动冲突解决方法的理论基础,所以掌握好View的事件分发机制是十分重要的。android

1、基本认识

1. 事件分发的对象

事件分发的对象是点击事件(Touch事件),而当用户触摸屏幕时,将产生点击事件。ios

事件类型分为四种,以下所示:markdown

类型 说明
MotionEvent.ACTION_DOWN 手指刚接触屏幕,通常为事件的开始
MotionEvent.ACTION_MOVE 手指在屏幕移动,在移动的过程当中会产生多个move事件
MotionEvent.ACTION_UP 手指从屏幕上松开的一瞬间
MotionEvent.ACTION_CANCEL 结束事件,非人为缘由

同一个事件序列:指从手指刚接触屏幕,到手指离开屏幕的那一刻结束,在这一过程产生的一系列事件,这个序列通常以down事件开始,中间含有多个move事件,最终以up事件结束app

2. 事件分发的本质

事件分发的本质,其实就是将点击事件(MotionEvent)传递到某个具体的View处理的整个过程ide

3. 事件分发的顺序

事件传递的顺序:Activity->Window->DecorView->ViewGroup->View。一个点击事件发生后,老是先传递给当前的Activity,而后经过Window传给DecorView再传给ViewGroup,最终传到View。oop

在《开发艺术探索》中的事件分发的顺序是:Activity->Window->View,而有的博客上的顺序是:Activity->ViewGroup->View,不过其实二者是同样的(下列会从源码进行分析)。Window是抽象类,其惟一实现类为PhoneWindow,PhoneWindow将事件直接传递给DecorView,而DecorView继承FrameLayout,FrameLayout又是ViewGroup的子类,因此兜兜转转最后也能够认为Window事件分发的实现实际上是ViewGroup来实现的。因此也能够认为事件传递的顺序是:Activity->ViewGroup->View。源码分析

2、核心方法

View的事件分发机制主要由事件分发->事件拦截->事件处理三步来进行逻辑控制,很巧的这三步恰好对应了三个核心方法布局

1. 事件分发:dispatchTouchEvent

用来进行事件的分发,若是事件可以传递给当前View,则该方法必定会被调用。返回结果受当前View的onTouchEvent和下级的dispatchTouchEvent的影响,表示是否消耗当前事件。动画

原型:public boolean dispatchTouchEvent(MotionEvent ev)

return:

  • ture:当前View消耗全部事件
  • false:中止分发,交由上层控件的onTouchEvent方法进行消费,若是本层控件是Activity,则事件将被系统消费,处理

2. 事件拦截:onInterceptTouchEvent

需注意的是在Activity,ViewGroup,View中只有ViewGroup有这个方法。故一旦有点击事件传递给View,则View的onTouchEvent方法就会被调用

在dispatchTouch Event内部使用,用来判断是否拦截事件。若是当前View拦截了某个事件,那么该事件序列的其它方法也由当前View处理,故该方法不会被再次调用,由于已经无须询问它是否要拦截该事件。

原型:public boolean onInterceptTouchEvent(MotionEvent ev)

return:

  • ture:对事件拦截,交给本层的onTouchEvent进行处理
  • false:不拦截,分发到子View,由子View的dispatchTouchEvent进行处理
  • super.onInterceptTouchEvent(ev):默认不拦截

3. 事件处理:onTouchEvent

在dispatchTouchEvent中调用,用来处理点击事件,返回结果表示是否消耗当前事件,若是不消耗,则在同一事件序列中,当前View没法再接受到剩下的事件,而且事件将从新交给它的父元素处理,即父元素的onTouchEvent会被调用

原型:public boolean onTouchEvent(MotionEvent ev)

return:

  • true:表示onTouchEvent处理后消耗了当前事件
  • false:不响应事件,不断的传递给上层的onTouchEvent方法处理,直到某个View的onTouchEvent返回true,则认为该事件被消费,若是到最顶层View仍是返回false,则该事件不消费,将交由Activity的onTouchEvent处理。
  • super.onTouchEvent(ev):默认消耗当前事件,与返回true一致。

3、事件分发机制

在分析事件分发机制时,应该从事件分发的顺序入手一步一步解剖。从上文咱们知道事件分发顺序为:Activity->Window->DecorView->ViewGroup->View。因为Window与DecorView能够看做是Activity->ViewGroup的过程,故这里将从三部分经过源码来分析事件分发机制:

  • Activity对点击事件的分发机制
  • ViewGroup对点击事件的分发机制
  • View对点击事件的分发机制

1. Activity事件的分发机制

咱们知道,当一个点击事件发生时,事件老是最早传递到当前Activity中,由Activity的dispatchTouchEvent来进行事件分发。而Activity会将事件传递给Window对象来分发,Window对象再传递给DecorView。下面将进行源码分析来验证这个过程:

源码:Activity#dispatchTouchEvent

public boolean dispatchTouchEvent(MotionEvent ev) {
    	//点击事件的开始通常为按下事件,因此老是true
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
			
        }
		//若是Activity所属Window的dispatchTouchEvent返回了ture
		//则Activity.dispatchTouchEvent返回ture,点击事件中止往下传递
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
		//若是Window的dispatchTouchEvent返回了false,则点击事件传递给Activity.onTouchEvent
        return onTouchEvent(ev);
    }
复制代码

上面代码为Activity中的dispatchTouchEvent的源码,经过源码咱们能够知道当点击事件发生时,首先会执行onUserInteraction();这个方法又是什么呢?不急,咱们跟踪下去。

//空方法,当该Activity在栈顶时,触屏点击home,back,menu会触发此方法
    public void onUserInteraction() {
    }
复制代码

从源码中能够看出在Activity中该方法为空方法,当该Activity在栈顶时,触屏点击home,back,menu会触发此方法,因此这个方法能够实现屏保功能。让咱们回到Activity中的dispatchTouchEvent方法中,接着调用了getWindow().superDispatchTouchEvent(ev)方法将事件交给Activity所附属的Window进行分发,若是最终事件被消耗了,则返回true,若是事件没人处理,则Activity调用在本身的onTouchEvent()方法来处理事件。

getWindow是一个Window对象,在Window源码中咱们能够发现其实Window就是一个抽象类,显而易见其方法天然是抽象方法,因此咱们必须找出其具体实现类。

源码:Window#superDispatchTouchEvent

/** * Abstract base class for a top-level window look and behavior policy. An * instance of this class should be used as the top-level view added to the * window manager. It provides standard UI policies such as a background, title * area, default key processing, etc. * * <p>The only existing implementation of this abstract class is * android.view.PhoneWindow, which you should instantiate when needing a * Window. */
public abstract class Window {
    ...
     //抽象方法
    public abstract boolean superDispatchTouchEvent(MotionEvent event);
    ...
}
复制代码

从源码中咱们能够发如今Window类前面的注释中是有解释的,这时候就要考验偶们的英语能力辽!其实整体上来讲仍是挺容易理解的,其实咱们只要关注后面一部分的注释就行。

The only existing implementation of this abstract class isandroid.view.PhoneWindow, which you should instantiate when needing a Window.

从这里咱们能够知道它的惟一实现类就是PhoneWindow,废话很少说,咱们直接看看PhoneWindow中superDispatchTouchEvent方法的实现是如何的呢?

源码:PhoneWindow#superDispatchTouchEvent

private DecorView mDecor;
   @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }
复制代码

从源码中能够发现PhoneWindow将事件直接传递给了DecorView,而这个DecorView又是何方圣神呢?以下所示

public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {    
	......
	public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }
    ......
}

@RemoteView
public class FrameLayout extends ViewGroup {
    ......
}
复制代码

其实DecorView就是咱们经过setContentView设置布局的父容器,咱们能够经过getWindow().getDecorView().findViewById(android.R.id.content).getChildAt(0)这个方式就能获取到setContentView中设置的布局。从DecorView的源码中能够发现DecorView是继承FrameLayout,而FrameLayout又是继承ViewGroup的,故DecorView的间接父类为ViewGroup,在DecorView中superDispatchTouchEvent方法是使用super来调用父类的dispatchTouchEvent,故等于调用ViewGroup的dispatchTouchEvent方法(从源码中咱们能够得知FrameLayout并无dispatchTouchEvent这个方法),因而DecorView将事件传递到了ViewGroup去处理。也能够这么说,事件已经传递到了顶级View也就是Activity中经过setContentView所设置的View(顶级View一般为ViewGroup)。

流程图以下:

在这里插入图片描述
到这里,咱们也验证了前面提到的事件分发的顺序是:Activity->Window->DecorView->ViewGroup。那么ViewGroup又是如何将事件传递给View呢?让咱们来继续分析!

2. ViewGroup事件的分发机制

从上面Activity事件的分发机制咱们能够知道,ViewGroup事件分发机制是从dispatchTouchEvent()开始的,因此咱们从这部分的源码开始分析,因为该方法代码量不少,下面将根据须要贴出相关代码:

源码:ViewGroup#dispatchTouchEvent

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        	........
            // Check for interception.
            final boolean intercepted;  //是否拦截
            /* * 当事件由ViewGroup的子元素处理时,mFirstTouchTarget会被赋值并指向子元素 */
            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判断是否要拦截只会是在ACTION_DOWN的时候,或者是mFirstTouchTarget != null。mFirstTouchTarget 从后面的代码才能知道其做用。它的做用就是:当事件被ViewGroup的某个子View处理时,mFirstTouchTarget 就会指向这个子View。因此当事件被这个ViewGroup拦截时,子类就不会处理这个事件,所以mFirstTouchTarget =null,那么这个时候ACTION_MOVE和ACTION_UP事件到来时,因为判断条件为false,将致使ViewGroup的onInterceptTouchEvent不会再被调用,而后intercepted被赋予true,因此同一事件序列的其它事件都会默认交给该ViewGroup来处理。在上面源码中咱们还能够发现这么一句语句:

final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
复制代码

FLAG_DISALLOW_INTERCEPT是个标记位,这个标记位是经过 requestDisallowInterceptTouchEvent(boolean disallowIntercept)方法来设置的,通常用在子View中。若是FLAG_DISALLOW_INTERCEPT被设置后,ViewGroup将没法拦截除了ACTION_DOWN之外的其它点击事件,这是由于ViewGroup在分发事件中,若是是ACTION_DOWN事件,将会重置FLAG_DISALLOW_INTERCEPT这个标记位。让咱们来看看源码。

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();  //对FLAG_DISALLOW_INTERCWPT进行重置
            
            }
复制代码

上面源码是在判断是否拦截的前面的,因此可以重置标记位,从这里咱们也能够发现,当点击事件为ACTION_DOWN时,ViewGroup老是会调用本身的onInterceptTouchEvent来询问本身是否要拦截事件。

requestDisallowInterceptTouchEvent方法针对的是ACTION_DOWN之外的其余事件,而且是在不拦截ACTION_DOWN事件的状况下才会起做用。

接下来让咱们瞧瞧ViewGroup再也不拦截事件的时候,事件的分发状况,源码以下:

final View[] children = mChildren; 
                        for (int i = childrenCount - 1; i >= 0; i--) {  //遍历ViewGroup的全部子元素
                            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);
							
							//dispatchTransformedTouchEvent实际调用的是子元素的dispatchTouchEvent方法
							
                            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);
                                //记录ACTION_DOWN事件已经被处理了
                                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来处理事件时,则调用dispatchTransformedTouchEvent()方法。来看看这个方法重要实现逻辑:

if (child == null) {
                    handled = super.dispatchTouchEvent(event);
                } else {
				   ........
                    handled = child.dispatchTouchEvent(event);
                }
复制代码

能够发现因为在上面中的child并不等于null,因此将直接调用子元素的dispatchTouchEvent方法,使得事件传递到子View上,而后继续分发。

你觉得这样就结束了?答案确定是没有,从上面源码中咱们能够发现当子元素的dispatchTouchEvent返回true后,还有相应操做:

newTouchTarget = addTouchTarget(child, idBitsToAssign);
//记录ACTION_DOWN事件已经被处理了
alreadyDispatchedToNewTouchTarget = true;
break;
复制代码

这几行代码完成了mFirstTouchTarget的赋值并终止了对子元素的遍历。若是子元素的dispatchTouchEvent返回false,则ViewGroup就会把事件分发给下一个元素(若是还有子元素的话),看到这你也许又纳闷了,mFirstTouchTarget的赋值?怎么没看见mFirstTouchTarget的影子呢,答案其实在addTouchTarget这个方法中:

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

这个方法能够看出其实mFirstTouchTarget是一种单链表结构,首先根据坐标点找到了目标子View,而后将子View放在链表头上,从而实现了mFirstTouchTarget!=null。

到这里就完成了ViewGroup一轮的事件分发了,然而尚未结束,若是遍历了全部子元素后事件都没有被合适处理呢?

没有合适处理包括了两种状况:

  • ViewGroup没有子元素
  • 子元素处理了点击事件,可是在dispatchTouchEvent中返回了false(默认是返回true,只有重写View的这个方法或者在onTouchEvent中返回了false)

那么这时候ViewGroup将会本身处理点击事件(当ViewGroup拦截了事件时也是作一样的处理)。

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

能够看出这时候仍是调用了dispatchTransformedTouchEvent方法,不过这时候第三个参数不是child而是null,因此会调用下面这句代码:

//dispatchTransformedTouchEvent
handled = super.dispatchTouchEvent(event);
复制代码

super其实就是View中的dispatchTouchEvent方法,因此点击事件开始交由View来处理。

ViewGroup并无调用onTouchEvent,ViewGroup也没有去重写onTouchEvent

流程图以下:

在这里插入图片描述

3. View事件的分发机制

从上面对ViewGroup事件分发机制可知,View事件分发机制是从dispatchTouchEvent开始的。

源码:View#dispatchTouchEvent

public boolean dispatchTouchEvent(MotionEvent event) {
	    ......
        boolean result = false;
		......
		//判断窗口是否被遮挡,若是被遮挡则返回false,好比有时候两个View是会重叠的,致使其中一个被遮挡了。
        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
		   //判断是否设置了mOnTouchListener,若是设置了onTouchListener,且onTouch方法返回了ture,
            //则result = true
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
			//在result = ture状况下,就不会调用onTouchEvent,可见onTouchListener的优先级高于onTouchEvent
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }
		......
        return result;
    }
复制代码

因为View是一个单独元素,没有子元素能够继续向下传递事件,只能本身处理事件,因此代码也会明显减小。从上面的源码中咱们能够看到View对点击事件的处理过程,result表明是否消耗该事件,而后进行onTouchListener的判断,若是onTouchListenter中的onTouch方法返回了true,那么就不会再调用onTouchEvent方法,因而可知onTouchListener的优先级高于onTouchEvent。

而后来看看onTouchEvent的实现。

public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();

        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

		//不可用状态下点击事件的处理,依然会消耗点击事件
        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设置了代理,将会执行代理的onTouchEvent方法
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }

        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    .....
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    	  ......
                    	  //通过种种判断
                           performClickInternal();
                    break;

                case MotionEvent.ACTION_DOWN:
                    ....
                    break;

                case MotionEvent.ACTION_CANCEL:
                    ....
                    break;

                case MotionEvent.ACTION_MOVE:
                    ....
                    break;
            }
		   //若该控件可点击,就必定返回true
            return true;
        }
        //若该控件不可点击,就必定返回false
        return false;
    }
复制代码

从上面代码能够知道只要View的CLICKABLE,LONG_CLICKABLE,CONTEXT_CLICKABLE有一个为true,那么它就会消耗该事件,无论它是否是DISABLE状态。而后假如控件可点击,就对四种事件类型进行相对应的处理,这里值得一说的是ACTION_UP事件,从源码中能够发如今ACTION_UP事件发生时,会触发performClickInternal方法。这个方法内部实现又是怎样的呢?以下:

private boolean performClickInternal() {
        // Must notify autofill manager before performing the click actions to avoid scenarios where
        // the app has a click listener that changes the state of views the autofill service might
        // be interested on.
        notifyAutofillManagerOnClick();

        return performClick();
    }
复制代码

咱们能够发现最后仍是会调用performClick,而在performClick内部中:

public boolean performClick() {
        // We still need to call this method to handle the cases where performClick() was called
        // externally, instead of through performClickInternal()
        notifyAutofillManagerOnClick();

        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;
    }                                                         
复制代码

只要咱们经过setOnClickListener为View注册点击事件,那么就会给li.mOnClickListener赋值,则会调用onClick方法。

流程图以下:

在这里插入图片描述

从流程图咱们能够发现onTouch,onTouchEvent,onClick的优先级:onTouch>onTouchEvent>onClick

总结

到这里,咱们已经经过源码将点击事件的分发机制梳理一遍了。事件分发的大体过程以下:

  • 当一个点击事件发生后,老是先传递给当前的Activity,由Activity的dispatchTouchEvent进行分发,而Activity会将事件传递给Window,而后由Window的惟一实现类PhoneWindow将事件传递给DecorView,接着DecorView将事件传递给本身的父类ViewGroup,此时的ViewGroup就是经过setContentView所设置的View,故能够称为顶级View,这时候ViewGroup多是本身处理该事件或者传递给子View,可是最终都会调用View的dispatchTouchEvent来处理事件。
  • 在View的dispatchTouchEvent中,若是设置了onTouchListener,会调用其onTouch方法,若是onTouch返回true,则再也不调用onTouchEvent。若是有设置点击事件,则在onTouchEvent会调用onClick方法。若是子View的onTouchEvent返回了false,则表示不消耗事件,事件会回传给上一级的ViewGroup的onTouchEvent,若是全部的ViewGroup都没有返回true,则最终会回传到Activity的onTouchEvent。

参考博客:

相关文章
相关标签/搜索