Android Touch事件分发深入了解
本文带着大家深入学习触摸事件的分发,具体内容如下
1. 触摸动作及事件序列
(1)触摸事件的动作
触摸动作一共有三种:action_down、action_move、action_up。当用户手指接触屏幕时,便产生一个动作为action_down的触摸事件,此时若用户的手指立即离开屏幕,会产生一个动作为action_up的触摸事件;若用户手指接触屏幕后继续滑动,当滑动距离超过了系统中预定义的距离常数,则产生一个动作为action_move的触摸事件,系统中预定义的用来判断用户手指在屏幕上的滑动是否是一个action_move动作的这个距离常量叫做touchslop,可通过viewconfiguration.get(getcontext()).getscaledtouchslop()获取。
(2)事件序列
当用户的手指接触屏幕,在屏幕上滑动,又离开屏幕,这个过程会产生一系列触摸事件:action_down-->若干个action_move-->action_up。这一系列触摸事件即为一个事件序列。
2. 触摸事件的分发
(1)概述
当产生了一个触摸时间后,系统要负责把这个触摸事件给一个view(targetview)来处理,touch事件传递到targetview的过程即为touch事件的分发。
触摸事件的分发顺序:activity-->*view-->*view的子view-->. . .-->target view
触摸事件的响应顺序:targetview --> targetview的父容器 --> . . . -->*view -->activity
(2)toush事件分发的具体过程
a. activity对touch事件的分发
当用户手指接触屏幕时,便产生了一个touch事件,封装了touch事件的motionevent最先被传递给当前activity,activity的dispatchtouchevent方法负责touch事件的分发。分发touch事件的实际工作由当前activity的window完成,而window会将touch事件传递给decorview(当前用户界面*view)。activity的dispatchtouchevent方法代码如下:
public boolean dispatchtouchevent(motionevent ev) { if (ev.getaction() == motionevent.action_down) { onuserinteraction(); } if (getwindow().superdispatchtouchevent(ev)) { return true; } return ontouchevent(ev); }
根据以上代码可以知道,touch事件会交由window的superdispatchtouchevent进行分发,若这个方法返回true,意味touch事件的分发过程结束,返回false则说明经过层层分发,没有子view对这个事件进行处理,即所有子view的ontouchevent方法都返回false(即这个touch事件没有被“消耗”)。这时会调用activity的ontouchevent方法来处理这个touch事件。
在window的superdispatchtouchevent方法中,首先会把touch事件分发给decorview,因为它是当前用户界面的*view。window的superdispatchtouchevent方法如下:
public abstract boolean superdispatchtouchevent(motionevent ev);
是个抽象方法,这个方法由window的实现类phonewindow实现,phonewindow的superdispatchtouchevent方法的代码如下:
public boolean superdispatchtouchevent(motionevent ev) { return mdecor.superdispatchtouchevent(event); }
由以上代码可得,phonewindow的superdispatchtouchevent方法实际上是通过decorview的superdispatchtouchevent方法来完成自己的工作,也就是说,当前activity的window直接将这个touch事件传递给了decorview。也就是说,目前touch事件已经经过了如下的分发:activity-->window-->decorview。
b. *view对touch事件的分发
经过activity与window的分发,现在touch事件已经被传递到了decorview的dispatchtouchevent方法中。decorview本质上是一个viewgroup(更具体的说是framelayout),viewgroup的dispatchtouchevent方法所做的工作可以分为如下几个阶段,第一个阶段的主要代码如下:
//handle an initial down. if (actionmasked == motionevent.action_down) { //throw away all previous state when starting a new touch gesture. //the framework may have dropped the up or cancel event for the previous gesture due to an app switch, anr, or some other state change. cancelandcleartouchtargets(ev); resettouchstate(); }
第一阶段的主要工作有俩:一是在第6行的resettouchstate方法中完成了对flag_disallow_intercept标记的重置;二是第5行的cancelandcleartouchtargets方法会清除当前motionevent的touch target。关于flag_disallow_intercept标记和touch target,在下文会有相关说明。
第二阶段的主要工作是决定当前viewgroup是否拦截本次的touch事件,主要代码如下:
//check for interception. final boolean intercepted; if (actionmasked == motionevent.action_dowm || mfirsttouchtarget != null) { final boolean disallowintercept = (mgroupflags & flag_disallow_intercept) != 0; if (!disallowintercept) { intercepted = onintercepttouchevent(ev); ev.setaction(action); //restore action in case it was changed } else { intercepted = false; } } else { //there are no touch targets and this action is not an initial down so this view group continues to intercept touches. intercept =true; }
由以上代码我们可以知道,当一个touch事件被传递到viewgroup时,会先判断这个touch事件的动作是否是action_down,如果这个事件是action_down或者mfirsttouchtarget不为null,就会根据flag_disallow_intercept标记决定是否拦截这个touch事件。那么mfirsttouchtarget是什么呢?当touch事件被viewgroup的子view成功处理时,mfirsttouchtarget就会被赋值为成功处理touch事件的view,也就是上面提高的touch target。
总结一下上述代码的流程:在子view不干预viewgroup的拦截的情况下(上述代码中的disallowintercept为false),若当前事件为action_down或者mfirsttouchtarget不为空,则会调用viewgroup的onintercepttouchevent方法来决定最终是否拦截此事件;否则(没有targetview并且此事件不是action_down),当前viewgroup就拦截下此事件。 一旦viewgroup拦截了某次touch事件,那么mfirsttouchtarget就不会被赋值,因此当再有action_move或是action_up传递到该viewgroup时,mtouchtarget就为null,所以上述代码第3行的条件就为false,viewgroup会拦截下来。由此可得到的结论是:一旦viewgroup拦截了某次事件,则同一事件序列中的剩余事件也会它默认被拦截而不会再询问是否拦截(即不会再调用onintercepttouchevent)。
这里存在一种特殊情形,就是子view通过requestdisallowintercepttouchevent方法设置父容器的flag_disallow_intercept为true,这个标记指示是否不允许父容器拦截,为true表示不允许。这样做能够禁止父容器拦截除action_down以外的所有touch事件。之所以不能够拦截action_down事件,是因为每当action_down事件到来时,都会重置flag_disallow_intercept这个标记位为默认值(false),所以每当开始一个新touch事件序列(即到来一个action_down动作),都会通过调用onintercepttoucheven询问viewgroup是否拦截此事件。当action_down事件到来时,重置标记位的工作是在上面的第一阶段完成的。
接下来,会进入第三阶段的工作:
final boolean canceled = resetcancelnextupflag(this) || actionmasked == motionevent.action_cancel; final boolean split = (mgroupflags & flag_split_motion_events) != 0; touchtarget newtouchtarget = null; boolean alreadydispatchedtonewtouchtarget = false; if (!canceled && !intercepted) { // 不是action_cancel并且不拦截 if (actionmasked == motionevent.action_down) { // 若当前事件为action_down则去寻找这次事件新出现的touch target final int actionindex = ev.getactionindex(); // always 0 for down ... final int childrencount = mchildrencount; if (newtouchtarget == null && childrencount != 0) { // 根据触摸的坐标寻找能够接收这个事件的touch target final float x = ev.getx(actionindex); final float y = ev.gety(actionindex); final view[] children = mchildren; // 遍历所有子view for (int i = childrencount - 1; i >= 0; i--) { final int childindex = i; final view child = children[childindex]; // 寻找可接收这个事件并且touch事件坐标在其区域内的子view if (!canviewreceivepointerevents(child) || !istransformedtouchpointinview(x, y, child, null)) { continue; } newtouchtarget = gettouchtarget(child); // 找到了符合条件的子view,赋值给newtouchtarget if (newtouchtarget != null) { //child is already receiving touch within its bounds. //give it the new pointer in addition to ones it is handling. newtouchtarget.pointeridbits |= idbitstoassign; break; } resetcancelnextupflag(child); // 把action_down事件传递给子组件进行处理 if (dispatchtransformedtouchevent(ev, false, child, idbitstoassign)) { //child wants to receive touch within its bounds. mlasttouchdowntime = ev.getdowntime(); if (preorderedlist != null) { //childindex points into presorted list, find original index for (int j=0;j<childrencount;j++) { if (children[childindex]==mchildren[j]) { mlasttouchdownindex=j; break; } } } else { mlasttouchdownindex = childindex; } mlasttouchdownx = ev.getx(); mlasttouchdowny = ev.gety(); //把mfirsttouchtarget赋值为newtouchtarget,此子view成为新的touch事件的起点 newtouchtarget = addtouchtarget(child, idbitstoassign); alreadydispatchedtonewtouchtarget = true; break; } } } } }
当viewgroup不拦截本次事件,则touch事件会分发给它的子view进行处理,相关代码从第21行开始:遍历所有viewgroup的子view,寻找能够处理此touch事件的子view,若一个子view不在播放动画并且touch事件坐标位于其区域内,则该子view能够处理此touch事件,并且会把该子view赋值给newtouchtarget。
若当前遍历到的子view能够处理此touch事件,就会进入第38行的dispatchtransformedtouchevent方法,该方法实际上调用了子view的dispatchtouchevent方法。dispatchtransformedtouchevent方法中相关的代码如下:
if (child == null) { handled = super.dispatchtouchevent(event); } else { handled = child.dispatchtouchevent(event); }
若dispatchtransformedtouchevent方法传入的child参数不为null,则会调用child(即处理touch事件的子view)的dispatchtouchevent方法。若该子view的dispatchtouchevent方法返回true,则dispatchtransformedtouchevent方法也会返回true,则表示成功找到了一个处理该事件的touch target,会在第55行把newtouchtarget赋值给mfirsttouchtarget(这一赋值过程是在addtouchtarget方法内部完成的),并跳出对子view遍历的循环。若子view的dispatchtouchevent方法返回false,viewgroup就会把事件分发给下一个子view。
若遍历了所有子view后,touch事件都没被处理(该viewgroup没有子view或是所有子view的dispatchtouchevent返回false),viewgroup会自己处理touch事件,相关代码如下:
if (mfirsttouchtarget == null) { handled = dispatchtransformedtouchevent(ev, canceled, null, touchtarget.all_pointer_ids); }
由以上代码可知,viewgroup自己处理touch事件时,会调用dispatchtransformedtouchevent方法,传入的child参数为null。根据上文的分析,传入的chid为null时,会调用super.dispatchtouchevent方法,即调用view类的dispatchtouchevent方法。
c. view对touch事件的处理
view的dispatchtouchevent方法的主要代码如下:
public boolean dispatchtouchevent(motionevent event) { boolean result = false; . . . if (onfiltertoucheventforsecurity(event)) { //noinspection simplifiableifstatement 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; }
由上述代码可知,view对touch事件的处理过程如下:由于view不包含子元素,所以它只能自己处理事件。它首先会判断是否设置了ontouchlistener,若设置了,会调用ontouch方法,若ontouch方法返回true(表示该touch事件已经被消耗),则不会再调用ontouchevent方法;若ontouch方法返回false或没有设置ontouchlistener,则会调用ontouchevent方法,ontouchevent对touch事件进行具体处理的相关代码如下:
if (((viewflags & clickable) == clickable || (viewflags & long_clickable) == long_clickable)) { switch (event.getaction()) { case motionevent.action_up: boolean prepressed = (mprivateflags & pflag_prepressed) != 0; if ((mprivateflags & pflag_pressed) != 0 || prepressed) { . . . if (!mhasperformedlongpress) { //this is a tap, so remove the longpress check removelongpresscallback(); //only perform take click actions if we were in the pressed state if (!focustaken) { //use a runnable and post this rather than calling performclick directly. //this lets other visual state of the view update before click actions start. if (mperformclick == null) { mperformclck = new peformclick(); } if (!post(mperformclick)) { performclick(); } } } . . . } break; } . . . return true; }
由以上代码可知,只要view的clickable属性和long_clickable属性有一个为true(view的clickable属性和具体view有关,long_clickable属性默认为false,setoncliklistener和setonlongclicklistener会分别自动将以上俩属性设为true),那么这个view就会消耗这个touch事件,即使这个view处于disabled状态。若当前事件是action_up,还会调用performclick方法,该view若设置了onclicklistener,则performclick方法会在其内部调用onclick方法。performclick方法代码如下:
public boolean performclick() { final boolean result; final listenerinfo li = mlistenerinfo; if (li != null && li.monclicklistener != null) { playsoundeffect(soundeffectconstants.click); li.monclicklistener.onclick(this); result = true; } else { result = false; } sendaccessibilityevent(accessibilityevent.type_view_clicked); return result; }
以上是我学习android中触摸事件分发后的简单总结,很多地方叙述的还不够清晰准确
下一篇: rxjava 八:背压