Android的事件分发机制以及滑动冲突的解决方案
android的事件分发机制以及滑动冲突的解决
android事件分发机制详解:史上最全面、最易懂
个人感觉该文较全面,总结也很好,但略显冗长,且基于android5.0以前的版本。本文中简化了核心原理,再补充了一些必要知识点,并使用最新的源码重新做了分析。
基础知识
事件分发的对象
montionevent
当用户触摸屏幕时(view 或 viewgroup派生的控件),将产生点击事件(touch事件)。而touch事件的相关细节(发生触摸的位置、时间等)被封装成motionevent对象。事件分发其实就是对montionevent对象的分发。
montionevent的类型
motionevent.action_down:按下view(所有事件的开始)
motionevent.action_up:抬起view(与down对应)
motionevent.action_move:滑动view
motionevent.action_cancel:结束事件(非人为原因)
关于action_cancel
当控件收到前驱事件之后,后面的事件如果被父控件拦截,那么当前控件就会收到一个cancel事件,并会把此cancel事件传递给它的子控件。
前驱事件:一个从down一直到up的所有事件组合称为完整的手势,中间的任意一次事件对于下一个事件而言就是它的前驱事件。
场景一:scrollview中有一个button,手指触下button区域进行滑动的过程中,button会收到scrollview传给它的cancel事件。这个过程是这样的,当scrollview收到down事件时,无法确定这是个点击事件还是滑动事件,并且发现该次点击正好落在button的区域内,于是它把down事件传给button,然后后续到来的事件是一个move事件,这时scrollview能确定本次是一个滑动事件,于是它决定要拦截move事件,此时button就会收到一个cancel事件了,并且这个事件序列中的后续事件都不再会交给buutton处理。在整个过程中,scrollview收到的事件序列是:down → 多个move → up,而button收到的事件序列是:down → cancel。
场景二:scrollview包含viewpager时,对viewpager做左右滑动,滑到一页的一半时改为上下滑动,此时viewpager就会收到cancel事件,而viewpager在cancel中就可以做一些恢复状态的处理,回到先前那一页,而不是停在中间。这个过程中,scrollview收到的事件序列是:down → 多个move → up,而viewpager收到的事件序列是:down → 多个move → cancel。
事件序列
从手指接触屏幕 至 手指离开屏幕,这个过程产生的一系列事件。
一般情况下,事件列都是以down事件开始、up事件结束:
点击控件后松开,事件序列为:down → up; 点击控件滑动一会再松开,事件序列为:down → move → … → move → up; 点击控件滑动到控件外再松开,事件序列为:down → move → … → move → cancel;
事件在哪些对象之间进行传递?
activity、window(viewgroup)、view
android ui界面的组成
从上而下看,android ui界面包含以下:
activity:一个ui界面就是一个activity
window:抽象类,其唯一实现是android.policy.phonewindow,可以通过getwindow()获得。
decorview:phonewindow的一个内部类,父view是framelayout,是界面的*view,setcontentview所设置的view就是作为decorview的子view,可以通过getwindow().getdecorview()获得。而要获取activity所设置的view,则可以通过((viewgroup)getwindow().getdecorview().findviewbyid(android.r.id.content)).getchildat(0)。
viewgroup:view的子类,可以包含其他view
view:不能包含其他view
事件分发的顺序
当一个点击事件产生后,它的传递过程遵循如下顺序:activity -> window(viewgroup) -> view,即事件总是先传递给activity,activity会交给其内部的window进行派发,window会将事件传递给decor view,decor view一般就是当前界面的底层容器(即setcontentview所设置的view的父容器),通过activiyt.getwindow().getdecorview()可以获得。而decorview的父类是framelayout,所以decorview的间接父类就是viewgroup了。
从以上分析可知,要想充分理解android的事件分发机制,本质上是要理解:
activity对点击事件的分发机制 viewgroup对点击事件的分发机制 view对点击事件的分发机制
事件分发机制
事件分发三兄弟
事件的分发过程由3个很重要的方法来共同完成:
public boolean dispatchtouchevent(motionevent ev)
事件传递的开始,用来分发点击事件。返回结果受当前view的ontouchevent和下级view的dispatchtouchevent影响,表示是否消耗了事件。
true:消耗了,事件不再向下传递; false(default):未消耗,事件继续向下传递;
public boolean onintercepttouchevent(motionevent ev)
在上述方法内部调用,用来判断是否拦截事件,如果当前view拦截了某个事件,那么在同一个事件序列当中,此方法不会被再次调用,返回结果表示是否拦截当前事件。
true:拦截,事件由当前viewgroup处理,不再向下传递; false(default):不拦截,事件继续向下传递;
public boolean ontouchevent(motionevent ev)
在dispatchtouchevent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗 ,则在同一个事件序列中,当前view无法再次收到事件。
true:消耗,事件不再向上抛(可点击的控件必定返回true) false(default):不消耗,事件向上抛给父view(viewgroup)处理(不可点击的控件必定返回false)
考虑一种情况,如果一个view的ontouchevent返回false,那么它的父容器的ontouchevent将会被调用,以此类推。如果所有的元素都不处理这个事件,那么这个事件将会最终被抛给activity处理,即activity的ontouchevent将会被调用。
上述三个方法的关系可以用如下伪代码表示:
// 步骤1:调用dispatchtouchevent() public boolean dispatchtouchevent(motionevent ev) { boolean consume = false; //代表 是否会消费事件 // 步骤2:判断是否拦截事件 if (onintercepttouchevent(ev)) { // a. 若拦截,则将该事件交给当前view进行处理 // 即调用ontouchevent ()方法去处理点击事件 consume = ontouchevent (ev) ; } else { // b. 若不拦截,则将该事件传递到下层 // 即 下层元素的dispatchtouchevent()就会被调用,重复上述过程 // 直到点击事件被最终处理为止 consume = child.dispatchtouchevent (ev) ; } // 步骤3:最终返回通知 该事件是否被消费(接收 & 处理) return consume; }
activity的事件分发
当一个点击事件发生时,事件最先传到activity的dispatchtouchevent()进行事件分发。
/** * activity.dispatchtouchevent() */ public boolean dispatchtouchevent(motionevent ev) { // 一般事件列开始都是down事件 = 按下事件,故此处基本是true if (ev.getaction() == motionevent.action_down) { onuserinteraction(); // ->>分析1 } // ->>分析2 if (getwindow().superdispatchtouchevent(ev)) { return true; // 若getwindow().superdispatchtouchevent(ev)返回true // 则activity.dispatchtouchevent()就返回true,则方法结束。即 :该点击事件停止往下传递 & 事件传递过程结束 // 否则:继续往下调用activity.ontouchevent } return ontouchevent(ev); }
先整体上分析一下上面的代码。首先事件开始交给activity所附属的window进行分发,如果返回true,整个事件循环就结束了,返回false意味着事件没人处理,所有view的ontouchevent都返回了false,那么activity的ontouchevent就会被调用。
/** * 分析1:onuserinteraction() * 注: * a. 该方法为空方法 * b. 用户交互事件,包括触屏事件、按键事件(home,back,menu键)、以及轨迹球事件等都会触发此方法 * 以下是官方文档的说明: * called whenever a key, touch, or trackball event is dispatched to the * activity. implement this method if you wish to know that the user has * interacted with the device in some way while your activity is running. * * all calls to your activity's {@link #onuserleavehint} callback will * be accompanied by calls to {@link #onuserinteraction}. * * 无论是按键事件、触摸事件或者轨迹球事件被分发给activity,都会调用activity#onuserinteraction()。 * 如果你想知道用户通过某种方式和你正在运行的activity进行了交互,可以重写activity#onuserinteraction()。 * 所有调用activity#onuserleavehint()的回调都会首先回调activity#onuserinteraction()。 */ public void onuserinteraction() { }
和onuserinteraction()对应的还有一个onuserleavehint()方法。当你想知道是否是因为用户操作导致你的activity将要进入后台,则可以重写该方法。
/** * called as part of the activity lifecycle when an activity is about to go into the background as the result of user choice. * for example, when the user presses the home key, {@link #onuserleavehint} will be called, but * when an incoming phone call causes the in-call activity to be automatically brought to the foreground, *{@link #onuserleavehint} will not be called on the activity being interrupted. * * 当用户的操作使一个activity准备进入后台时,此方法会像activity的生命周期的一部分被调用。例如,当用户按下home键, * activity#onuserleavehint()将会被回调。但是当来电导致来电activity自动占据前台,activity#onuserleavehint()将不会被回调。 */ public void onuserleavehint() { }
/** * 分析2:getwindow().superdispatchtouchevent(ev) * 说明: * a. getwindow() = 获取window类的对象 * b. window类是抽象类,其唯一实现类 = phonewindow类;即此处的window类对象 = phonewindow类对象 * c. window类的superdispatchtouchevent() = 1个抽象方法,由子类phonewindow类实现 */ @override public boolean superdispatchtouchevent(motionevent event) { return mdecor.superdispatchtouchevent(event); // mdecor = 顶层view(decorview)的实例对象 // ->> 分析3 }
/** * 分析3:mdecor.superdispatchtouchevent(event) * 定义:属于顶层view(decorview) * 说明: * a. decorview类是phonewindow类的一个内部类 * b. decorview继承自framelayout,是所有界面的父类 * c. framelayout是viewgroup的子类,故decorview的间接父类 = viewgroup */ public boolean superdispatchtouchevent(motionevent event) { return super.dispatchtouchevent(event); // 调用父类的方法 = viewgroup的dispatchtouchevent() // 即 将事件传递到viewgroup去处理,详细请看viewgroup的事件分发机制 }
viewgroup的事件分发
从activity的事件分发可知,viewgroup的事件分发也是从dispatchtouchevent开始。这个方法比较长,我们分段来说明。先看下面这一段,很显然它描述的是当前viewgroup是否拦截点击事件的逻辑。
// check for interception. final boolean intercepted; // 条件1:当前事件是down事件时 // 条件2:mfirsttouchtarget != null,当事件被子元素成功处理时,mfirsttouchtarget 就会被赋值并指向子元素,也就是说viewgroup不拦截事件并将事件交由子元素处理时该条件就会成立,反过来,一旦事件由当前viewgroup拦截时,该条件就不成立。于是,当move和up到来时,整个条件为false,将导致viewgroup的onintercepttouchevent不会被执行。 if (actionmasked == motionevent.action_down || mfirsttouchtarget != null) { final boolean disallowintercept = (mgroupflags & flag_disallow_intercept) != 0; // disallowintercept很重要,相当于一个开关,可以关闭viewgroup对事件的拦截,但仅限于down事件以外的其他事件。 if (!disallowintercept) { // ->>分析1 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. intercepted = true; }
/** * 分析1:viewgroup.onintercepttouchevent() * 作用:是否拦截事件 * 说明: * a. 返回true = 拦截,即事件停止往下传递(需手动设置,即复写onintercepttouchevent(),从而让其返回true) * b. 返回false = 不拦截(默认) */ public boolean onintercepttouchevent(motionevent ev) { return false; }
这里还有一个flag_disallow_intercept标记位,很显然它也会影响onintercepttouchevent的执行,这个标记位是通过requestdisallowintercepttouchevent方法来设置的,一般用于子view中。该标记位一旦设置后,viewgroup将无法拦截除了action_down以外的其他事件。为什么说是除了action_down以外的其他事件呢?这是因为viewgroup在分发事件时,如果是action_down事件,就会重置flag_disallow_intercept这个标记位,将导致子view中设置的这个标记位无效。换言之,当遇到action_down事件时,viewgroup总是会调用自己的onintercepttouchevent方法来询问自己是否要拦截事件。这可以从源码中看出来。
// 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(); }
上面的代码是在检查是否要拦截之前的一段,很显然它是处理一些初始化的工作,由于任何一个事件序列都是开始于down事件,也就是说,一旦遇到down事件,就可以认为是一个新的事件序列的开始,所以此时要首先做一系列重置状态的动作,而flag_disallow_intercept就是在resettouchstate方法中被重置的,因此子view调用requestdisallowintercepttouchevent并不能影响viewgroup对down事件的处理。
接下来看viewgroup不拦截事件时,事件会向下分发给子view进行处理,源码如下:
final view[] children = mchildren; for (int i = childrencount - 1; i >= 0; i--) { final int childindex = getandverifypreorderedindex( childrencount, i, customorder); final view child = getandverifypreorderedview( preorderedlist, children, childindex); // if there is a view that has accessibility focus we want it // to get the event first and if not handled we will perform a // normal dispatch. we may do a double iteration but this is // safer given the timeframe. if (childwithaccessibilityfocus != null) { if (childwithaccessibilityfocus != child) { continue; } childwithaccessibilityfocus = null; i = childrencount - 1; } // 条件1:canviewreceivepointerevents,当前child是否能够接收到点击事件 // 条件2:istransformedtouchpointinview,点击事件的坐标是否落在当前child的区域内 // 因此,当前child无法接收到事件或者点击事件不在当前child的区域内,就跳过,继续遍历下一个child if (!canviewreceivepointerevents(child) || !istransformedtouchpointinview(x, y, child, null)) { ev.settargetaccessibilityfocus(false); continue; } newtouchtarget = gettouchtarget(child); if (newtouchtarget != null) { // child is already receiving touch within its bounds. // give it the new pointer in addition to the ones it is handling. newtouchtarget.pointeridbits |= idbitstoassign; break; } resetcancelnextupflag(child); // 如果当前child是被点击的child,代码就会执行到这里,调用dispatchtransformedtouchevent方法进行事件的分发 // ->>分析2 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(); newtouchtarget = addtouchtarget(child, idbitstoassign); alreadydispatchedtonewtouchtarget = true; break; } // the accessibility focus didn't handle the event, so clear // the flag and do a normal dispatch to all children. ev.settargetaccessibilityfocus(false); }
/** * 分析2:viewgroup.dispatchtransformedtouchevent() * 作用:实现viewgroup到view的事件分发 * 若viewgroup不拦截事件,那么参数中的child就不是null,因此会执行child.dispatchtouchevent(event),实现了一轮分发 * 若viewgroup拦截事件,那么参数中的child是null,因此会执行super.dispatchtouchevent(event),意思就是把当前的viewgroup当做view来对待,执行view的dispatchtouchevent,由它自己来处理这个事件。 */ private boolean dispatchtransformedtouchevent(motionevent event, boolean cancel, view child, int desiredpointeridbits) { final boolean handled; ... if (child == null) { handled = super.dispatchtouchevent(event); } else { handled = child.dispatchtouchevent(event); } ... }
view的事件分发
从viewgroup的事件分发可以看出,view的事件分发也是从dispatchtouchevent开始。
/** * view.dispatchtouchevent() */ public boolean dispatchtouchevent(motionevent event) { if (montouchlistener != null && (mviewflags & enabled_mask) == enabled && montouchlistener.ontouch(this, event)) { return true; } return ontouchevent(event); } // 说明:只有以下3个条件都为真,dispatchtouchevent()才返回true;否则执行ontouchevent() // 1. montouchlistener != null // 2. (mviewflags & enabled_mask) == enabled // 3. montouchlistener.ontouch(this, event) // 下面对这3个条件逐个分析
/** * 条件1:montouchlistener != null * 说明:montouchlistener变量在view.setontouchlistener()方法里赋值 */ public void setontouchlistener(ontouchlistener l) { montouchlistener = l; // 即只要我们给控件注册了touch事件,montouchlistener就一定被赋值(不为空) }
/** * 条件2:(mviewflags & enabled_mask) == enabled * 说明: * a. 该条件是判断当前点击的控件是否enable * b. 由于很多view默认enable,故该条件恒定为true */
/** * 条件3:montouchlistener.ontouch(this, event) * 说明:即 回调控件注册touch事件时的ontouch();需手动复写设置,具体如下(以按钮button为例) */ button.setontouchlistener(new ontouchlistener() { @override public boolean ontouch(view v, motionevent event) { return false; } }); // 若在ontouch()返回true,就会让上述三个条件全部成立,从而使得view.dispatchtouchevent()直接返回true,事件分发结束 // 若在ontouch()返回false,就会使得上述三个条件不全部成立,从而使得view.dispatchtouchevent()中跳出if,执行ontouchevent(event)
接下来,我们看看ontouchevent的源码。android 5.0后 view.ontouchevent()源码发生了变化(更加复杂),但原理相同;为了更容易理解,这里采用android 5.0前的版本。
/** * view.ontouchevent() */ public boolean ontouchevent(motionevent event) { final int viewflags = mviewflags; // 若该控件被disable了,返回值取决于是否可点击或长按,是则一定返回true,消耗事件,否则一定返回false,事件被抛给上级的viewgroup处理 // 结论(9) if ((viewflags & enabled_mask) == disabled) { return (((viewflags & clickable) == clickable || (viewflags & long_clickable) == long_clickable)); } if (mtouchdelegate != null) { return true; } } // 若该控件可点击,则进入switch判断中 if (((viewflags & clickable) == clickable || (viewflags & long_clickable) == long_clickable)) { switch (event.getaction()) { // a. 若当前的事件 = 抬起view(主要分析) // 结论(10) case motionevent.action_up: boolean prepressed = (mprivateflags & prepressed) != 0; ...// 经过种种判断,此处省略 // 执行performclick() ->>分析1 performclick(); break; // b. 若当前的事件 = 按下view case motionevent.action_down: if (mpendingcheckfortap == null) { mpendingcheckfortap = new checkfortap(); } mprivateflags |= prepressed; mhasperformedlongpress = false; postdelayed(mpendingcheckfortap, viewconfiguration.gettaptimeout()); break; // c. 若当前的事件 = 结束事件(非人为原因) case motionevent.action_cancel: mprivateflags &= ~pressed; refreshdrawablestate(); removetapcallback(); break; // d. 若当前的事件 = 滑动view case motionevent.action_move: final int x = (int) event.getx(); final int y = (int) event.gety(); int slop = mtouchslop; if ((x < 0 - slop) || (x >= getwidth() + slop) || (y < 0 - slop) || (y >= getheight() + slop)) { // outside button removetapcallback(); if ((mprivateflags & pressed) != 0) { // remove any future long press/tap checks removelongpresscallback(); // need to switch from pressed to not pressed mprivateflags &= ~pressed; refreshdrawablestate(); } } break; } // switch语句结束 // 若该控件可点击,就一定返回true,消耗事件,不再往上抛 return true; } // switch外层的if条件句结束 // 若该控件不可点击,就一定返回false,表示事件未消耗,抛给上级的viewgroup处理 return false; }
/** * 分析1:performclick() */ public boolean performclick() { if (monclicklistener != null) { playsoundeffect(soundeffectconstants.click); monclicklistener.onclick(this); return true; // 只要我们通过setonclicklistener()为控件view注册1个点击事件 // 那么就会给monclicklistener变量赋值(即不为空) // 则会往下回调onclick() & performclick()返回true } return false; }
重要结论:
view的事件监听器响应先后顺序:ontouchlistener > onclicklistener,即ontouch先于onclick响应,同时ontouch的返回值决定了onclick是否被响应。ontouch返回true,则事件传递终止,onclick不响应;ontouch返回false,则事件继续传递,onclick响应。
事件分发的工作流程总结
关于事件分发的一些结论
某个view一旦决定拦截某个事件,那么会将这一事件序列内的其他事件都直接交给它来处理,而且不会再调用它的onintercepttouchevent去询问它是否要拦截了。因此,正常情况下,一个事件序列只能被一个view拦截且消耗。但通过特殊手段可以达到让不同view同时处理一个事件序列的目的,比如一个view将本该自己处理的事件通过ontouchevent强行传递给其他view处理。 onintercepttouchevent不是每次都会被调用的,如果我们想提前处理所有的点击事件,要选择dispatchtouchevent方法,只有这个方法能确保每次都会调用,当然前提是点击事件能传递到当前的viewgroup。 某个view一旦开始处理事件,如果它不消耗action_down事件(ontouchevent返回false),那么同一事件序列中的其他事件都不会再交给它来处理,并且事件将重新交给它的父元素去处理,即父元素的ontouchevent会被调用。意思就是说事件一旦交给一个view来处理,它就必须消耗掉,否则同一个事件序列中剩下的事件就不再交给它来处理了。 某个view一旦开始处理事件,如果它消耗了action_down事件,却不消耗后续的其他事件,那么这个点击事件会消失,此时父元素的ontouchevent并不会被调用,并且当前view可以持续收到后续事件,最终这些消失的点击事件会传递给activity处理。 viewgroup默认不拦截任何事件。源码中其onintercepttouchevent默认返回false。 view没有onintercepttouchevent方法,一旦有点击事件传递给它,那么它的ontouchevent方法就会被调用。 view的ontouchevent默认都会消耗事件(返回true),除非它是不可点击的(clickable和longclickable同时为false)。view的longclickable默认都是false,而clickable则要分情况,比如button默认是true,textview默认是false。 view的setonclicklistener会自动将clickable设为true,setonlongclicklistener会自动将longclickable设为true。 view的enable属性不影响ontouchevent的默认返回值。哪
怕一个view是disable状态的,只要它的clickable或者longclickable有一个为true,那么它的ontouchevent就返回true,意味着事件被该view处理消耗了,不再继续传递。 onclick会发生的前提是当前view是可点击的,并且它收到了down和up事件。 事件传递过程是由外向内的,即事件总是先传递给父元素,然后再由父元素分发给子view,通过requestdisallowintercepttouchevent方法可以在子元素中干预父元素的事件分发过程,但是action_down事件除外。
view的滑动冲突
滑动冲突是app开发中很常见的一个问题,也是view体系中一个深入的话题。只要界面中内外两层同时可以滑动,就会产生滑动冲突。那么如何解决滑动冲突呢?这既是一件困难的事又是一件简单的事,说困难是因为许多开发者面对滑动冲突都会显得束手无策,说简单是因为滑动冲突的解决是有固定套路的,只要知道了这个套路问题就迎刃而解了。
前面讲view的事件分发机制,其实也是为本节内容做准备。
常见的滑动冲突场景
场景1 - 外部滑动和内部滑动方向不一致
这种情况比较常见,主要是将viewpager和fragment配合使用所组成的页面滑动效果,很多主流应用都会使用这个效果,比如微信。在这种效果中,可以通过左右滑动来切换页面,而每个页面内部往往又是一个listview。这里有些同学可能要反驳了:我用viewpager和listview嵌套时,从来没遇到过滑动冲突!其实这种情况下滑动冲突是确实存在的,只不过viewpager内部已经帮我们处理了滑动冲突,因此我们在使用viewpager时无需关注这个问题,如果我们不是用viewpager而是scrollview等,那你就会真真切切的感受到滑动冲突带来的烦恼了,这时候就必须手动处理滑动冲突了,否则就会发现内外两层只有一层能够滑动。
场景2 - 外部滑动和内部滑动方向一致
这种情况稍微复杂一些,当内外两层都在同一个方向可以滑动的时候,显然存在逻辑问题。因为系统无法知道用户到底是想让哪一层滑动,要么只有一层能滑动,要么就是内外两层都滑动地很卡顿。比如某个界面同时集成了googlemap和slidemenu,就会遇到此种冲突。
场景3 - 以上两种情况的嵌套
这种场景的滑动冲突看起来就更复杂了。比如外部有一个slidemenu效果,然后内部有一个viewpager,viewpager的每一个页面中又是一个listview。虽然这种情况看起来更复杂,但它是几个单一的滑动冲突的叠加,因此只需要分别处理内层和中层、中层和外层之间的滑动冲突即可。
解决滑动冲突的步骤
概括来讲,解决滑动冲突只需要两步:
制定合适的滑动策略; 套用“固定套路”,实现策略;
从本质上说,以上3种场景的复杂度其实是相同的,因为它们的区别仅仅是滑动策略的不同,至于解决冲突的方法,它们几个是通用的。
制定滑动策略
对于场景1,它的处理规则是:当用户左右滑动时,让外部view拦截点击事件,当用户上下滑动时,让内部view拦截点击事件。那么如何判断是水平滑动还是竖直滑动呢?方法有很多,比如可以依据滑动路径和水平方向形成的夹角,也可以依据水平方向和竖直方向上的距离差来判断,某些特殊的时候还可以依据水平和竖直方向的速度差来做判断。
方便起见,我们可以依据水平和垂直方向的距离差来判断滑动方向,比如竖直方向的距离差大,就当做竖直滑动处理,否则判断为水平滑动。当然这个规则有点粗糙,有些用户就是喜欢挑战开发者的智商,不竖不横,偏偏要斜着滑,这时单纯依据距离差判断就显得有点不够友好了。此时需要重新制定滑动策略:比如使用夹角做判断依据,当水平夹角小于30度时认定为水平滑动,大于60度时认定为竖直滑动,30-60度之间的丢弃不处理。说到底滑动策略的制定完全是由开发者来决定的,你也可以根据具体的业务需求来完善你的滑动策略。
对于场景2,它比较特殊,无法根据滑动的角度、距离差、速度差来判断,但这种时候一般都能在业务上找到突破点,比如业务上有规定:当处于某种状态时需要外部view响应用户的滑动,而处于另一种状态时需要内部view来响应用户的滑动。针对同时集成googlemap和slidemenu的情况,从业务上可能不一定能找到突破点,但仍然可以制定相应的滑动策略,比如将手机屏幕边缘(占屏幕1/10处)的滑动进行拦截,交由slidemenu处理,而屏幕其他范围内的滑动则交由map处理。
对于场景3,它的滑动规则就更复杂了,和场景2一样,也无法单纯的根据滑动的角度、距离差、速度差进行判断,但同样还是可以从业务上找到突破点。
其实无论哪个场景,处理原则都是从业务上找突破点,制定合适的滑动策略。
解决滑动冲突的“套路”
上面说过,3种场景的滑动冲突解决,唯一的区别只在滑动策略的不同,有了滑动策略后,具体又该如何解决滑动冲突呢?我们以较为简单的场景1为例来得出通用的解决方案,至于场景2和3只需要修改有关滑动规则的逻辑即可。场景1的距离差其实就是滑动规则,而针对滑动冲突的解决,这里给出两种方式:外部拦截法和内部拦截法。
外部拦截法
所谓外部拦截法,是指点击事件都先经过父容器的拦截处理,如果父容器需要就拦截,否则就不拦截,这种方法比较符合点击事件的分发机制。外部拦截法需要重写父容器的onintercepttouchevent方法,在内部做相应的拦截即可,伪代码如下所示:
public boolean onintercepttouchevent(motionevent event) { boolean intercepted = false; int x = (int) event.getx(); int y = (int) event.gety(); switch (event.getaction()) { case motionevent.action_down: { // 必须false,否则后续的move和up将全部交由该父容器处理,不再传递给子view intercepted = false; break; } case motionevent.action_move: { if (父容器需要当前点击事件) { intercepted = true; } else { intercepted = false; } break; } case motionevent.action_up: { // 必须false,对于viewgroup来说up本身没有太多意义,但却会影响子view的onclick是否被触发 intercepted = false; break; } default: break; } mlastxintercept = x; mlastyintercept = y; return intercepted; }
上述代码就是外部拦截法的典型逻辑,针对不同的滑动冲突,只要修改“父容器需要当前点击事件”这个条件即可,其他均不需也不能修改。这里再做一些说明,首先是action_down事件,父容器必须返回false,即不拦截,这是因为一旦父容器决定拦截action_down事件了,那么后续的action_move和action_up事件都会直接交由它来处理(系统不再会调用onintercepttouchevent进行询问),没法再传递给子元素了。其次是action_move事件,这个事件可以根据需要决定是否拦截;最后是action_up事件,这里必须返回false,对于作为父容器的viewgroup来说up本身没有太多意义,但却会影响子view的onclick是否被触发,如果这里返回true,子元素就无法收到这个up事件,这时候子元素的onclick就无法触发(结论10:onclick会发生的前提是当前view是可点击的,并且它收到了down和up事件)。
内部拦截法
内部拦截法是指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交给父容器进行处理,这种方法和android的事件分发机制不一致,需要配合requestdisallowintercepttouchevent方法才能正常工作,较外部拦截法稍显复杂。内部拦截法需要重写子元素的dispatchtouchevent方法,伪代码如下:
public boolean dispatchtouchevent(motionevent event) { int x = (int) event.getx(); int y = (int) event.gety(); switch (event.getaction()) { case motionevent.action_down: { // 关闭父元素的拦截功能 parent.requestdisallowintercepttouchevent(true); break; } case motionevent.action_move: { int deltax = x - mlastx; int deltay = y - mlasty; if (父容器需要此类点击事件) { // 激活父元素原本的拦截功能 parent.requestdisallowintercepttouchevent(false); break; } break; } case motionevent.action_up: { break; } default: break; } mlastx = x; mlasty = y; return super.dispatchtouchevent(event); }
上述代码是内部拦截法的典型代码,针对不同的滑动冲突,只要修改“父容器需要此类点击事件”这个条件即可,其他均不需也不能修改。除了子元素要做处理外 ,父元素也要默认拦截除了action_down以外的其他事件,这样当子元素调用parent.requestdisallowintercepttouchevent(false)方法时,父元素才能继续拦截所需要的事件。
为什么父元素不能拦截action_down事件呢?那是因为action_down事件并不受flag_disallow_intercept标记位的控制,一旦父元素拦截了action_down事件,那么所有的事件都无法传递到子元素中去,内部拦截法就失效了。父元素所做的修改如下所示:
public boolean onintercepttouchevent(motionevent event) { int action = event.getaction(); if (action == motionevent.action_down) { // action_down事件不能拦截 return false; } else { // 其余事件默认拦截 return true; } }
上一篇: 宋朝太后刘娥为何没有选择称帝?有何顾虑?
下一篇: 超详细注释之OpenCV旋转图像任意角度
推荐阅读
-
站在初学者的角度看图理解Android事件分发机制流程
-
Android的事件分发机制以及滑动冲突的解决方案
-
Android 深入探究自定义view之事件的分发机制与处理详解
-
Android必备知识点之View及View的事件分发机制
-
Android——简单易懂说原理之View的点击事件分发机制
-
Android进阶必备:滑动冲突解决与事件分发机制(附视频讲解)这篇看完还不懂请寄刀片
-
Android从源码分析ScrollView自动滑动的焦点问题以及解决方案
-
Android的事件分发机制以及滑动冲突的解决方案
-
Android开发知识(九):Android事件处理机制:事件分发、传递、拦截、处理机制的原理分析(下)
-
Android事件处理机制:事件分发、传递、拦截、处理机制的原理分析(上)