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 falseswitch (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中这个方法就复杂得多。像之前一样,我们就直接抽取其中的主要逻辑来看。

 @Overridepublic 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 onInterceptTouchEvent(MotionEvent ev)

    /*** 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的小例子,一步一步走过这些坑。

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

Android自定义view之事件传递机制相关推荐

  1. Android自定义View2--触摸事件传递机制

    转载文章 :https://juejin.im/post/6844904041487532045#heading-6 https://juejin.im/post/684490389410388378 ...

  2. android缓冲机制,Android自定义View之双缓冲机制和SurfaceView

    Android自定义View系列 双缓冲机制 问题的由来 CPU访问内存的速度要远远快于访问屏幕的速度.如果需要绘制大量复杂的图像时,每次都一个个从内存中读取图形然后绘制到屏幕就会造成多次地访问屏幕, ...

  3. 安卓自定义View进阶-事件分发机制原理

    之前讲解了很多与View绘图相关的知识,你可以在 安卓自定义View教程目录 中查看到这些文章,如果你理解了这些文章,那么至少2D绘图部分不是难题了,大部分的需求都能满足,但是关于View还有很多知识 ...

  4. 精通Android自定义View(十三)事件分发简述

    1 事件序列 (1)手指接触屏幕后会产生一系列事件,事件分为3种:ACTION_DOWN(手指刚刚接触屏幕).ACTION_MOVE(手指在屏幕移动).ACTION_UP(手指从屏幕松开) (2)一个 ...

  5. 安卓自定义View进阶-事件分发机制原理【转自 app架构师 微信公众号】

    注意:本文中所有源码分析部分均基于 API23(Android 6.0) 版本,由于安卓系统源码改变很多,可能与之前版本有所不同,但基本流程都是一致的. 为什么要有事件分发机制? 安卓上面的View是 ...

  6. 安卓自定义View进阶-事件分发机制详解

    原文地址:http://www.gcssloop.com/customview/dispatch-touchevent-source Android 事件分发机制详解,在上一篇文章 事件分发机制原理  ...

  7. Android自定义view之ViewPager指示器——1

    Android自定义view之ViewPager指示器--1 在上两篇文章<Android自定义view之measure.layout.draw三大流程>以及<Android自定义v ...

  8. Android自定义View之Paint绘制文字和线

    Android自定义View系列 Android自定义View注意事项 Android自定义View之图像的色彩处理 Android自定义View之Canvas Android自定义View之轻松实现 ...

  9. Android自定义View注意事项

    Android自定义View系列 Android自定义View之Paint绘制文字和线 Android自定义View之图像的色彩处理 Android自定义View之Canvas Android自定义V ...

最新文章

  1. SAP EWM 代码实现Transportation Unit(TU)的创建
  2. 万万没想到,“红孩儿”竟然做了程序员,还是 CTO!
  3. 使Struts2与Servlet并存解决办法 Filter转发Servlet
  4. [vue] 什么是双向绑定?原理是什么?
  5. android逐行写入读取_Android外部存储-读取,写入,保存文件
  6. PyTorch学习—9.模型容器与AlexNet构建
  7. DAY16-Django之model
  8. Android ViewFlipper源码分析
  9. Flex布局应用---导航栏实现
  10. [Python] 微信for PC自动群发消息、图片以及文件
  11. windows7系统如何连接远程桌面
  12. 弘辽科技:如何做好淘宝店铺推广?有什么技巧吗?
  13. 什么是大数据?大数据学习路线和就业方向
  14. void 和 void *区别(c++)
  15. 括号配对检测python123_括的意思
  16. 米扑科技助力公益:寻找失踪儿童一起回家
  17. 通过蒲公英让两台异地电脑组建局域网
  18. 阿尔法围棋击败人类是计算机在那方面的应用,阿尔法围棋战胜人类:人工智能又一胜利...
  19. 问题解决:VScode高CPU占有率 Microsoft.VSCode.CPP.Extension.darwin
  20. 【日语】流行日语【二】

热门文章

  1. 本体学习的概念及目标
  2. RabbitMQ学习笔记(3)----RabbitMQ Worker的使用
  3. 使用Android DataBinding BindingAdapter和Dagger 2
  4. Linux——SUID、SGID、SBIT简介
  5. java 输出helloword
  6. POJ 1141 Brackets Sequence
  7. 除了随机还要进化——对Infinity进一步的想法
  8. Cygwin中解决vi编辑器方向键和Backspace键不好使、安装vim的方法
  9. Python3 捕捉异常
  10. emctl start dbconsole失败问题的解决