Android中TouchEvent触摸事件机制
tyycxw
9年前
<p>当我们的手指在Android屏幕上点击或滑动时,就会触发触摸事件TouchEvent。在App中ViewGroup和View存在多级嵌套,在最外层的是Activity,最内层的View,介于Activity与View之间的是一些ViewGroup。本文为了简化讨论,我们假设一个Activity中只有一个ViewGroup,这个ViewGroup中只有一个View。当我们用手指触摸到View的UI时,就会产生触摸事件TouchEvent,总的过程如下图所示:</p> <p><img alt="这里写图片描述" src="https://simg.open-open.com/show/992cec8b6612002d00868f685b61f013.png"></p> <p>首先是最外层的Activity接收到该事件,触发Activity的dispatchTouchEvent的执行,在该方法中Activity又会调用内部ViewGroup的dispatchTouchEvent方法的执行,在ViewGroup的dispatchTouchEvent方法中又会调用最内层的View的dispatchTouchEvent方法的执行,在View的dispatchTouchEvent方法中可能会执行View的onTouchEvent方法,然后ViewGroup也有可能执行ViewGroup的onTouchEvent方法,然后Activity也有可能执行Activity的onTouchEvent方法的执行。</p> <p>上图是精简过的主要流程图,总共是两条主线:</p> <ol> <li> <p>第一条主线是,从Activity -> ViewGroup -> View,从外向内依次调用dispatchTouchEvent方法,Android会依次把MotionEvent参数传递给该方法。dispatchTouchEvent的作用是传递触摸事件,<strong>该主线体现了将触摸事件从外向内逐级传递派发的过程</strong>,dispatchTouchEvent是每次传递触摸事件的入口。</p> </li> <li> <p>第二条主线是,从View -> ViewGroup -> Activity,从内向外依次调用onTouchEvent方法,Android会依次把MotionEvent参数传递给该方法。onTouchEvent的作用是处理触摸事件,<strong>该主线体现了将触摸事件从内向外逐级处理的过程</strong>。</p> </li> </ol> <p>dispatchTouchEvent和onTouchEvent都接收一个MotionEvent类型的参数,MotionEvent封装了触摸事件的数据信息,包括触摸事件的类型以及坐标位置等,详见博文<a href="/misc/goto?guid=4959675989672561649">《Android中的MotionEvent》</a>。dispatchTouchEvent和onTouchEvent都有一个boolean类型的返回值,如果返回true,表示当前对象已经对触摸事件进行了处理;如果返回false,表示当前对象没有对触摸事件进行处理。</p> <p>下面分别对Activity、ViewGroup、View的事件派发、处理的过程详细说明。</p> <h2>Activity</h2> <ul> <li> <p>dispatchTouchEvent<br> 所有在UI上的触摸操作生成的触摸事件都首先会触发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>上述方法的关键是,Activity会首先通过getWindow()方法获取当前的window对象,然后调用window的superDispatchTouchEvent方法,实际上,getWindow()返回的是一个PhoneWindow类型的实例,这样就会调用PhoneWindow的<a href="/misc/goto?guid=4959675989749244826">superDispatchTouchEvent方法</a>,其源码如下所示:</p> <pre> <code class="language-java">@Override public boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event); }</code></pre> <p>mDecor是PhoneWindow中一个DecorView类型的变量,DecorView代表了当前Window最顶级的View,可以看做是根View。由上代码看出,后面会执行DecorView的<a href="/misc/goto?guid=4959675989842830465">superDispatchTouchEvent方法</a>,其源码如下所示:</p> <pre> <code class="language-java">public boolean superDispatchTouchEvent(MotionEvent event) { return super.dispatchTouchEvent(event); }</code></pre> <p>实际上DectorView继承自FrameLayout,所以DectorView间接继承自ViewGroup,所以会DectorView执行其父类ViewGroup对应的dispatchTouchEvent方法。在该方法中,DectorView会找到其触摸的子节点,实际上其子节点也是一个ViewGroup,然后再执行该ViewGroup的dispatchTouchEvent方法,这样就实现了将触摸事件参数MotionEvent从Activity中传入到DecorView的子ViewGroup中了。我们会在后面探讨ViewGroup中的dispatchTouchEvent方法中的执行逻辑,此处就不再过多介绍了。</p> <p>以上介绍了借助superDispatchTouchEvent和dispatchTouchEvent方法将触摸事件从Activity到ViewGroup中的传递过程,这两个方法均返回一个boolean类型的参数,如果返回true,表示触摸事件被处理了,反之表示触摸事件没有被处理。我们再看一下上面Activity中dispatchTouchEvent的源码,就会发现如果PhoneWindow的superDispatchTouchEvent返回了true,那么Activity的dispatchTouchEvent方法也就直接返回了true,表明触摸事件被Window给处理了,所以就不会执行后面Activity的 onTouchEvent方法。只有Window没处理触摸事件的情况下,Activity才会调用onTouchEvent方法去处理事件。</p> </li> <li> <p>onTouchEvent<br> onTouchEvent的<a href="/misc/goto?guid=4959675989934941728">源码</a>如下所示:</p> <pre> <code class="language-java">public boolean onTouchEvent(MotionEvent event) { if (mWindow.shouldCloseOnTouch(this, event)) { finish(); return true; } return false; }</code></pre> <p>只有当触摸事件没有被任何的View或ViewGroup处理过的时候,Activity才会执行自己的onTouchEvent去处理触摸事件。一种典型的情形就是,当前触摸点在Window范围之外,这样Window里面所有的View都不会接收更不会处理该触摸事件,这时候我们可以重写该方法实现一些自己的逻辑处理这种情形。如果我们处理了,就返回true,否则返回false。其默认实现基本一直返回false。</p> </li> </ul> <h2>ViewGroup</h2> <ul> <li> <p>dispatchTouchEvent<br> 当Activity接收到触摸事件之后,会通过DectorView调用ViewGroup的dispatchTouchEvent方法,由于该方法的源码太长,此处就不贴源码了,<a href="/misc/goto?guid=4959675990023474932">点此查看其源码</a>。此处主要说一下该方法中的主要逻辑。dispatchTouchEvent方法是ViewGroup对触摸事件进行处理的入口。</p> <p>ViewGroup中定义了一个TouchTarget类型的成员变量<a href="/misc/goto?guid=4959675990116733955">mFirstTouchTarget</a>,用于保存当前ViewGroup中处理了触摸事件的子View。</p> <p>首先,dispatchTouchEvent方法会调用其自身的onInterceptTouchEvent方法,onInterceptTouchEvent是用来拦截ViewGroup将触摸事件传递给其子View的,如果该方法返回true,就表示ViewGroup应该拦截触摸事件;<a href="/misc/goto?guid=4959675990195456256">如果返回false</a>,表示ViewGroup不应该拦截触摸事件,应该将触摸事件传递给子View。在dispathTouchEvent方法中还定义了一个boolean类型的handled变量,用于保存dispathTouchEvent方法的返回值,如果是true就表示触摸事件被当前的ViewGroup处理了,反之则表示没被处理。</p> <p>然后,只有当onInterceptTouchEvent返回了false,<a href="/misc/goto?guid=4959675990286472875">ViewGroup才会依次遍历其子View</a>,其会通过调用<a href="/misc/goto?guid=4959675990370529776">isTransformedTouchPointInView方法</a>判断MotionEvent所携带的触摸事件的坐标是否落在子View的范围内,如果触摸事件的坐标恰好落在了该子View范围内,说明我们触摸了当前ViewGroup内的该子View,这样ViewGroup就会把触摸事件的坐标以及该子View传递给<a href="/misc/goto?guid=4959675990463893944">dispatchTransformedTouchEvent方法</a>,在该方法内会调用子View的dispatchTouchEvent方法,其返回值表示自View是否处理了触摸事件,如果dispatchTransformedTouchEvent返回true,表示子View处理了触摸事件,这样ViewGroup会通过调用<a href="/misc/goto?guid=4959675990553156078">addTouchTarget方法</a>将mFirstTouchTarget绑定该子View,并且变量<a href="/misc/goto?guid=4959675990629548084">alreadyDispatchedToNewTouchTarget</a>也会设置为true,表示已经有子View处理了触摸事件。一旦有子View处理了触摸事件,ViewGroup就会通过break跳出for循环,不再对其他子View进行遍历。</p> <p>在经过了对子View的for循环之后,如果没有任何的子View处理了触摸事件,那么mFirstTouchTarget就还是null,此时ViewGroup就会将null作为child参数传入<a href="/misc/goto?guid=4959675990721166775">dispatchTransformedTouchEvent方法</a>中,该方法会调用super.dispatchTouchEvent方法,由于ViewGroup继承自View,以此处就相当于执行了View类中的dispatchTouchEvent方法,这样就很有可能执行ViewGroup从View中继承来的onTouchEvent方法。dispatchTransformedTouchEvent的返回值会作为局部变量handled的值。关于View类中的dispatchTouchEvent方法会在下面详细说明。</p> <p>在经过了对子View的for循环之后,如果发现某个子View对触摸事件进行了处理,那么alreadyDispatchedToNewTouchTarget就是true,<a href="/misc/goto?guid=4959675990803000222">从而会将局部变量handled设置为true</a>,即表示只要有子View处理了触摸事件,就表示当前的ViewGroup也处理了触摸事件,并且这种情况下ViewGroup不会调用从View中继承来的dispatchTouchEvent方法,从而不会触发ViewGroup的onTouchEvent方法的执行。</p> </li> <li> <p>onInterceptTouchEvent<br> 之前提到过onInterceptTouchEvent用于拦截ViewGroup向子View传递触摸事件,ViewGroup中的默认实现一直返回false,即表示不拦截。我们可以重写该方法以实现我们自己的触摸事件拦截逻辑。</p> </li> <li> <p>dispatchTransformedTouchEvent<br> <a href="/misc/goto?guid=4959675990882471520">点此查看源码</a>,其主要的逻辑代码如下所示:</p> <pre> <code class="language-java">private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { final boolean handled; final MotionEvent transformedEvent; ...... // Perform any necessary transformations and dispatch. if (child == null) { handled = super.dispatchTouchEvent(transformedEvent); } else { final float offsetX = mScrollX - child.mLeft; final float offsetY = mScrollY - child.mTop; transformedEvent.offsetLocation(offsetX, offsetY); if (! child.hasIdentityMatrix()) { transformedEvent.transform(child.getInverseMatrix()); } handled = child.dispatchTouchEvent(transformedEvent); } // Done. transformedEvent.recycle(); return handled; }</code></pre> <p>该方法的主要目的是将MotionEvent中的x、y的坐标转换成所传入的child变量所指定的的View的坐标系中的坐标,transformedEvent表示了已经完成了指定坐标系转换的MotionEvent。如果传入的child参数是null,表示传入的是当前的ViewGroup,此时就将直接调用<a href="/misc/goto?guid=4959675990960037387">super.dispatchTouchEvent(transformedEvent)</a>,这样就让ViewGroup调用了父类View中的dispatchTouchEvent方法;如果传入的child参数不是null,表示传入的当前ViewGroup的一个子View,那么就会调用<a href="/misc/goto?guid=4959675991065740236">child.dispatchTouchEvent(transformedEvent)</a>,从而将触摸事件从ViewGroup传递到子View中去。我们会在下面介绍View的dispatchTouchEvent的实现逻辑。</p> </li> <li> <p>onTouchEvent<br> ViewGroup的onTouchEvent继承自View的onTouchEvent方法,ViewGroup并没有重写,我们在下面会介绍View的onTouchEvent方法的实现逻辑。</p> </li> </ul> <h2>View</h2> <ul> <li> <p>dispatchTouchEvent<br> <a href="/misc/goto?guid=4959675991149653819">点此查看源码</a>,其源码的主要逻辑如下所示:</p> <pre> <code class="language-java">public boolean dispatchTouchEvent(MotionEvent event) { ...... boolean result = false; ...... if (onFilterTouchEventForSecurity(event)) { //noinspection SimplifiableIfStatement ListenerInfo li = mListenerInfo; //如果设置了OnTouchListener,那么会在此处执行OnTouchListener的onTouch方法 if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { //如果OnTouchListener的onTouch方法返回true,就表示触摸事件被处理了,result就会设置为true result = true; } //如果触摸事件没有被OnTouchListener处理,那么就会执行View的onTouchEvent方法 if (!result && onTouchEvent(event)) { //如果onTouchEvent返回了true,就表示触摸事件被View处理了,result就被设置为了true result = true; } } ...... return result; }</code></pre> <p>dispatchTouchEvent是View处理触摸事件的入口。在该方法中,View首先会查看其有没有设置过OnTouchListener,如果设置过就调用<a href="/misc/goto?guid=4959675991225275994">OnTouchListener的onTouch方法</a>,如果其返回了true,就表明触摸事件被处理了,result就会设置为true。如果触摸事件没有被OnTouchListener处理,那么就会执行<a href="/misc/goto?guid=4959675991315858747">View的onTouchEvent方法</a>,如果onTouchEvent返回了true,就表示触摸事件被View处理了,result就被设置为了true。</p> <p>由上可以看出,在dispatchTouchEvent方法中是先执行OnTouchListener的onTouch方法,一旦其返回true,就不会调用View自身的onTouchEvent方法了,只有OnTouchListener没有处理触摸事件才会在后面执行View的onTouchEvent方法。</p> </li> <li> <p>onTouchEvent<br> <a href="/misc/goto?guid=4959675991391562268">点此查看源码</a>,View.onTouchEvent()方法中,如果View注册了<a href="/misc/goto?guid=4959675991475773547">CLICK或LONG_CLICK等事件监听器</a>,那么就会让注册的事件监听器处理触摸事件,这样<a href="/misc/goto?guid=4959675991558854916">onTouchEvent就返回true</a>。会根据ACTION的不同,执行不同的处理,比如如果是ACTION_UP,会执行<a href="/misc/goto?guid=4959675991633099019">performClick()方法</a>,该方法会触发<a href="/misc/goto?guid=4959675991713507530">OnClickListener.onClick()</a>的执行。<br> 如果View没有注册任何的CLICK或LONG_CLICK等的事件监听器,那么<a href="/misc/goto?guid=4959675991795079868">onTouchEvent就返回false</a>,表示onTouchEvent没有对传入的触摸事件MotionEvent做任何处理。</p> </li> </ul> <h2>总结</h2> <p>我们通过对上面Activity、ViewGroup、View各个层级对触摸事件的处理过程可以发现,Android中每个层级对触摸事件的处理都是从dispatchTouchEvent方法开始的,首先先调用下一层级的dispatchTouchEvent方法,将触摸事件传递给下一层级,如果下一层级对触摸事件进行了处理,就可认为本层级也对触摸事件进行了处理,那么本层级就不会对触摸事件仅需做其他特殊处理了;如果下一层级没有对触摸事件进行处理,即下一层级的dispatchTouchEvent方法返回false,那么才会调用本层级的onTouchEvent方法对触摸事件进行处理。</p> <p>我的更多博文可参见<a href="/misc/goto?guid=4959651691429659270">《我的Android博文整理汇总》</a>,希望本文对大家理解Android中的触摸事件机制有所帮助!</p> <p> </p> <p>来自:http://blog.csdn.net/iispring/article/details/50364126</p> <p> </p>