设计ui时,亲爱的交互设计师们总会有一些天马行空的想法,大多数情况下原生的控件已不能支持这些“看似简单”的交互逻辑,需要继承<code>listview</code>、<code>viewpager</code>、<code>scrollview</code>甚至直接继承view来自定义一些特性来支撑。在处理触摸事件时,无可避免的需要重写<code>onintercepttouchevent</code>与<code>ontouchevent</code>这两个方法。本文将从源码的角度,从这两个棘手的函数为切入点,对触摸事件在<code>view</code>中的传递逻辑进行梳理。
本文中只简单的考虑单指触摸事件。一次触摸事件通常有一系列<code>touchevent</code>组成,这一系列<code>touchevent</code>通常由一个<code>action_down</code>开始,并且由一个<code>action_up/action_cancel</code>结束。这一系列<code>touchevent</code>都会自上而下传入视图结构,上层<code>view</code>根据自身需求决定是由自身来处理该事件,或者将其传入下一层视图处理。通常而言<code>viewgroup.onintercepttouchevent</code>决定了父<code>view</code>是否拦截该触摸事件,而<code>view.ontouchevent</code>中则实现了其自身如何处理该触摸事件。
<code>viewgroup.onintercepttouchevent</code>
api 24对该方法的官方说明:
实现该方法以拦截所有的屏幕触摸事件,从而使你能够监控触摸事件分发给子view的过程并且随时拦截。 使用该方法时需小心谨慎,因为该方法与<code>view.ontouchevent</code>的交互相当复杂,并且要正确的实现这两个方法。<code>touchevent</code>将会根据以下顺序被接收: 你将在这里接收到<code>action_down</code> <code>action_down</code>要么由一个子view来处理,要么由你自身的<code>ontouchevent</code>来处理。后者意味着你应该实现<code>ontouchevent</code>并返回<code>true</code>,你才能收到后续的<code>touchevent</code>(而不是由你的父<code>view</code>来处理);并且,当你在<code>ontouchevent</code>中返回<code>true</code>时,你将不会在<code>onintercepttouchevent</code>中接收到后续的<code>touchevent</code>,但是仍然会正常的传递到<code>ontouchevent</code>中 如果你在此方法中返回<code>false</code>,那么本次触摸事件中所有后续的<code>touchevent</code>都会先传递到这里,然后传递到目标<code>view</code>的<code>ontouchevent</code>中 如果你在此方法中返回<code>true</code>,本次触摸事件中所有后续的<code>touchevent</code>都不会再传递到此方法。原本的目标<code>view</code>将会接收到一个同样的<code>touchevent</code>(但是action为<code>action_cancel</code>),之后的<code>touchevent</code>会传递到你自身的<code>touchevent</code>并且不再出现在此处
<code>onintercepttouchevent</code>定义在<code>viewgroup</code>中,intercept一词为拦截的意思。简而言之,该方法的用意为决定是否拦截该<code>touchevent</code>,如果该方法返回<code>true</code>表示拦截此<code>touchevent</code>,否则会向下传递到子<code>view</code>中。在<code>viewgroup</code>中该方法直接返回<code>true</code>,继承于<code>viewgroup</code>的控件根据自身需求自己实现。
<code>view.ontouchevent</code>
<code>viewgroup.dispatchtouchevent(motionevent ev)</code>方法是触摸事件在视图结构中传递逻辑的主导者。该方法最初定义在<code>view</code>中(会调用<code>ontouchevent</code>并返回是否消费),在<code>viewgroup</code>中被重写。<code>touchevent</code>传入<code>viewgroup</code>后<code>dispatchtouchevent</code>首先被调用以负责触摸事件在自身与子<code>view</code>之间的分发处理逻辑,并且通过返回值通知父<code>view</code>是否消费了<code>touchevent</code>。<code>onintercepttouchevent</code>与<code>ontouchevent</code>都由其直接或间接被调用,多层视图结构通过一层层向下调用<code>dispatchtouchevent</code>寻找触摸事件的“主人”。本节主要对以注释的形式对该方法源码进行分析以初步了解touchevent在视图结构中的分发过程。
通过上面对源码的分析,本节主要详细梳理一次常规的单指触摸事件(以<code>action_down</code>开始,并以<code>action_up</code>结束,中间全为<code>action_move</code>)在一个<code>viewgroup</code>中的分发处理过程。
<code>action_down</code>
作为触摸事件的开始,初始化,进入下一步
如果有子<code>view</code>调用<code>requestdisallowintercepttouchevent</code>,则<code>intercepted</code>为<code>false</code>,进入第3步;否则进入下一步
调用<code>onintercepttouchevent</code>,返回值赋予<code>intercepted</code>,进入下一步
如果<code>intercepted</code>为<code>false</code>,进入下一步;否则进入第5步;
遍历所有子view找到在触摸位置的<code>view</code>,将坐标转换后调用其<code>dispatchtouchevent</code>,将第一个返回<code>true</code>的子<code>view</code>作为触摸事件的<code>target</code>,进入下一步
如果<code>target</code>为空,进入下一步;否则进入第7步
没有<code>target</code>,则将<code>touchevent</code>交由自身的<code>ontouchevent</code>处理,返回值赋予<code>handled</code>,进入第8步
有<code>target</code>,则将<code>handled</code>置为<code>true</code>,以通知父视图“我将处理这此触摸事件”,进入下一步
返回<code>handled</code>
<code>action_move/action_up/action_cancel</code>
如果<code>target</code>为空,<code>intercepted</code>置为<code>true</code>,进入第4步;否则进入下一步
如果有子<code>view</code>调用<code>requestdisallowintercepttouchevent</code>,则<code>intercepted</code>为<code>false</code>,进入第4步;否则进入下一步
如果<code>target</code>为空,进入下一步;否则进入第6步
没有<code>target</code>,调用自身<code>ontouchevent</code>,返回值赋予<code>handled</code>
有<code>target</code>。如果<code>intercepted</code>为<code>false</code>,进入下一步;否则进入第8步
调用<code>target</code>的<code>dispatchtouchevent</code>,返回值赋予<code>handled</code>,进入第9步
调用<code>target</code>的<code>dispatchtouchevent</code>并传入一个<code>action_cancel</code>,<code>handled</code>置为<code>true</code>,清空<code>target</code>,进入下一步
流程示意图
概括来讲,<code>action_down</code>的分发过程对于整个触摸事件来讲是相当重要的,而<code>dispatchtouchevent</code>就是为<code>action_down</code>寻找“主人”的一个过程,如果找到了则返回<code>true</code>。<code>viewgroup.onintercepttouchevent</code>在分发<code>action_down</code>时,如果<code>intercepted = false</code>,便会向下传递寻找有没有子视图能做这次事件的“主人”。如果<code>intercepted = true</code>,或者在子视图中没有找到“主人”,那么就将其本身视为一个普通的<code>view</code>来调用<code>ontouchevent</code>来处理。如果有子视图或者其自身能handled,那么就向上返回<code>true</code>表示“爸爸,我找到它的主人了”。
<code>action_move</code>进入<code>disallowintercept</code>时,如果之前在子视图中找到了“主人”就直接将其传递至目标,否则就将其本身视为一个普通的<code>view</code>来调用<code>ontouchevent</code>来处理。如果<code>intercepted = true</code>则给之前的“主人”传递一个<code>action_cancel</code>,同时清空目标,那么之后进入的<code>touchevent</code>将会被自身来处理。
至此为止,本文主要在横向地分析<code>touchevent</code>在<code>viewgroup</code>中的分发过程,而在开发过程中,通常我们更多需要关注的是<code>touchevent</code>在视图层次中纵向的传递过程。基于以上对于<code>touchevent</code>分发过程的分析,可以很清晰地整理出纵向传递的逻辑(本节的分析过程基于一个四层的视图结构,上方三层为<code>viewgroup</code>,最底层为普通的<code>view</code>):
情景一
情景二
情景三
情景四
情景五
情景六
本节以两个具体样例来协助理解上述纵向传递过程。
样例一
考虑这样的一个三层视图结构:从上到下依次为<code>scrollview</code>,<code>viewpager</code>,<code>listview</code>。如果不做任何处理,那么手指在屏幕上下滑动将会是以下的一个处理过程:
样例二
本例基于样例一的模型,但是对<code>scrollview</code>进行处理,使其永远在<code>onintercepttouchevent</code>中返回<code>false</code>。
考虑这样的一个三层的视图(忽略了一些无关紧要的层次):
<code>scrollview</code>中含有一个<code>textview</code>与<code>viewpager</code>,其中<code>viewpager</code>的高度与<code>scrollview</code>的高度一致,而在<code>viewpager</code>的某一页为一个同等大小的<code>listview</code>,通过在<code>onmeasure</code>中作一些必要的处理从而将整个视图完整的显示之后,会发现<code>listview</code>完全无法滚动。而这个视图结构应该挺常见,交互的需求应该更常见:手指向上滑动时,先滚动<code>scrollview</code>,滚动到底后再滚动<code>listview</code>;手指向下滑动时,先滚动<code>listview</code>,滚动到底后再滚动<code>scrollview</code>。
首先对于这个需求,相信大家会首先想到api 21推出的<code>nestedscroll</code>。在学习了<code>android</code>触摸事件传递之后,决定从<code>onintercepttouchevent</code>与<code>ontouchevent</code>这两个方法做做手脚,来实现这一需求。我的思路分为两步:
对<code>onintercepttouchevent</code>做手脚。手指向上滑动时,当<code>scrollview</code>滑动到边界时,<code>onintercepttouchevent</code>返回<code>false</code>,将事件交由<code>listview</code>处理,使<code>listview</code>能够滑动;手指向下滑动时,如果<code>listview</code>能够滑动,就在<code>onintercepttouchevent</code>中返回<code>false</code>,让<code>listview</code>优先滑动。这样下来,虽然还无法在一次手指滑动过程中切换<code>scrollview</code>与<code>listview</code>的滑动,但是已经能够用两次手指滑动来切换了。
对<code>ontouchevent</code>做手脚。手指向上滑动时,当<code>scrollview</code>滑动到边界时,首先分发一个<code>action_cancel</code>表示此次触摸事件已结束,同时马上再分发一个<code>action_down</code>表示新一次触摸事件开始,这时通过上一步<code>onintercepttouchevent</code>中做的手脚就将滑动切换到了<code>listview</code>,为了达到目的不择手段地强行将一次触摸事件拆分为两个;手指向下滑动时,当<code>listview</code>滑动到边界时,通知最顶层的<code>scrollview</code>分发两个新事件来进行强拆。
自定义scrollview
自定义listview