Android自定义view之事件传递机制

Android自定义view之事件传递机制

在上一篇文章《Android自定义view之measure、layout、draw三大流程》中,我们探讨了一下view的显示过程。不太熟悉的同学可以看下上篇文章巩固一下。本篇我们将一起探讨一下Android的事件分发机制,也就是触摸事件的流程。对于一个view来说,对动作的控制和显示一样重要。
本文一些知识点来自于《Android开发艺术探索》,在此感谢作者。文章中如有纰漏,欢迎留言讨论。

本文将会由浅入深讲解,事件分发机制不过是几个函数而已,只是其中的细节比较繁杂。控件分为两种:View和ViewGroup,事件分发流程有略微不同。

0. View的事件:MotionEvent类

开始之前,我们首先需要了解下包装事件的类:MotionEvent。Android的触摸事件是包装在这个类的对象之中,通过这个类,我们可以获取事件的各种信息,比如坐标值、事件发生时间、事件类型等。下面列举一些常用的方法:
(1) public final float getRawX() / getRawX()
这两个方法返回的是触摸点在屏幕上的绝对坐标,坐标值相对于屏幕而言。
(2) public final float getY() / getY(int index) / getX() / getX(int index)
返回触摸点基于该View的坐标值,有参数的方法则会返回某个点的坐标值,无参数的方法返回index为0的点的坐标值。这是针对多点触控。index值范围从0到getPointerCount() – 1。
(3) public final float getAction() / getActionMasked()
返回事件类型。getAction返回4种常用类型:ACTION_DOWN、ACTION_MOVE、ACTION_UP、ACTION_CANCEL。getActionMasked则可以多返回两种:ACTION_POINTER_DOWN、ACTION_POINTER_UP,它们代表是多点触控时有其他手指落下或抬起。某些时候,比如滚动,为了防止抬起落下多根手指时出现跳动,我们是需要检测并计算多点触控的。因此推荐直接用getActionMasked。
(4) public final void offsetLocation(float deltaX, float deltaY)
将一个事件中的坐标值进行位移变换。这个通常是在自定义滚动控件的时候会用到。由于滚动有两种方式,一种是改变子控件的位置,另一种就是利用方法setScrollY(int value) / setScrollX(int value),这两个方法会影响View类中的mScrollX / mScrollY两个属性,而这两个属性会影响View在分发事件以及绘制时的行为。简而言之,我们可以把View看做一个很大的画布,而我们能看到的部分其实就是一个屏幕大小的窗口,mScrollX / mScrollY则决定这个窗口在画布上的位置。这都是后话。

以上就是MotionEvent类中的主要内容。另外需要注意的是事件流,事件序列就是从触摸屏幕开始,到所有手指离开屏幕,其中会包含移动、另外的手指落下、抬起,这就是一个事件流。所以事件序列总是以ACTION_DOWN开始,以ACTION_UP结束。另外需要注意的是,CPU的处理速度很快,那些你以为很快的点击只是点击而已,其实基本绝大多数的点击都会有ACTION_MOVE的,在处理事件的时候尤其注意。

1. View的事件分发流程

首先了解一下View类的事件分发流程,毕竟View类是所有控件的父类。由于View类的源码比较繁杂,我们就直接列出和事件分发有关的函数。

(1)public boolean dispatchTouchEvent(MotionEvent event)

最关键的就是public boolean dispatchTouchEvent(MotionEvent event)这个函数,它是负责分发事件的,当一个事件到达一个view,首先调用的就是这个函数。在View类中它的注释是

/** * Pass the touch screen motion event down to the target view, or this * view if it is the target. * * @param event The motion event to be dispatched. * @return True if the event was handled by the view, false otherwise. */

很简单的功能,将一个事件分发下去,如果它自己就是目标view,那么就它自己消化这个事件。参数是要分发的事件,返回true时代表它或者它的子view消化了这个事件,返回false代表它以及它的子view都不消化这个事件。由于这里是View,因此不会有子view存在,因此它只负责检查自己是否能消化这个事件。所以将它简化后我们能得到下面的流程伪代码:

public boolean dispatchTouchEvent(MotionEvent event) { boolean result = false; … if(onTouchListener != null) { result = onTouchListener.onTouchEvent(event); } if(!result &;&; onTouchEvent(event)) { result = true; } … return result; }

这便是一个简化的流程,其他的部分都省略掉了,毕竟现在的关注点不是那些。我们可以看到,首先这个函数会检查View的onTouchListener,如果它不为空,那么就将事件传递给它处理。如果它返回了true,在下面的步骤中就不会调用onTouchEvent,最后返回result。如果它返回了false,那么还会调用onTouchEvent,如果onTouchEvent返回了false,那么最后result就是false,否则result为true。
然后看一下OnTouchListener这个接口,它其实只有onTouch一个函数:

/** * Interface definition for a callback to be invoked when a touch event is * dispatched to this view. The callback will be invoked before the touch * event is given to the view. */ public interface OnTouchListener { /** * Called when a touch event is dispatched to a view. This allows listeners to * get a chance to respond before the target view. * * @param v The view the touch event has been dispatched to. * @param event The MotionEvent object containing full information about * the event. * @return True if the listener has consumed the event, false otherwise. */ boolean onTouch(View v, MotionEvent event); }

注释写得很明白。这个接口对象如果不为空,那么它就会在调用onTouchEvent之前被调用。其实这也就是给了我们一个在view对事件进行反应之前来处理事件的机会,如果我们在这个接口中返回true,即消化这个事件,那么view就不会对事件作出反应了,同样的,我们也可以在此之前对事件进行加工来达到各种效果。
接下来看onTouchEvent,毕竟OnTouchListener这么针对它了,那它的地位肯定非常重要。

(2)public boolean onTouchEvent(MotionEvent event)

这个函数相比于第一个就不是很容易看明白了,不过就算源码看不明白,咱们也不能放过注释。

/** * Implement this method to handle touch screen motion events. * * If this method is used to detect click actions, it is recommended that * the actions be performed by implementing and calling * {@link #performClick()}. This will ensure consistent system behavior, * including: * <ul> * <li>obeying click sound preferences * <li>dispatching OnClickListener calls * <li>handling {@link AccessibilityNodeInfo#ACTION_CLICK ACTION_CLICK} when * accessibility features are enabled * </ul> * * @param event The motion event. * @return True if the event was handled, false otherwise. */

翻译:实现这个函数来处理触摸事件。如果要在这个函数中判断点击动作,推荐使用performClick()函数来进行点击操作,因为它可以确保一些系统响应,包括点击音效、调用OnClickListener等。

很简单明了,它就是view真正消化触摸事件并作出响应的地方。其实一般来讲,一个普通的onTouchEvent函数内部可能会是如下的结构:

public boolean onTouchEvent(MotionEvent event) { boolean result = true; //or false switch (event.getAction()) { case MotionEvent.ACTION_DOWN … break; case MotionEvent.ACTION_MOVE: … break; case MotionEvent.ACTION_UP: … break; … } return result; }

其实就是对触摸的不同动作来响应,比如我们会在View类中看到setPress函数,它一般就是在ACTION_DOWN时调用,来反馈控件被按下的状态。刚才提到的performClick()函数则是在ACTION_UP时调用。
接下来我们需要看一下performClick()函数:

/** * Call this view’s OnClickListener, if it is defined. Performs all normal * actions associated with clicking: reporting accessibility event, playing * a sound, etc. * * @return True there was an assigned OnClickListener that was called, false * otherwise is returned. */ 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; }

可以清楚看到它调用了OnClickListener.onClick,同时也有playSoundEffect。
现在我们就基本清楚了View类的事件分发流程,也知道了我们平时用的OnClickListener等监听器是在何处被调用的。但是对于自定义View来说,我们通常是会重写onTouchEvent函数的,所以其中的细节往往比较麻烦,包括何时判断为点击、长按和双击等操作,以及相应的操作造成的View的音效、视觉反馈等。而这些都只能靠我们自己。

以上基本就是View类的事件分发过程。接下来探索ViewGroup类的事件分发流程。相比于View类,ViewGroup会比较复杂些,因为它不但自己可以消耗事件,也要负责将事件传递给自己的子View。

2. ViewGroup的事件分发流程

由于是View类的子类,因此方法上肯定大同小异。我们还是先从dispatchTouchEvent(MotionEvent event)开始看起。

(1)public boolean dispatchTouchEvent(MotionEvent event)

相比于View类,ViewGroup中这个方法就复杂得多。像之前一样,我们就直接抽取其中的主要逻辑来看。

@Override public boolean dispatchTouchEvent(MotionEvent ev) { boolean consumed = false; if(onInterceptTouchEvent(ev)) { consumed = super.dispatchTouchEvent(ev); }else { consumed = dispatchTouchEventToChild(ev); } return consumed; }

这就是其中的主要逻辑。我们可以看到,事件分发有两条路,一条是ViewGroup自己消化,也就是super.dispatchTouchEvent(ev),另一条则是分发给子view,dispatchTouchEventToChild(ev)(需要注意的是源码里并没这个方法,这里只是伪代码)。而决定事件去向的,明显就是onInterceptTouchEvent(ev)这个方法了。这个方法在View类中并没有,下面看一下它的源码和注释。

(2)public boolean dispatchTouchEvent(MotionEvent event)

/** * Implement this method to intercept all touch screen motion events. This * allows you to watch events as they are dispatched to your children, and * take ownership of the current gesture at any point. * * Using this function takes some care, as it has a fairly complicated * interaction with {@link View#onTouchEvent(MotionEvent) * View.onTouchEvent(MotionEvent)}, and using it requires implementing * that method as well as this one in the correct way. Events will be * received in the following order: * * <ol> * <li> You will receive the down event here. * <li> The down event will be handled either by a child of this view * group, or given to your own onTouchEvent() method to handle; this means * you should implement onTouchEvent() to return true, so you will * continue to see the rest of the gesture (instead of looking for * a parent view to handle it). Also, by returning true from * onTouchEvent(), you will not receive any following * events in onInterceptTouchEvent() and all touch processing must * happen in onTouchEvent() like normal. * <li> For as long as you return false from this function, each following * event (up to and including the final up) will be delivered first here * and then to the target’s onTouchEvent(). * <li> If you return true from here, you will not receive any * following events: the target view will receive the same event but * with the action {@link MotionEvent#ACTION_CANCEL}, and all further * events will be delivered to your onTouchEvent() method and no longer * appear here. * </ol> * * @param ev The motion event being dispatched down the hierarchy. * @return Return true to steal motion events from the children and have * them dispatched to this ViewGroup through onTouchEvent(). * The current target will receive an ACTION_CANCEL event, and no further * messages will be delivered here. */ public boolean onInterceptTouchEvent(MotionEvent ev) { return false; }

好家伙,代码没多少全是注释。我给大家把注释翻译在下面:

翻译:实现这个方法拦截所有从屏幕上传来的触摸事件。这允许你监测所有传递到子view的事件,并且可以在任何节点获取对事件的掌控。
使用这个方法时需要注意,因为它和OnTouchEvent方法具有相互的作用,并且需要和这个方法一样正确地实现它。事件将会按照如下的顺序被接收:
1. 你将会在本方法中接收到落下事件(ACTION_DOWN)
2. 落下事件将会被该ViewGroup的子view处理。或者由你自己的onTouchEvent()方法处理;这意味着你应该实现onTouchEvent()并且使之返回true,这样你就可以收到这个手势剩下的事件(而不是指望父view来处理它)。同时,如果你在onTouchEvent()中返回true,那么你就不会在onInterceptTouchEvent()中收到任何接下来的事件,所有的事件都会在你的onTouchEvent()中像往常一样处理。
3. 一旦你从这里返回false,接下来的所有事件都会被首先交到这里,然后才交给目标View的onTouchEvent()方法。
4. 如果你从这里返回true,你就不会在这个方法里收到接下来的任何事件:目标子view也会收到这个事件但是动作是ACTION_CANCEL,并且接下来所有事件都会被直接提交到你的onTouchEvent()方法中而不会出现在这里。
返回值:true意味着你将会拦截从此开始所有的事件并将事件发送到该ViewGroup的onTouchEvent()中,当前的目标子view将会收到ACTION_CANCEL,并且之后也不会有事件被发送到这里。

说了那么多,第2条到第4条有点绕(本人英语力不强啊)。其实简单说,返回true代表你要拦截这个事件自己处理,返回false代表你不拦截这个事件,可以把它分发到子view中。你可以在任何时候从这个方法中拦截事件,一旦你拦截了,那么该事件和之后的事件都会被直接交给该ViewGroup的onTouchEvent()处理并且不会再出现在这里,意味着接下来事件分发就不会再调用onInterceptTouchEvent()了;并且之前已经收到事件的子view会收到一个ACTION_CANCEL以做出响应。如果你没有拦截,事件就会被分发到子view中,并且在整个事件流过程中,分发事件时这个函数都会被调用,意味着你仍然有机会在任何时候拦截事件。

以上说的事件以及过程是指一个事件流中,每当新的事件流发生时(以ACTION_DOWN开始),所有过程是重新来过的,之前是否拦截不会对后面的过程产生影响,这也很好理解。

以上基本就是ViewGroup的事件分发流程。按照顺序我们接下来要看onTouchEvent()方法,但在ViewGroup类中并没有重写这个方法,而是沿用了View类的(super.dispatchTouchEvent(ev)),这也很好理解,毕竟View类已经把消耗事件写好了,ViewGroup就没必要自己再写一遍。

onInterceptTouchEvent()和onTouchEvent()方法联系得非常紧密,大家在重写这两个方法的时候一定要控制好。

3. 特殊情况及注意事项

这一部分的内容说实话在正常情况下比较少发生(如果你写代码的时候考虑得够周全的话)。不过肯定有车到山前的时候,所以我列出了供大家遇到的时候有路可循。

  • 事件的传递总是由外向内的,即从父元素传递给子元素。但是子元素可以通过调用requestDisallowInterceptTouchEvent(boolean)方法来干预父元素的分发流程。顾名思义,传入true代表我们要求父控件不要拦截这个事件。详细用法大家可以自行再查。

好吧,很特殊的情况其实我也没想到有别的,基本很多的情况在上面我们探索源码的过程中就讲得很明白了。大家灵活运用一定可以应对各种情况。

4. 总结

事件分发流程对于View类和ViewGroup类是不同的。
对于View
事件到达一个View时,首先会调用dispatchTouchEvent(MotionEvent event)。然后这个方法会先调用OnTouchListener.onTouch()方法(如果注册过OnTouchListener的话),如果OnTouchListener不消耗事件,那么会接着调用View的onTouchEvent()方法。
对于ViewGroup
事件到达一个ViewGroup时,同样先调用dispatchTouchEvent(MotionEvent event)方法,不过这个方法和View类中的有不同。该方法会首先调用onInterceptTouchEvent()方法是否拦截这个事件。如果拦截,则交由该ViewGroup自己的onTouchEvent()方法(其实就是走了super.dispatchTouchEvent(ev)流程,和上面View类处理事件的流程一样了)。如果不拦截,则会将这个事件分发给子view。所以对于ViewGroup来说,我们可以在两个地方拦截事件:一是onInterceptTouchEvent(),二是OnTouchListener.onTouch(),只要在这两个方法中的任何一个返回true,ViewGroup的onTouchEvent()都不会被调用。(ViewGroup:我好可怜(:з」∠))

以上就是Android的基本的事件分发流程了。看起来其实比较容易,也比上一章绘制显然篇幅小得多,不过用起来就会知道坑其实还是挺多的。后续我会写几篇自定义view的小例子,一步一步走过这些坑。

如果有错误或者疑问,欢迎大家留言讨论。