源码跟踪分析View的事件分发机制

hv0377 7年前
   <p>想要写好Android的界面,解决View的滑动冲突是十分重要的,因此需要对Android的事件分发机制有一定了解和认识。之前校招面试的时候自己也被问过相关问题,加上自己最近在写一个小Demo遇到的一些玄学Bug,索性就系统的学习了一下。下面分享一下学习成果,同时也希望有人能指出我的错误和不足。</p>    <h3>写在前面的话</h3>    <ul>     <li>1.dispatchTouchEvent和onTouchEvent返回true则表示当前View要消耗点击事件,就不再向下分发,返回false则开始向上回溯;</li>     <li>2.ViewGroup是继承自View类的,只不过ViewGroup是容器它可以有子View而View没有;</li>     <li>3.同一个事件序列是指从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中所产生的一系列事件,这个事件序列以down事件开始,中间夹杂着许多个move事件,最终以up事件结束;</li>     <li>4.一个MotionEvent对象即封装了一个点击事件。</li>    </ul>    <p>先介绍点击事件分发过程中3个比较重要的方法:</p>    <pre>  <code class="language-java">public boolean dispatchTouchEvent(MotionEvent ev)</code></pre>    <p>这个方法的作用是用来分发点击事件的,如果点击事件传递到了当前 <em>View</em> ,那么该方法一点会被调用,返回结果受当前 <em>View</em> 的onTouchEvent和下级 <em>View</em> 的dispatchTouchEvent的返回值的影响,表示是否消耗当前事件。如果所有 <em>View</em> 都不消耗,那么该点击事件最后会交给 <em>Activity</em> 的onTouchEvent方法处理。</p>    <pre>  <code class="language-java">public boolean onInterceptEvent(MotionEvent ev)</code></pre>    <p>在上述方法内部调用,返回结果表示是否拦截事件。如果当前View拦截了某个事件,那么在同一个事件序列中此方法不会被再次调用。</p>    <pre>  <code class="language-java">public boolean onTouchEvent(MotionEvent ev)</code></pre>    <p>在dispatchTouchEvent中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前 View 无法再次接收到事件。</p>    <p>下面这幅图是参照 <a href="/misc/goto?guid=4959675424536518722" rel="nofollow,noindex">图解 Android 事件分发机制</a> 这篇博客画的一幅流程图:</p>    <p><img src="https://simg.open-open.com/show/1712c0e4ad26ec13b8ddec47f926a754.png"></p>    <p>事件分发流程图.png</p>    <p>从左上角开始,点击事件总是先传递给Activity,再由activity向下分发。下面就按照图中1-9的顺序从源码角度跟踪点击事件的去向。</p>    <p>1. Activity#dispatchTouchEvent -> ViewGroup#dispatchTouchEvent</p>    <p>当一个点击事件发生后首先会传递给Activity,由Activity的dispatchTouchEvent(MotionEvent ev)来接收并进行分发,首先看一下Activity的dispatchTouchEvent方法源码:</p>    <pre>  <code class="language-java">public boolean dispatchTouchEvent(MotionEvent ev) {      if (ev.getAction() == MotionEvent.ACTION_DOWN) {          onUserInteraction();      }      if (getWindow().superDispatchTouchEvent(ev)) {          return true;      }      return onTouchEvent(ev);  }</code></pre>    <p>onUserInteraction()方法是一个空方法,第一个if可以不看。接着看第二个if,即如果getWindow().superDispatchTouchEvent(ev)返回true,则Activity的dispatchTouchEvent返回true。</p>    <p>这时候点击事件就从Activity传递到getWindow()返回的Window了,Android里面的Window类是一个抽象类,Window类的唯一一个实现类是PhoneWindow,接着看看PhoneWindow的superDispatchTouchEvent方法:</p>    <pre>  <code class="language-java">public boolean superDispatchTouchEvent(MotionEvent event) {      return mDecor.superDispatchTouchEvent(event);  }</code></pre>    <p>先来看一下mDecor这个对象,这是一个DecorView类对象,这个DecorView是整个应用窗口的根View:</p>    <pre>  <code class="language-java">private final class DecorView extends FrameLayout implements RootViewSurfaceTaker</code></pre>    <p>看一下它的superDispatchTouchEvent方法:</p>    <pre>  <code class="language-java">public boolean superDispatchTouchEvent(MotionEvent event){          return super.dispatchTouchEvent(event);  }</code></pre>    <p>因为DecorView是继承自FrameLayout的,即间接继承自GroupView,所以上述super.dispatchTouchEvent方法调用的即是GroupView的dispatchTouchEvent,自此点击事件就传递到了GroupView了。</p>    <p>2.ViewGroup#dispatchTouchEvent -> Activity#onTouchEvent</p>    <p>回过头看Activity的dispatchTouchEvent方法,如果getWindow().superDispatchTouchEvent(ev)返回了false即ViewGroup的dispatchTouchEvent返回false,则return onTouchEvent(ev)这时候点击事件就交给Activity的onTouchEvent去处理了。</p>    <p>3. ViewGroup#dispatchTouchEvent -> ViewGroup#onInterceptTouchEvent</p>    <p>来看一下ViewGroup的dispatchTouchEvent 中是否拦截相关逻辑:</p>    <pre>  <code class="language-java">public boolean dispatchTouchEvent(MotionEvent ev) {      ...      // Check for interception.      final boolean intercepted;      if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {              final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;              if (!disallowIntercept) {                      intercepted = onInterceptTouchEvent(ev);              ev.setAction(action);           } else {                      intercepted = false;              }      } else {              intercepted = true;      }      ...  }</code></pre>    <p>先看第一个if的判断条件,事件类型为ACTION_DOWN或者mFirstTouchTarget != null。这个mFirstTouchTarget 在当前ViewGroup的子View决定拦截处理事件时会被赋值,这时候mFirstTouchTarget != null成立。当ACTION_MOVE和ACTION_UP过来,actionMasked == MotionEvent.ACTION_DOWN不成立,这时候如果mFirstTouchTarget == null说明没有子View要处理这次点击事件,则intercepted = true, 即当前ViewGroup要拦截这次事件(因为没有子View处理,而且事件能传递到当前ViewGroup,所以当前ViewGroup拦截)。如果mFirstTouchTarget != null则点击事件再继续往下传递。</p>    <p>如果第一个if条件成立,进入到if内部。mGroupFlags和FLAG_DISALLOW_INTERCEPT是2个标志位。mGroupFlags这个标志位我不知道是用来做什么的,也希望知道的能给我说明下。FLAG_DISALLOW_INTERCEPT是通过requestDisallowInterceptTouchEvent来设置,一般是通过在子View调用该方法来设置该标志,该标志一旦设置,ViewGroup将无法拦截除了ACTION_DOWN之外的其他点击事件,因为ACTION_DOWN事件会重置FLAG_DISALLOW_INTERCEPT标志位。</p>    <p>第二个if的条件,如果disallowIntercept为false即允许拦截,intercepted = onInterceptTouchEvent(ev)这句执行,此时点击事件就传递到了onInterceptTouchEvent。而onInterceptTouchEvent是默认返回false的,即不拦截。</p>    <pre>  <code class="language-java">public boolean onInterceptTouchEvent(MotionEvent ev) {      return false;  }</code></pre>    <p>4. ViewGroup#onInterceptTouchEvent -> ViewGroup#onTouchEvent</p>    <p>看一下ViewGroup点击事件向下分发源码</p>    <pre>  <code class="language-java">public boolean dispatchTouchEvent(MotionEvent ev) {      ...      final View[] children = mChildren;        //遍历所有子View      for (int i = childrenCount - 1; i >= 0; i--) {          final int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i;          final View child = (preorderedList == null) ? children[childIndex] : preorderedList.get(childIndex);               //判断子View是否能接收点击事件          if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) {              continue;          }            //判断当前view是否已存在mFirstTouchTarget队列中,当多点触控时,多次点击同一个view只保存一个          newTouchTarget = getTouchTarget(child);              if (newTouchTarget != null) {              newTouchTarget.pointerIdBits |= idBitsToAssign;                      break;              }                resetCancelNextUpFlag(child);            //事件已传递到子View          if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)){                      mLastTouchDownTime = ev.getDownTime();                      if (preorderedList != null) {                              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);                      alreadyDispatchedToNewTouchTarget = true;                      break;              }          }      ...  }</code></pre>    <p>上面的dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)里面调用的就是就是子View的dispatchTouchEvent,该方法中有如下一段代码:</p>    <pre>  <code class="language-java">if (child == null) {      handled = super.dispatchTouchEvent(event);  } else {      handled = child.dispatchTouchEvent(event);  }</code></pre>    <p>这里有2种情况。第一种情况,如果child == null,则handled = super.dispatchTouchEvent(event),此时会调用ViewGroup的父类View的dispatchTouchEvent,先来看一下该类中的部分方法:</p>    <pre>  <code class="language-java">public boolean dispatchTouchEvent(MotionEvent event) {      boolean result = false ;      ...      if (onFilterTouchEventForSecurity(event)) {          ListenerInfo li = mListenerInfo;          if (li != null && li.mOnTouchListener != null                   && (mViewFlags & ENABLED_MASK) == ENABLED                    && li.mOnTouchListener.onTouch(this, event)) {               result = true;          }          if (!result && onTouchEvent(event)) {              result = true;          }      }      ...      return  result ;  }</code></pre>    <p>ListenerInfo 在OnTouchListener 被设置的时候会初始化,onFilterTouchEventForSecurity做的是一些标志位判断,所以直接来看这句</p>    <pre>  <code class="language-java">if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event))</code></pre>    <p>当OnTouchListener 被设置后不为null并且OnTouchListener的onTouch返回了true,则result = true。那么下一个if判断!result为false,根据&&运算符特性则onTouchEvent不会被调用。即只要设置了OnTouchListener则onTouchEvent就不会被调用,可见OnTouchListener的优先级高于onTouchEvent。到这里点击事件就传递给onTouchEvent了,这里的onTouchEvent是GroupView的onTouchEvent,先记住这点。</p>    <p>5.ViewGroup#onTouchEvent -> Activity#onTouchEvent</p>    <p>如果onTouchEvent方法return false,将间接导致ViewGroup的dispatchTouchEvent返回false,则发生前面 <strong>2</strong> 中 的情况,即点击事件传递到Activity的onTouchEvent。</p>    <p>6.ViewGroup#onInterceptTouchEvent -> View#dispatchTouchEvent</p>    <p>4中提到的2种情况,当child == null不成立时,此时会调用child.dispatchTouchEvent方法,这样点击事件就传递到了子View的dispatchTouchEvent方法了。</p>    <p>7.View#dispatchTouchEvent -> View#onTouchEvent</p>    <p>参考4中View的dispatchTouchEvent 方法的部分代码,如果View设置了OnTouchListener,onTouchEvent就不会调用;反之,onTouchEvent会被调用,此时点击事件就传递到View的onTouchEvent了。</p>    <p>8.View#diapatchTouchEvent -> ViewGroup#onTouchEvent</p>    <p>如果View的diapatchTouchEvent返回false说明当前View不准备消费点击事件,那么前面提到的mFirstTouchTarget就为null ,因为前面说过只有子View明确表示要消费点击事件,它才会被赋值。如果mFirstTouchTarget == null,ViewGroup就会自己处理点击事件:</p>    <pre>  <code class="language-java">if (mFirstTouchTarget == null) {      handled = dispatchTransformedTouchEvent(ev, canceled, null,  TouchTarget.ALL_POINTER_IDS);  }</code></pre>    <p>dispatchTransformedTouchEvent这个方法在前面提到过,只不过当时第三个参数是child不是null,这样前面child == null就直接成立,所以handled = super.dispatchTouchEvent(event)会被调用,从而调用onTouchEvent。</p>    <p>9.View#onTouchEvent -> ViewGroup#onTouchEvent</p>    <p>当点击事件传递到View的onTouchEvent,但是该方法返回了false,间接导致View的dispatchTouchEvent返回了false,从而出现8当中的情况,从而ViewGroup的onTouchEvent被调用。</p>    <h2>总结:</h2>    <ul>     <li>1.点击事件从Activity中的dispatchTouchEvent开始向下分发,向上回溯是决定getWindow().superDispatchTouchEvent(ev)的返回值;</li>     <li>2.onInterceptTouchEvent方法不是每一次都会被调用,只有当传递过来的为ACTION_DOWN事件或者mFirstTouchTarget != null;</li>     <li>3.View的dispatchTouchEvent方法和onTouchEvent方法返回了false,那么它的父容器View(如果有的话)的onTouchEvent方法一定被调用,因为此时child为null(参考8和9);</li>     <li>4.子View的onTouchEvent要在父View的onTouchEvent之前被调用;</li>     <li>5.onTouchListener的优先级高于onTouchEvent,如果前者被赋值,后者将被屏蔽;</li>     <li>6.可以通过调用requestDisallowInterceptTouchEvent设置FLAG_DISALLOW_INTERCEPT这个标志位来拦截除了ACTION_DOWN以外的其他点击事件,因为来ACTION_DOWN到来时会重置标志位。</li>    </ul>    <p>参考链接:</p>    <p><a href="/misc/goto?guid=4959675424536518722" rel="nofollow,noindex">图解 Android 事件分发机制</a></p>    <p>参考书籍:</p>    <p>《Android开发艺术探索》</p>    <p> </p>    <p>来自:http://www.jianshu.com/p/5597d6295130</p>    <p> </p>