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

来源:互联网 发布:模拟和网络监控的区别 编辑:程序博客网 时间:2024/05/16 06:21

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.     * <p>     * 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.     *     * <p>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的事件发生的区域中没有子view时会怎么办。此时即使ViewGroup在onInterceptTouchEvent()中返回了false,那么事件仍然还是会交给ViewGroup自己处理。ViewGroup的dispatchTouchEvent()的流程中,会先找到事件的坐标对应的子View,然后调用dispatchTransformedTouchEvent()来执行真正的事件分发逻辑,该函数声明如下:

    /**     * Transforms a motion event into the coordinate space of a particular child view,     * filters out irrelevant pointer ids, and overrides its action if necessary.     * If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.     */    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,            View child, int desiredPointerIdBits)

从注释很清楚可以看到,它会对事件执行变换操作,然后分发下去。如果没有对应的子view,那么事件就会分发给ViewGroup本身来处理。

以上基本就是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的小例子,一步一步走过这些坑。

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

原创粉丝点击