android事件分发机制的实现原理
android中的事件处理,以及解决滑动冲突问题都离不开事件分发机制,android中的事件流,即motionevent都会经历一个从分发,拦截到处理的一个过程。即dispatchtouchevent(),oninterceptevent()到ontouchevent()的一个过程,在dispatchtouchevent()负责了事件的分发过程,在dispatchtouchevent()中会调用oninterceptevent()与ontouchevent(),如果oninterceptevent()返回true,那么会调用到当前view的ontouchevent()方法,如果不拦截,事件就会下发到子view的dispatchtouchevent()中进行同样的操作。本文将带领大家从源码角度来分析android是如何进行事件分发的。
android中的事件分发流程最先从activity的dispatchtouchevent()开始:
public boolean dispatchtouchevent(motionevent ev) { if (ev.getaction() == motionevent.action_down) { onuserinteraction(); } if (getwidow().superdispatchtouchevent(ev)) { return true; } return ontouchevent(ev); }
这里调用了getwindow().superdispatchtouchevent(ev),这里可以看出activity将motionevent传寄给了window。而window是一个抽象类,superdispatchtouchevent()也是一个抽象方法,这里用到的是window的子类phonewindow。
@override public boolean superdispatchtouchevent(motionevent event) { return mdecor.superdispatchtouchevent(event); }
从这里可以看出,event事件被传到了decorview,也就是我们的顶层view.我们继续跟踪:
public boolean superdispatchtouchevent(motionevent event) { return super.dispatchtouchevent(event); }
这里调用到了父类的dispatchtouchevent()方法,而decorview是继承自framelayout,framelayout继承了viewgroup,所以这里会调用到viewgroup的dispatchtouchevent()方法。
所以整个事件流从activity开始,传递到window,最后再到我们的view(viewgroup也是继承自view)中,而view才是我们整个事件处理的核心阶段。
我们来看一下viewgroup的dispatchtouchevent()中的实现:
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(); }
这是dispatchtouchevent()开始时截取的一段代码,我们来看一下,首先,当我们手指按下view时,会调用到resettouchstate()方法,在resettouchstate()中:
private void resettouchstate() { cleartouchtargets(); resetcancelnextupflag(this); mgroupflags &= ~flag_disallow_intercept; mnestedscrollaxes = scroll_axis_none; }
我们继续跟踪cleartouchtargets()方法:
private void cleartouchtargets() { touchtarget target = mfirsttouchtarget; if (target != null) { do { touchtarget next = target.next; target.recycle(); target = next; } while (target != null); mfirsttouchtarget = null; } }
在cleartouchtargets()方法中,我们最终将mfirsttouchtarget赋值为null,我们继续回到dispatchtouchevent()中,接着执行了下段代码:
// 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); // 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; }
当view被按下或mfirsttouchtarget != null 的时候,从前面可以知道,当每次view被按下时,也就是重新开始一次事件流的处理时,mfirsttouchtarget都会被设置成null,一会我们看mfirsttouchtarget是什么时候被赋值的。
从disallowintercept属性我们大概能猜到是用来判断是否需要坐拦截处理,而我们知道可以通过调用父view的requestdisallowintercepttouchevent(true)可以让我们的父view不能对事件进行拦截,我们先来看看requestdisallowintercepttouchevent()方法中的实现:
@override public void requestdisallowintercepttouchevent(boolean disallowintercept) { if (disallowintercept == ((mgroupflags & flag_disallow_intercept) != 0)) { // we're already in this state, assume our ancestors are too return; } if (disallowintercept) { mgroupflags |= flag_disallow_intercept; } else { mgroupflags &= ~flag_disallow_intercept; } // pass it up to our parent if (mparent != null) { mparent.requestdisallowintercepttouchevent(disallowintercept); } }
这里也是通过设置标志位做判断处理,所以这里是通过改变mgroupflags标志,然后在dispatchtouchevent()刚发中变更disallowintercept的值判断是否拦截,当为true时,即需要拦截,这个时候便会跳过onintercepttouchevent()拦截判断,并标记为不拦截,即intercepted = false,我们继续看viewgroup的onintercepttouchevent()处理:
public boolean onintercepttouchevent(motionevent ev) { if (ev.isfromsource(inputdevice.source_mouse) && ev.getaction() == motionevent.action_down && ev.isbuttonpressed(motionevent.button_primary) && isonscrollbarthumb(ev.getx(), ev.gety())) { return true; } return false; }
即默认情况下,只有在action_down时,viewgroup才会表现为拦截。
我们继续往下看:
final int childrencount = mchildrencount; if (newtouchtarget == null && childrencount != 0) { final float x = ev.getx(actionindex); final float y = ev.gety(actionindex); // find a child that can receive the event. // scan children from front to back. final arraylist<view> preorderedlist = buildtouchdispatchchildlist(); final boolean customorder = preorderedlist == null && ischildrendrawingorderenabled(); 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; } 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); 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); } if (preorderedlist != null) preorderedlist.clear(); }
这段代码首先会通过一个循环去遍历所有的子view,最终会调用到dispatchtransformedtouchevent()方法,我们继续看dispatchtransformedtouchevent()的实现:
private boolean dispatchtransformedtouchevent(motionevent event, boolean cancel, view child, int desiredpointeridbits) { final boolean handled; // canceling motions is a special case. we don't need to perform any transformations // or filtering. the important part is the action, not the contents. final int oldaction = event.getaction(); if (cancel || oldaction == motionevent.action_cancel) { event.setaction(motionevent.action_cancel); if (child == null) { handled = super.dispatchtouchevent(event); } else { handled = child.dispatchtouchevent(event); } event.setaction(oldaction); return handled; } // calculate the number of pointers to deliver. final int oldpointeridbits = event.getpointeridbits(); final int newpointeridbits = oldpointeridbits & desiredpointeridbits; // if for some reason we ended up in an inconsistent state where it looks like we // might produce a motion event with no pointers in it, then drop the event. if (newpointeridbits == 0) { return false; } // if the number of pointers is the same and we don't need to perform any fancy // irreversible transformations, then we can reuse the motion event for this // dispatch as long as we are careful to revert any changes we make. // otherwise we need to make a copy. final motionevent transformedevent; if (newpointeridbits == oldpointeridbits) { if (child == null || child.hasidentitymatrix()) { if (child == null) { handled = super.dispatchtouchevent(event); } else { final float offsetx = mscrollx - child.mleft; final float offsety = mscrolly - child.mtop; event.offsetlocation(offsetx, offsety); handled = child.dispatchtouchevent(event); event.offsetlocation(-offsetx, -offsety); } return handled; } transformedevent = motionevent.obtain(event); } else { transformedevent = event.split(newpointeridbits); } // 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; }
这段代码就比较明显了,如果child不为null,始终会调用到child.dispatchtouchevent();否则调用super.dispatchtouchevent();
如果child不为null时,事件就会向下传递,如果子view处理了事件,即dispatchtransformedtouchevent()即返回true。继续向下执行到addtouchtarget()方法,我们继续看addtouchtarget()方法的执行结果:
private touchtarget addtouchtarget(@nonnull view child, int pointeridbits) { final touchtarget target = touchtarget.obtain(child, pointeridbits); target.next = mfirsttouchtarget; mfirsttouchtarget = target; return target; }
这个时候我们发现mfirsttouchtarget又出现了,这时候会给mfirsttouchtarget重新赋值,即mfirsttouchtarget不为null。也就是说,如果事件被当前view或子view消费了,那么在接下来的action_move或action_up事件中,mfirsttouchtarget就不为null。但如果我们继承了该viewgroup,并在onintercepttouchevent()的action_move中拦截了事件,那么后续事件将不会下发,将由该viewgroup直接处理,从下面代码我们可以得到:
// dispatch to touch targets, excluding the new touch target if we already // dispatched to it. cancel touch targets if necessary. touchtarget predecessor = null; touchtarget target = mfirsttouchtarget; while (target != null) { final touchtarget next = target.next; if (alreadydispatchedtonewtouchtarget && target == newtouchtarget) { handled = true; } else { final boolean cancelchild = resetcancelnextupflag(target.child) || intercepted; if (dispatchtransformedtouchevent(ev, cancelchild, target.child, target.pointeridbits)) { handled = true; } if (cancelchild) { if (predecessor == null) { mfirsttouchtarget = next; } else { predecessor.next = next; } target.recycle(); target = next; continue; } } predecessor = target; target = next; }
当存在子view并且事件被子view消费时,即在action_down阶段mfirsttouchtarget会被赋值,即在接下来的action_move事件中,由于intercepted为true,所以将action_cancel 事件传递过去,从dispatchtransformedtouchevent()中可以看到:
if (cancel || oldaction == motionevent.action_cancel) { event.setaction(motionevent.action_cancel); if (child == null) { handled = super.dispatchtouchevent(event); } else { handled = child.dispatchtouchevent(event); } event.setaction(oldaction); return handled; }
并将mfirsttouchtarget 最终赋值为 next,而此时mfirsttouchtarget位于touchtarget链表尾部,所以mfirsttouchtarget会赋值为null,那么接下来的事件将不会进入到onintercepttouchevent()中。也就会直接交由该view处理。
如果我们没有进行事件的拦截,而是交由子view去处理,由于viewgroup的onintercepttouchevent()默认并不会拦截除了action_down以外的事件,所以后续事件将继续交由子view去处理,如果存在子view且事件位于子view内部区域的话。
所以无论是否进行拦截,事件流都会交由view的dispatchtouchevent()中进行处理,我们接下来跟踪一下view中的dispatchtouchevent()处理过程:
if (actionmasked == motionevent.action_down) { // defensive cleanup for new gesture stopnestedscroll(); } if (onfiltertoucheventforsecurity(event)) { if ((mviewflags & enabled_mask) == enabled && handlescrollbardragging(event)) { result = true; } //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; } }
当被按下时,即action_down时,view会停止内部的滚动,如果view没有被覆盖或遮挡时,首先会进行mlistenerinfo是否为空的判断,我们看下mlistenerinfo是在哪里初始化的:
listenerinfo getlistenerinfo() { if (mlistenerinfo != null) { return mlistenerinfo; } mlistenerinfo = new listenerinfo(); return mlistenerinfo; }
这里可以看出,mlistenerinfo一般不会是null,知道在我们使用它时调用过这段代码,而当view被加入window中的时候,会调用下面这段代码,从注释中也可以看出来:
/** * add a listener for attach state changes. * * this listener will be called whenever this view is attached or detached * from a window. remove the listener using * {@link #removeonattachstatechangelistener(onattachstatechangelistener)}. * * @param listener listener to attach * @see #removeonattachstatechangelistener(onattachstatechangelistener) */ public void addonattachstatechangelistener(onattachstatechangelistener listener) { listenerinfo li = getlistenerinfo(); if (li.monattachstatechangelisteners == null) { li.monattachstatechangelisteners = new copyonwritearraylist<onattachstatechangelistener>(); } li.monattachstatechangelisteners.add(listener); }
到这里我们就知道,mlistenerinfo一开始就是被初始化好了的,所以li不可能为null,li.montouchlistener != null即当设置了touchlistener时不为null,并且view是enabled状态,一般情况view都是enable的。这个时候会调用到ontouch()事件,当ontouch()返回true时,这个时候result会赋值true。而当result为true时,ontouchevent()将不会被调用。
从这里可以看出,ontouch()会优先ontouchevent()调用;
当view设置touch监听并返回true时,那么它的ontouchevent()将被屏蔽。否则会调用ontouchevent()处理。
那么让我们继续来看看ontouchevent()中的事件处理:
if ((viewflags & enabled_mask) == disabled) { if (action == motionevent.action_up && (mprivateflags & pflag_pressed) != 0) { setpressed(false); } // a disabled view that is clickable still consumes the touch // events, it just doesn't respond to them. return (((viewflags & clickable) == clickable || (viewflags & long_clickable) == long_clickable) || (viewflags & context_clickable) == context_clickable); }
首先,当view状态是disabled时,只要view是clickable或long_clickable或context_clickable,都会返回true,而button默认是clickable的,textview默认不是clickable的,而view一般默认都不是long_clickable的。
我们继续向下看:
if (mtouchdelegate != null) { if (mtouchdelegate.ontouchevent(event)) { return true; } }
如果有代理事件,仍然会返回true.
if (((viewflags & clickable) == clickable || (viewflags & long_clickable) == long_clickable) || (viewflags & context_clickable) == context_clickable) { switch (action) { case motionevent.action_up: boolean prepressed = (mprivateflags & pflag_prepressed) != 0; if ((mprivateflags & pflag_pressed) != 0 || prepressed) { // take focus if we don't have it already and we should in // touch mode. boolean focustaken = false; if (isfocusable() && isfocusableintouchmode() && !isfocused()) { focustaken = requestfocus(); } if (prepressed) { // the button is being released before we actually // showed it as pressed. make it show the pressed // state now (before scheduling the click) to ensure // the user sees it. setpressed(true, x, y); } if (!mhasperformedlongpress && !mignorenextupevent) { // 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) { mperformclick = new performclick(); } if (!post(mperformclick)) { performclick(); } } } if (munsetpressedstate == null) { munsetpressedstate = new unsetpressedstate(); } if (prepressed) { postdelayed(munsetpressedstate, viewconfiguration.getpressedstateduration()); } else if (!post(munsetpressedstate)) { // if the post failed, unpress right now munsetpressedstate.run(); } removetapcallback(); } mignorenextupevent = false; break; case motionevent.action_down: mhasperformedlongpress = false; if (performbuttonactionontouchdown(event)) { break; } // walk up the hierarchy to determine if we're inside a scrolling container. boolean isinscrollingcontainer = isinscrollingcontainer(); // for views inside a scrolling container, delay the pressed feedback for // a short period in case this is a scroll. if (isinscrollingcontainer) { mprivateflags |= pflag_prepressed; if (mpendingcheckfortap == null) { mpendingcheckfortap = new checkfortap(); } mpendingcheckfortap.x = event.getx(); mpendingcheckfortap.y = event.gety(); postdelayed(mpendingcheckfortap, viewconfiguration.gettaptimeout()); } else { // not inside a scrolling container, so show the feedback right away setpressed(true, x, y); checkforlongclick(0, x, y); } break; case motionevent.action_cancel: setpressed(false); removetapcallback(); removelongpresscallback(); mincontextbuttonpress = false; mhasperformedlongpress = false; mignorenextupevent = false; break; case motionevent.action_move: drawablehotspotchanged(x, y); // be lenient about moving outside of buttons if (!pointinview(x, y, mtouchslop)) { // outside button removetapcallback(); if ((mprivateflags & pflag_pressed) != 0) { // remove any future long press/tap checks removelongpresscallback(); setpressed(false); } } break; } return true; }
当view是clickable或long_clickable或context_clickable状态时,当手指抬起时,如果设置了click监听,最终会调用到performclick(),触发click()事件。这点从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; }
从这里我们也可以得出,click事件会在ontouchevent()中被调用,如果view设置了ontouch()监听并返回true,那么click事件也会被屏蔽掉,不过我们可以在ontouch()中通过调用view的performclick()继续执行click()事件,这个就看我们的业务中的需求了。
从这里我们可以看出,如果事件没有被当前view或子view处理,即返回false,那么事件就会交由外层view继续处理,直到被消费。
如果事件一直没有被处理,会最终传递到activity的ontouchevent()中。
到这里我们总结一下:
事件是从activity->window->view(viewgroup)的一个传递流程;
如果事件没有被中途拦截,那么它会一直传到最内层的view控件;
如果事件被某一层拦截,那么事件将不会向下传递,交由该view处理。如果该view消费了事件,那么接下来的事件也会交由该view处理;如果该view没有消费该事件,那么事件会交由外层view处理,...并最终调用到activity的ontouchevent()中,除非某一层消费了该事件;
一个事件只能交由一个view处理;
dispatchtouchevent()总是会被调用,而且最先被调用,onintercepttouchevent()和ontouchevent()在dispatchtouchevent()内部调用;
子view不能干扰viewgroup对action_down事件的处理;
子view可以通过requestdisallowintercepttouchevent(true)控制父view不对事件进行拦截,跳过onintercepttouchevent()方法的执行。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。