源码跟踪分析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>