Android:View的事件分发与消费机制

jpug6876 8年前
   <h2>写在前面</h2>    <p>最近一直在看自定义控件的一些知识,基本弄清楚自定义控件的一般流程。我们知道一般自定义控件都需要重写控件的触摸事件。而自定义控件需要继承 View /ViewGroup或者其他已有的控件 ,这个时候我们就要考虑到View中一个非常重要且难懂的知识——事件分发与消费机制。我自己也在学习的过程中出现过一些由于没有处理好触摸事件的分发而导致的一系列滑动触摸问题,所以花了接近三天的时间彻底弄明白View中的这样一个机制。</p>    <p>在学习过程中,我参考了很多资料,除了黑马的教程以外,最重要的两个资料是《Android开发艺术探索》和一个开发大牛的博客。现在我用语言来把自己所理解的写出来,内容也大部分参考这两个资料。有些内容就是书中或博客中的原文,只是我用自己的理解来解释说明。本文中所用的项目也是引用这个博客的项目,不是图方便,而是没有什么例子比这个项目更好且更能说明这个问题了。</p>    <p>借鉴的博客地址为:<a href="http://www.open-open.com/lib/view/open1463017270047.html">Android 编程下 Touch 事件的分发和消费机制</a></p>    <h2>一、Touch的三个重要方法</h2>    <p>在Android中,与触摸事件也就是 Touch 相关的有三个重要方法,这三个方法共同完成触摸事件的分发。</p>    <ul>     <li>public boolean dispatchTouchEvent(MotionEvent ev) :事件分发</li>     <li>public boolean onInterceptTouchEvent(MotionEvent ev):事件拦截</li>     <li>public boolean onTouchEvent(MotionEvent ev):事件响应</li>    </ul>    <p>下面就依次来分析这三个方法。</p>    <h3>1、事件分发</h3>    <p><strong>public boolean dispatchTouchEvent(MotionEvent ev)</strong></p>    <p>顾名思义,事件的分发就是当一个触摸事件发生的时候,会按照<strong>Activity -> Window -> View</strong>的顺序依次往下传递。也就是说系统会把这个事件传递给一个具体的View,从而来执行或者说响应这个事件。我们来看博客上是如何说明的:</p>    <p>Touch 事件发生时 Activity 的 <strong>dispatchTouchEvent(MotionEvent ev) </strong>方法会以隧道方式(从根元素依次往下传递直到最内层子元素或在中间某一元素中由于某一条件停止传递)将事件传递给最外层 View 的 dispatchTouchEvent(MotionEvent ev) 方法,并由该 View 的 dispatchTouchEvent(MotionEvent ev) 方法对事件进行分发。</p>    <p>这里要注意几个地方:</p>    <blockquote>     <p>第一触摸事件传递的开始一定是Activity;</p>     <p>第二传递方式是通过隧道方式传递;</p>     <p>第三一直传递到一个最外层的View,也就是顶级View,由该View的这个方法来进行分发。</p>    </blockquote>    <p>那么我们不禁有个疑问,那Activity能不能直接分发呢,换句话说,传递过程什么时候终止呢?答案就是通过判断这个方法的返回值来处理分发的逻辑。我们看到,分发方法的返回值是 boolean ,所以返回值有 true 和 false ,再加上一个继承超类的方法 super ,所以一共有三种返回值。<br> 依次来看:</p>    <ul>     <li> <p>如果 return true,事件会分发给当前 View 并由 dispatchTouchEvent 方法进行消费,同时事件会停止向下传递;</p> </li>     <li> <p>如果 return false,事件会分发给事件来源Activity或者父级View的 onTouchEvent 进行消费;</p> </li>     <li> <p>如果return super.dispatchTouchEvent(ev),事件会自动的分发给当前 View 的 onInterceptTouchEvent 方法。</p> </li>    </ul>    <p>博客上的这个结论单独拿来看可能有点抽象,我当时看的时候也是,先不急着说清楚,稍后看案例就恍然大悟了。现在只要明白一个概念,事件的分发是按照依次往下的顺序,并根据返回结果,决定由谁进行消费。</p>    <h3>2、事件拦截</h3>    <p><strong>public boolean onInterceptTouchEvent(MotionEvent ev)</strong></p>    <p>与事件分发不同的是,该方法是在事件分发的 dispatchTouchEvent 方法内部进行调用。是用来判断在触摸事件传递过程中,是否拦截某个事件。博客的解释是这样的:</p>    <blockquote>     <p>在外层 View 的 dispatchTouchEvent(MotionEvent ev) 方法返回系统默认的 super.dispatchTouchEvent(ev) 情况下,事件会自动的分发给当前 View 的 onInterceptTouchEvent 方法。</p>    </blockquote>    <p>同样的是,该方法仍然通过返回值来判断是否拦截当前事件。</p>    <ul>     <li> <p>如果return true,则表示将事件进行拦截,并将拦截到的事件交由当前 View 的 onTouchEvent 进行处理;</p> </li>     <li> <p>如果return false,则表示将事件放行,当前 View 上的事件会被传递到子 View 上,再由子 View 的 dispatchTouchEvent 来开始这个事件的分发;</p> </li>     <li> <p>如果return super.onInterceptTouchEvent(ev),事件默认会被拦截,并将拦截到的事件交由当前 View 的 onTouchEvent 进行处理。</p> </li>    </ul>    <p>当然现在我们也不需要深究是什么意思,只需注意,如果当前的View已经拦截了某一个事件,那么在触摸事件的一整个事件序列中,也就是down -> move -> ... -> up一整个事件中,此方法不会被在调用。</p>    <h3>3、事件响应</h3>    <p><strong>public boolean onTouchEvent(MotionEvent ev)</strong></p>    <p>这个方法就是用来处理具体点击事件的,它是在dispatchTouchEvent方法中调用,博客是这样说的:</p>    <blockquote>     <p>在 dispatchTouchEvent 返回 super.dispatchTouchEvent(ev) 并且 onInterceptTouchEvent 返回 true 或返回 super.onInterceptTouchEvent(ev) 的情况下 onTouchEvent 会被调用。</p>    </blockquote>    <p>它的返回值表示是否消费当前事件,也就是是否响应当前事件,具体逻辑如下:</p>    <ul>     <li> <p>如果return true 则会接收并消费该事件。</p> </li>     <li> <p>如果return false,那么这个事件会向上传递,并由上层 View 的 onTouchEvent 来接收,如果传递到上面的 onTouchEvent 还是返回 false,这个事件无效,且接收不到下一次事件。</p> </li>     <li> <p>如果return super.onTouchEvent(ev) 默认处理事件的逻辑和返回 false 时相同。</p> </li>    </ul>    <p>值得注意的是,返回结果表示是否消费当前事件,如果不消费的话,那么当前View就无法再次接受到事件。</p>    <p>那么看到这里的话,我相信大家还是没有一个清晰的认识,下面我们就从三者的联系与区别上再次说明。</p>    <h2>二、三种方法的区别与联系</h2>    <h3>1、区别</h3>    <p>三者的区别在博客开头已经说明的非常清楚了,我也仿照文章中表格的形式,总结了一个表格。</p>    <p><img src="https://simg.open-open.com/show/708eda35d478c59fa34d762778e2bd95.png" alt="Android:View的事件分发与消费机制" width="1067" height="131"></p>    <p>Touch事件</p>    <p>也就是说,这三个触摸事件相关的方法,Activity、View、ViewGroup及其子类都能够响应。但是Activity对事件拦截不响应。</p>    <p>值得注意的是,如果当前View能够添加子View或者整个View中有多个子View,那么方法都能响应。但是如果当前View本身已经是一个最小View,那就只能够响应onTouchEvent。</p>    <p>原因简单想一下就知道了,事件分发与事件拦截都是由上级往下级传递事件,如果一个View已经是最后一级了,它就无法进行事件分发或事件拦截的必要了。就相当于做汽车,中间站可能会停站进行让乘客上车下车,但是到了终点站你只有下车,这是一样的道理。</p>    <h3>2、联系</h3>    <p>上面已经说明了三者的区别,那么三者的关系是怎么样的呢?在《Android开发艺术探索》一书中,有这样一串伪代码,就跟书中说明的一样,“已经将三者的关系表现得淋漓尽致”,我们来看这串伪代码:</p>    <pre>  <code class="language-java">/**  * @Title: dispatchTouchEvent  * @Description: 三者关系的伪代码  * @return: boolean  *  /  public boolean dispatchTouchEvent(MotionEvent ev){      //默认返回值      boolean consume = false;        //如果事件发生了拦截      if(onInterceptTouchEvent(ev)){          //消费事件          consume = onTouchEvent(ev);      }else{          //否则分发给子View          consume = child.dispatchTouchEvent(ev);      }        //返回值      return consume;  }</code></pre>    <p>用语言来说就是,一旦发生触摸事件,根 View/ViewGroup 会调用 dispatchTouchEvent 方法,如果这个方法返回 false ,触摸事件不生效。但是如果它的 onInterceptTouchEvent 方法返回了true,代表事件被拦截,那么事件就会交给当前View的 onTouchEvent 方法对事件进行消费。但是如果没有拦截呢,那么会继续将事件分发给当前View的子View,子View继续调用 dispatchTouchEvent 方法,如此循环,直到事件被消费。</p>    <p>但是需要注意的是,我们需要考虑另外一种情况,那就是最终的View的 onTouchEvent 方法仍然返回了false,那么此时,它的父View的 onTouchEvent 方法将会被调用,如果父View仍然没有消费该事件,那么就继续往上级传递,直到传到最后的Activity调用 onTouchEvent 方法。</p>    <p>这就跟我们之前的返回值一一对应了起来,现在回头看看,应该能对事件分发有了一个比较清晰的概念。好吧,如果还是没有概念,我们只能上图了。图也是黑马教程中的图,我优化了一下,看的更加清晰。</p>    <p><img src="https://simg.open-open.com/show/104ecf967f3cfbf7155a7dac6e326397.png" alt="Android:View的事件分发与消费机制" width="1024" height="635"></p>    <p>事件消费</p>    <p>至此,我们理论上的东西基本已经讲完了,现在我们就通过一个例子来说明具体的事件分发情况。</p>    <h2>三、案例分析</h2>    <h3>1、案例说明</h3>    <p>文章开头已经说过了,没有更好的例子能说明这个问题了。我将类的结构稍微更改了一下,内容不变。</p>    <p>先自定义两个View继承 LinearLayout ,其中为 TouchEventFather 为父View ,TouchEventChilds 为子View。</p>    <pre>  <code class="language-java">/**   * @ClassName: TouchEventFather   * @Description:父View   * @author: iamxiarui@foxmail.com   * @date: 2016年5月9日 下午9:54:14   */  public class TouchEventFather extends LinearLayout {         public TouchEventFather(Context context) {           super(context);       }         public TouchEventFather(Context context, AttributeSet attrs) {           super(context, attrs);       }         public boolean dispatchTouchEvent(MotionEvent ev) {           Log.e("sunzn", "TouchEventFather | dispatchTouchEvent --> " + TouchEventUtil.getTouchAction(ev.getAction()));           return super.dispatchTouchEvent(ev);           // return false;       }         public boolean onInterceptTouchEvent(MotionEvent ev) {           Log.i("sunzn", "TouchEventFather | onInterceptTouchEvent --> " + TouchEventUtil.getTouchAction(ev.getAction()));           return super.onInterceptTouchEvent(ev);           // return false;       }         public boolean onTouchEvent(MotionEvent ev) {           Log.d("sunzn", "TouchEventFather | onTouchEvent --> " + TouchEventUtil.getTouchAction(ev.getAction()));           return super.onTouchEvent(ev);       }    }    /**   * @ClassName: TouchEventFather   * @Description:子View   * @author: iamxiarui@foxmail.com   * @date: 2016年5月9日 下午9:54:14   */  public class TouchEventChilds extends LinearLayout {         public TouchEventChilds(Context context) {           super(context);       }         public TouchEventChilds(Context context, AttributeSet attrs) {           super(context, attrs);       }         public boolean dispatchTouchEvent(MotionEvent ev) {           Log.e("sunzn", "TouchEventChilds | dispatchTouchEvent --> " + TouchEventUtil.getTouchAction(ev.getAction()));           return super.dispatchTouchEvent(ev);           // return false;       }         public boolean onInterceptTouchEvent(MotionEvent ev) {           Log.i("sunzn", "TouchEventChilds | onInterceptTouchEvent --> " + TouchEventUtil.getTouchAction(ev.getAction()));           return super.onInterceptTouchEvent(ev);           // return false;       }         public boolean onTouchEvent(MotionEvent ev) {           Log.d("sunzn", "TouchEventChilds | onTouchEvent --> " + TouchEventUtil.getTouchAction(ev.getAction()));           return super.onTouchEvent(ev);       }    }</code></pre>    <p>定义好这两个自定义布局后,我们可以在布局文件中设置布局,注意一定要写自定义布局的完整包名。</p>    <pre>  <code class="language-java"><xml version="1.0" encoding="utf-8"?>  <cn.sunzn.tevent.view.TouchEventFather xmlns:android="http://schemas.android.com/apk/res/android"       android:layout_width="fill_parent"       android:layout_height="fill_parent"       android:background="#468AD7"       android:gravity="center"       android:orientation="vertical">         <cn.sunzn.tevent.view.TouchEventChilds           android:id="@+id/childs"           android:layout_width="200dp"           android:layout_height="200dp"           android:layout_gravity="center"           android:background="#E1110D" />    </cn.sunzn.tevent.view.TouchEventFather></code></pre>    <p>接下来就是主Activity:</p>    <pre>  <code class="language-java">/**    * @ClassName: TouchEventActivity   * @Description:事件分发机制详解   * @author: iamxiarui@foxmail.com   * @date: 2016年5月9日 下午9:53:27   */  public class TouchEventActivity extends Activity {         public void onCreate(Bundle savedInstanceState) {           super.onCreate(savedInstanceState);           setContentView(R.layout.main);       }         public boolean dispatchTouchEvent(MotionEvent ev) {           Log.w("sunzn", "TouchEventActivity | dispatchTouchEvent --> " + TouchEventUtil.getTouchAction(ev.getAction()));           return super.dispatchTouchEvent(ev);       }         public boolean onTouchEvent(MotionEvent event) {           Log.w("sunzn", "TouchEventActivity | onTouchEvent --> " + TouchEventUtil.getTouchAction(event.getAction()));           return super.onTouchEvent(event);       }    }</code></pre>    <p>最后是一个工具类,只是将各个点击状态封装到一个方法中:</p>    <pre>  <code class="language-java">/**    * @ClassName: TouchEventUtil   * @Description:点击事件工具类   * @author: iamxiarui@foxmail.com   * @date: 2016年5月9日 下午9:53:51   */  public class TouchEventUtil {         public static String getTouchAction(int actionId) {           String actionName = "Unknow:id=" + actionId;           switch (actionId) {               case MotionEvent.ACTION_DOWN:                   actionName = "ACTION_DOWN";                   break;               case MotionEvent.ACTION_MOVE:                   actionName = "ACTION_MOVE";                   break;               case MotionEvent.ACTION_UP:                   actionName = "ACTION_UP";                   break;               case MotionEvent.ACTION_CANCEL:                   actionName = "ACTION_CANCEL";                   break;               case MotionEvent.ACTION_OUTSIDE:                   actionName = "ACTION_OUTSIDE";                   break;          }          return actionName;      }    }</code></pre>    <p>好了,代码介绍完了,部署到手机上的时候,应该是这个样子。</p>    <p><img src="https://simg.open-open.com/show/a888e1fd45d11d0a5954c1045e69a29e.png" alt="Android:View的事件分发与消费机制" width="247" height="332"></p>    <p>事件分发案例</p>    <p>我们现在就通过不同的返回值,来具体看事件分发的过程,注意我们将代码部署到手机上的时候,默认做的动作是点击中间红色部分一下,这样更能直观的观察日志情况。另外由于原博主总结的非常好,这里我就直接截图过来,然后具体说明一下。</p>    <h3>2、情况一</h3>    <p><img src="https://simg.open-open.com/show/c2bf72fdb44eab7c4573713ff54d0239.png" alt="Android:View的事件分发与消费机制" width="954" height="423"></p>    <p>case1</p>    <p><strong>过程及结果分析:</strong></p>    <ul>     <li> <p>事件首先由 TouchEventActivity 的 dispatchTouchEvent 方法分发给 TouchEventFather 控件的dispatchTouchEvent;</p> </li>     <li> <p>而该TouchEventFather 控件的 dispatchTouchEvent 返回 false,表示对获取到的事件停止向下传递,同时也不对该事件进行消费;</p> </li>     <li> <p>由于 TouchEventFather 获取的事件直接来自 TouchEventActivity ,则会将事件返回给 TouchEventActivity 的 onTouchEvent 进行消费;</p> </li>     <li> <p>最后直接由 TouchEventActivity 来响应手指移动和抬起事件。</p> </li>    </ul>    <h3>3、情况二</h3>    <p><img src="https://simg.open-open.com/show/9a13c20c927a8a0feabea5ba33cd386a.png" alt="Android:View的事件分发与消费机制" width="950" height="452"></p>    <p>case2</p>    <p><strong>过程及结果分析:</strong></p>    <ul>     <li> <p>事件首先由 TouchEventActivity 的 dispatchTouchEvent 方法分发给 TouchEventFather 控件的 dispatchTouchEvent;</p> </li>     <li> <p>而该TouchEventFather 控件的 dispatchTouchEvent 返回 true,表示分发事件到 TouchEventFather 控件并由该控件的 dispatchTouchEvent 进行消费;</p> </li>     <li> <p>又因为TouchEventActivity 不断的分发事件到 TouchEventFather 控件的 dispatchTouchEvent,而 TouchEventFather 控件的 dispatchTouchEvent 也不断的将获取到的事件进行消费。</p> </li>    </ul>    <h3>4、情况三</h3>    <p><img src="https://simg.open-open.com/show/9f75724b9459abcbecb9545570c34f86.png" alt="Android:View的事件分发与消费机制" width="950" height="529"></p>    <p>case3</p>    <p><strong>过程及结果分析:</strong></p>    <ul>     <li> <p>事件首先由 TouchEventActivity 的 dispatchTouchEvent 方法分发给 TouchEventFather 控件的 dispatchTouchEvent;</p> </li>     <li> <p>而该TouchEventFather 控件的 dispatchTouchEvent 返回 super.dispatchTouchEvent(ev),表示对事件进行分发并向下传递给 TouchEventFather 控件的 onInterceptTouchEvent 方法;</p> </li>     <li> <p>而该方法返回 true 表示对所获取到的事件进行拦截并将事件传递给 TouchEventFather 控件的 onTouchEvent 进行处理,TouchEventFather 控件的 onTouchEvent 返回 super.onTouchEvent(ev) 表示对事件没有做任何处理直接将事件返回给上级控件;</p> </li>     <li> <p>由于 TouchEventFather 获取的事件直接来自 TouchEventActivity,所以 TouchEventFather 控件的 onTouchEvent 会将事件以冒泡方式直接返回给 TouchEventActivity 的 onTouchEvent 进行消费;</p> </li>     <li> <p>后续的事件则会跳过 TouchEventFather 直接由 TouchEventActivity 的 onTouchEvent 消费来自 TouchEventActivity 自身分发的事件。</p> </li>    </ul>    <h3>5、情况四</h3>    <p><img src="https://simg.open-open.com/show/e8230f6453155b972cd041ca92b7c61c.png" alt="Android:View的事件分发与消费机制" width="952" height="611"></p>    <p>case4</p>    <p><strong>过程及结果分析:</strong></p>    <ul>     <li> <p>事件首先由 TouchEventActivity 的 dispatchTouchEvent 方法分发给 TouchEventFather 控件的 dispatchTouchEvent;</p> </li>     <li> <p>而该控件的 dispatchTouchEvent 返回 super.dispatchTouchEvent(ev),表示对事件进行分发并向下传递给 TouchEventFather 控件的 onInterceptTouchEvent 方法;</p> </li>     <li> <p>该方法返回 false 表示事件会被放行并传递到子控件 TouchEventChilds 的 dispatchTouchEvent 方法;</p> </li>     <li> <p>同样 TouchEventChilds 的 dispatchTouchEvent 返回 super.dispatchTouchEvent(ev),表示对事件进行分发并向下传递给 TouchEventChilds 控件的 onInterceptTouchEvent 方法;</p> </li>     <li> <p>而TouchEventChilds 的 onInterceptTouchEvent 方法返回 super.onInterceptTouchEvent(ev) ,默认会将事件传递给 TouchEventChilds 的 onTouchEvent 进行处理;</p> </li>     <li> <p>而TouchEventChilds 的 onTouchEvent 返回 super.onTouchEvent(ev) 表示对事件没有做任何处理直接将事件返回给上级控件;</p> </li>     <li> <p>由于 TouchEventChilds 获取的事件直接来自 TouchEventFather,所以 TouchEventChilds 控件的 onTouchEvent 会将事件以冒泡方式直接返回给 TouchEventFather 的 onTouchEvent 进行消费;</p> </li>     <li> <p>而 TouchEventFather 的 onTouchEvent 也返回了 super.onTouchEvent(ev),同样 TouchEventFather 的 onTouchEvent 也会将事件返回给上级控件;</p> </li>     <li> <p>而 TouchEventFather 获取的事件直接来自 TouchEventActivity,所以 TouchEventFather 控件的 onTouchEvent 会将事件以冒泡方式直接返回给 TouchEventActivity 的 onTouchEvent 进行消费;</p> </li>     <li> <p>后续的事件则会跳过 TouchEventFather 和 TouchEventChilds 直接由 TouchEventActivity 的 onTouchEvent 消费来自 TouchEventActivity 自身分发的事件。</p> </li>    </ul>    <h3>6、情况五</h3>    <p><img src="https://simg.open-open.com/show/96102e343249b23124308fcd79b1e151.png" alt="Android:View的事件分发与消费机制" width="954" height="507"></p>    <p>case5</p>    <p><strong>过程及结果分析:</strong></p>    <ul>     <li> <p>事件首先由 TouchEventActivity 的 dispatchTouchEvent 方法分发给 TouchEventFather 控件的 dispatchTouchEvent;</p> </li>     <li> <p>该控件的 dispatchTouchEvent 返回 super.dispatchTouchEvent(ev),事件会分发到 TouchEventFather 的 onInterceptTouchEvent,此方法返回 false 表示放行当先事件;</p> </li>     <li> <p>事件会被传递到子控件 TouchEventChilds 的 dispatchTouchEvent 方法,dispatchTouchEvent 返回 true 表示事件被分发到 TouchEventChilds ,并由 dispatchTouchEvent 方法消费;</p> </li>     <li> <p>后续的事件也会不断的重复上面的逻辑最终被 TouchEventChilds 的 dispatchTouchEvent 消费。</p> </li>    </ul>    <h2>四、总结与归纳</h2>    <p>好了,看完理论与代码结果,我想应该已经够直观的说明事件分发机制了。虽然在实际开发过程还是会遇到各种各样的问题,但是有了理论基础,处理起来应该不会太难。接下来我就总结一下一些比较重要的注意事项和结论。有些是书上的重要结论。</p>    <blockquote>     <p>如果在你不知道返回什么的情况下,记住如果是完全自定义View就返回true,如果是继承已有的控件或者View那就返回super;</p>     <p>正常情况下,一个事件序列只能被一个View拦截,这是肯定的,因为某个事件被拦截后,只能通过这个拦截View来处理。当然前提是正常情况下。</p>     <p>View没有onInterceptTouchEvent方法,只要有触摸事件会直接调用onTouchEvent方法。</p>     <p>如果一个View设置了OnTouchListener方法,那么会优先调用onTouch方法,这个时候还要看onTouch方法的返回值,如果为false那么继续调用onTouchEvent方法,如果为true则不调用。</p>     <p>如果onTouchEvent方法里面设置了OnClickListener之类的方法,它会在onTouchEvent方法调用之后调用其中的onClick方法。</p>     <p>一个事件一旦交给了一个View进行处理,那么它必须消费掉事件,否则剩下的事件序列将不再由其处理。</p>    </blockquote>    <p>最后,花了几乎五个小时写完了三天学的东西,很有满足感。至少觉得努力没有白费。还要感谢资料带来的帮助,本文也大量参考了其中的内容,如有侵权,联系删改。</p>    <p> </p>    <p>via: http://www.iamxiarui.com/2016/05/09/__trashed/</p>