Android事件的分发机制详解
在分析android事件分发机制前,明确android的两大基础控件类型:view和viewgroup。view即普通的控件,没有子布局的,如button、textview. viewgroup继承自view,表示可以有子控件,如linearlayout、listview这些。今天我们先来了解view的事件分发机制。
先看下代码,非常简单,只有一个button,分别给它注册了onclick和ontouch的点击事件。
btn.setonclicklistener(new view.onclicklistener() { @override public void onclick(view v) { log.i("tag", "this is button onclick event"); } }); btn.setontouchlistener(new view.ontouchlistener() { @override public boolean ontouch(view v, motionevent event) { log.i("tag", "this is button ontouch action" + event.getaction()); return false; } });
运行一下项目,结果如下:
i/tag: this is button ontouch action0
i/tag: this is button ontouch action2
i/tag: this is button ontouch action2
i/tag: this is button ontouch action1
i/tag: this is button onclick event
可以看到,ontouch是有先于onclick执行的,因此事件的传递顺序是先ontouch,在到onclick。具体为什么这样,下面会通过源码来说明。这时,我们可能注意到了,ontouch的方法是有返回值,这里是返回false,我们将它改为true再运行一次,结果如下:
i/tag: this is button ontouch action0
i/tag: this is button ontouch action2
i/tag: this is button ontouch action2
i/tag: this is button ontouch action2
i/tag: this is button ontouch action1
对比两次结果,我们发现onclick方法不再执行,为什么会这样,下面我将通过源码给大家一步步理清这个思路。
查看源码时,首先要知道所有view类型控件事件入口都是dispatchtouchevent(),所以我们直接进入到view这个类里面的dispatchtouchevent()方法看一下。
public boolean dispatchtouchevent(motionevent event) { // if the event should be handled by accessibility focus first. if (event.istargetaccessibilityfocus()) { // we don't have focus or no virtual descendant has it, do not handle the event. if (!isaccessibilityfocusedvieworhost()) { return false; } // we have focus and got the event, then use normal event dispatch. event.settargetaccessibilityfocus(false); } boolean result = false; if (minputeventconsistencyverifier != null) { minputeventconsistencyverifier.ontouchevent(event, 0); } final int actionmasked = event.getactionmasked(); if (actionmasked == motionevent.action_down) { // defensive cleanup for new gesture stopnestedscroll(); } 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; } } if (!result && minputeventconsistencyverifier != null) { minputeventconsistencyverifier.onunhandledevent(event, 0); } // clean up after nested scrolls if this is the end of a gesture; // also cancel it if we tried an action_down but we didn't want the rest // of the gesture. if (actionmasked == motionevent.action_up || actionmasked == motionevent.action_cancel || (actionmasked == motionevent.action_down && !result)) { stopnestedscroll(); } return result; }
从源码第25行处可以看到,montouchlistener.ontouch()的方法首先被执行,如果li != null && li.montouchlistener != null&& (mviewflags & enabled_mask) == enabled&& li.montouchlistener.ontouch(this, event)都为真的话,result赋值为true,否则就执行ontouchevent(event)方法。
从上面可以看到要符合条件有四个,
1、listenerinfo li,它是view中的一个静态类,里面定义view的事件的监听等等,所以有涉及到view的事件,listenerinfo都会被实例化,因此li不为null
2、montouchilistener是在setontouchlistener方法里面赋值的,只要touch事件被注册,montouchilistener一定不会null
3、 (mviewflags & enabled_mask) == enabled,是判断当前点击的控件是否是enable的,button默认为enable,这个条件也恒定为true,
4、重点来了,li.montouchlistener.ontouch(this, event)就是回调控件ontouch方法,当这个条件也为true时,result=true,ontouchevent(event)将不会被执行。如果ontouch返回false,就会再执行ontouchevent(event)方法。
我们接着再进入到ontouchevent方法查看源码。
public boolean ontouchevent(motionevent event) { final float x = event.getx(); final float y = event.gety(); final int viewflags = mviewflags; final int action = event.getaction(); 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); } if (mtouchdelegate != null) { if (mtouchdelegate.ontouchevent(event)) { return 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); } 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; } return false; }
从源码的21行我们可以看出,该控件可点击就会进入到switch判断中,当我们触发了手指离开的实际,则会进入到motionevent.action_up这个case当中。我们接着往下看,在源码的50行,调用到了mperformclick()方法,我们继续进入到这个方法的源码看看。
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; }
现在我们可以看到,只要listenerinfo和monclicklistener不为null就会调用onclick这个方法,之前说过,只要有监听事件,listenerinfo就不为null,带monclicklistener又是在哪里赋值呢?我们再继续看下它的源码。
public void setonclicklistener(@nullable onclicklistener l) { if (!isclickable()) { setclickable(true); } getlistenerinfo().monclicklistener = l; }
看到这里一切就清楚了,当我们调用setonclicklistener方法来给按钮注册一个点击事件时,就会给monclicklistener赋值。整个分发事件的顺序是ontouch()-->ontouchevent(event)-->performclick()-->onclick()。
现在我们可以解决之前的问题。
1、ontouch方法是优先于onclick,所以是执行了ontouch,再执行onclick。
2、无论是dispatchtouchevent还是ontouchevent,如果返回true表示这个事件已经被消费、处理了,不再往下传了。在dispathtouchevent的源码里可以看到,如果ontouchevent返回了true,那么它也返回true。如果dispatchtouchevent在执行ontouch监听的时候,ontouch返回了true,那么它也返回true,这个事件提前被ontouch消费掉了。就不再执行ontouchevent了,更别说onclick监听了。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
上一篇: xml 解析之 JDOM解析