Android Touch事件分发过程详解
本文以实例形式讲述了android touch事件分发过程,对于深入理解与掌握android程序设计有很大的帮助作用。具体分析如下:
首先,从一个简单示例入手:
先看一个示例如下图所示:
布局文件 :
<framelayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/container" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="center" tools:context="com.example.touch_event.mainactivity" tools:ignore="mergerootframe" > <button android:id="@+id/my_button" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/hello_world" /> </framelayout>
mainactivity文件:
public class mainactivity extends activity { @override protected void oncreate(bundle savedinstancestate) { super.oncreate(savedinstancestate); setcontentview(r.layout.activity_main); button mbtn = (button) findviewbyid(r.id.my_button); mbtn.setontouchlistener(new ontouchlistener() { @override public boolean ontouch(view v, motionevent event) { log.d("", "### ontouch : " + event.getaction()); return false; } }); mbtn.setonclicklistener(new onclicklistener() { @override public void onclick(view v) { log.d("", "### onclick : " + v); } }); } @override public boolean dispatchtouchevent(motionevent ev) { log.d("", "### activity dispatchtouchevent"); return super.dispatchtouchevent(ev); } }
当用户点击按钮时会输出如下log:
08-31 03:03:56.116: d/(1560): ### activity dispatchtouchevent 08-31 03:03:56.116: d/(1560): ### ontouch : 0 08-31 03:03:56.196: d/(1560): ### activity dispatchtouchevent 08-31 03:03:56.196: d/(1560): ### ontouch : 1 08-31 03:03:56.196: d/(1560): ### onclick : android.widget.button{52860d98 vfed..c. ...ph... 0,0-1080,144 #7f05003d app:id/my_button}
我们可以看到首先执行了activity中的dispatchtouchevent方法,然后执行了ontouch方法,然后再是dispatchtouchevent --> ontouch, 最后才是执行按钮的点击事件。这里我们可能有个疑问,为什么dispatchtouchevent和ontouch都执行了两次,而onclick才执行了一次 ? 为什么两次的touch事件的action不一样,action 0 和 action 1到底代表了什么 ?
覆写过ontouchevent的朋友知道,一般来说我们在该方法体内都会处理集中touch类型的事件,有action_down、action_move、action_up等,不过上面我们的例子中并没有移动,只是单纯的按下、抬起。因此,我们的触摸事件也只有按下、抬起,因此有2次touch事件,而action分别为0和1。我们看看motionevent中的一些变量定义吧:
public final class motionevent extends inputevent implements parcelable { // 代码省略 public static final int action_down = 0; // 按下事件 public static final int action_up = 1; // 抬起事件 public static final int action_move = 2; // 手势移动事件 public static final int action_cancel = 3; // 取消 // 代码省略 }
可以看到,代表按下的事件为0,抬起事件为1,也证实了我们上面所说的。
在看另外两个场景:
1、我们点击按钮外的区域,输出log如下 :
08-31 03:04:45.408: d/(1560): ### activity dispatchtouchevent08-31 03:04:45.512: d/(1560): ### activity dispatchtouchevent
2、我们在ontouch函数中返回true, 输出log如下 :
08-31 03:06:04.764: d/(1612): ### activity dispatchtouchevent 08-31 03:06:04.764: d/(1612): ### ontouch : 0 08-31 03:06:04.868: d/(1612): ### activity dispatchtouchevent 08-31 03:06:04.868: d/(1612): ### ontouch : 1
以上两个场景为什么会这样呢 ? 我们继续往下看吧。
android touch事件分发
那么整个事件分发的流程是怎样的呢 ?
简单来说就是用户触摸手机屏幕会产生一个触摸消息,最终这个触摸消息会被传送到viewroot ( 看4.2的源码时这个类改成了viewrootimpl )的inputhandler,viewroot是gui管理系统与gui呈现系统之间的桥梁,根据viewroot的定义,发现它并不是一个view类型,而是一个handler。inputhandler是一个接口类型,用于处理keyevent和touchevent类型的事件,我们看看源码 :
public final class viewroot extends handler implements viewparent, view.attachinfo.callbacks { // 代码省略 private final inputhandler minputhandler = new inputhandler() { public void handlekey(keyevent event, runnable finishedcallback) { startinputevent(finishedcallback); dispatchkey(event, true); } public void handlemotion(motionevent event, runnable finishedcallback) { startinputevent(finishedcallback); dispatchmotion(event, true); // 1、handle 触摸消息 } }; // 代码省略 // 2、分发触摸消息 private void dispatchmotion(motionevent event, boolean senddone) { int source = event.getsource(); if ((source & inputdevice.source_class_pointer) != 0) { dispatchpointer(event, senddone); // 分发触摸消息 } else if ((source & inputdevice.source_class_trackball) != 0) { dispatchtrackball(event, senddone); } else { // todo log.v(tag, "dropping unsupported motion event (unimplemented): " + event); if (senddone) { finishinputevent(); } } } // 3、通过handler投递消息 private void dispatchpointer(motionevent event, boolean senddone) { message msg = obtainmessage(dispatch_pointer); msg.obj = event; msg.arg1 = senddone ? 1 : 0; sendmessageattime(msg, event.geteventtime()); } @override public void handlemessage(message msg) { // viewroot覆写handlermessage来处理各种消息 switch (msg.what) { // 代码省略 case do_traversal: if (mprofile) { debug.startmethodtracing("viewroot"); } performtraversals(); if (mprofile) { debug.stopmethodtracing(); mprofile = false; } break; case dispatch_pointer: { // 4、处理dispatch_pointer类型的消息,即触摸屏幕的消息 motionevent event = (motionevent) msg.obj; try { deliverpointerevent(event); // 5、处理触摸消息 } finally { event.recycle(); if (msg.arg1 != 0) { finishinputevent(); } if (local_logv || watch_pointer) log.i(tag, "done dispatching!"); } } break; // 代码省略 } // 6、真正的处理事件 private void deliverpointerevent(motionevent event) { if (mtranslator != null) { mtranslator.translateeventinscreentoappwindow(event); } boolean handled; if (mview != null && madded) { // enter touch mode on the down boolean isdown = event.getaction() == motionevent.action_down; if (isdown) { ensuretouchmode(true); // 如果是action_down事件则进入触摸模式,否则为按键模式。 } if(config.logv) { capturemotionlog("capturedispatchpointer", event); } if (mcurscrolly != 0) { event.offsetlocation(0, mcurscrolly); // 物理坐标向逻辑坐标的转换 } if (measure_latency) { lt.sample("a dispatching touchevents", system.nanotime() - event.geteventtimenano()); } // 7、分发事件,如果是窗口类型,则这里的mview对应的就是phonwwindow中的decorview,否则为根视图的viewgroup。 handled = mview.dispatchtouchevent(event); // 代码省略 } } // 代码省略 }
经过层层迷雾,不管代码7处的mview是decorview还是非窗口界面的根视图,其本质都是viewgroup,即触摸事件最终被根视图viewgroup进行分发!!!
我们就以activity为例来分析这个过程,我们知道显示出来的activity有一个顶层窗口,这个窗口的实现类是phonewindow, phonewindow中的内容区域是一个decorview类型的view,这个view这就是我们在手机上看到的内容,这个decorview是framelayout的子类,activity的的dispatchtouchevent实际上就是调用phonewindow的dispatchtouchevent,我们看看源代码吧,进入activity的dispatchtouchevent函数 :
public boolean dispatchtouchevent(motionevent ev) { if (ev.getaction() == motionevent.action_down) { onuserinteraction(); } if (getwindow().superdispatchtouchevent(ev)) { // 1、调用的是phonewindow的superdispatchtouchevent(ev) return true; } return ontouchevent(ev); } public void onuserinteraction() { }
可以看到,如果事件为按下事件,则会进入到onuserinteraction()这个函数,该函数为空实现,我们暂且不管它。继续看,发现touch事件的分发调用了getwindow().superdispatchtouchevent(ev)函数,getwindow()获取到的实例的类型为phonewindow类型,你可以在你的activity类中使用如下方式查看getwindow()获取到的类型:
log.d("", "### activiti中getwindow()获取的类型是 : " + this.getwindow()) ;
输出:
08-31 03:40:17.036: d/(1688): ### activiti中getwindow()获取的类型是 : com.android.internal.policy.impl.phonewindow@5287fe38
ok,废话不多说,我们还是继续看phonewindow中的superdispatchtouchevent函数吧。
@override public boolean superdispatchtouchevent(motionevent event) { return mdecor.superdispatchtouchevent(event); }
恩,调用的是mdecor的superdispatchtouchevent(event)函数,这个mdecor就是我们上面所说的decorview类型,也就是我们看到的activity上的所有内容的一个顶层viewgroup,即整个viewtree的根节点。看看它的声明吧。
// this is the top-level view of the window, containing the window decor. private decorview mdecor;
decorview
那么我继续看看decorview到底是个什么玩意儿吧。
private final class decorview extends framelayout implements rootviewsurfacetaker { /* package */int mdefaultopacity = pixelformat.opaque; /** the feature id of the panel, or -1 if this is the application's decorview */ private final int mfeatureid; private final rect mdrawingbounds = new rect(); private final rect mbackgroundpadding = new rect(); private final rect mframepadding = new rect(); private final rect mframeoffsets = new rect(); private boolean mchanging; private drawable mmenubackground; private boolean mwatchingformenu; private int mdowny; public decorview(context context, int featureid) { super(context); mfeatureid = featureid; } @override public boolean dispatchkeyevent(keyevent event) { final int keycode = event.getkeycode(); // 代码省略 return isdown ? phonewindow.this.onkeydown(mfeatureid, event.getkeycode(), event) : phonewindow.this.onkeyup(mfeatureid, event.getkeycode(), event); } @override public boolean dispatchtouchevent(motionevent ev) { final callback cb = getcallback(); return cb != null && mfeatureid < 0 ? cb.dispatchtouchevent(ev) : super .dispatchtouchevent(ev); } @override public boolean dispatchtrackballevent(motionevent ev) { final callback cb = getcallback(); return cb != null && mfeatureid < 0 ? cb.dispatchtrackballevent(ev) : super .dispatchtrackballevent(ev); } public boolean superdispatchkeyevent(keyevent event) { return super.dispatchkeyevent(event); } public boolean superdispatchtouchevent(motionevent event) { return super.dispatchtouchevent(event); } public boolean superdispatchtrackballevent(motionevent event) { return super.dispatchtrackballevent(event); } @override public boolean ontouchevent(motionevent event) { return onintercepttouchevent(event); } // 代码省略 }
可以看到,decorview继承自framelayout, 它对于touch事件的分发( dispatchtouchevent )、处理都是交给super类来处理,也就是framelayout来处理,我们在framelayout中没有看到相应的实现,那继续跟踪到framelayout的父类,即viewgroup,我们看到了dispatchtouchevent的实现,那我们就先看viewgroup (android 2.3 源码)是如何进行事件分发的吧。
viewgroup的touch事件分发
/** * {@inheritdoc} */ @override public boolean dispatchtouchevent(motionevent ev) { if (!onfiltertoucheventforsecurity(ev)) { return false; } final int action = ev.getaction(); final float xf = ev.getx(); final float yf = ev.gety(); final float scrolledxfloat = xf + mscrollx; final float scrolledyfloat = yf + mscrolly; final rect frame = mtemprect; boolean disallowintercept = (mgroupflags & flag_disallow_intercept) != 0; if (action == motionevent.action_down) { if (mmotiontarget != null) { // this is weird, we got a pen down, but we thought it was // already down! // xxx: we should probably send an action_up to the current // target. mmotiontarget = null; } // if we're disallowing intercept or if we're allowing and we didn't // intercept if (disallowintercept || !onintercepttouchevent(ev)) // 1、是否禁用拦截、是否拦截事件 // reset this event's action (just to protect ourselves) ev.setaction(motionevent.action_down); // we know we want to dispatch the event down, find a child // who can handle it, start with the front-most child. final int scrolledxint = (int) scrolledxfloat; final int scrolledyint = (int) scrolledyfloat; final view[] children = mchildren; final int count = mchildrencount; for (int i = count - 1; i >= 0; i--) // 2、迭代所有子view,查找触摸事件在哪个子view的坐标范围内 final view child = children[i]; if ((child.mviewflags & visibility_mask) == visible || child.getanimation() != null) { child.gethitrect(frame); // 3、获取child的坐标范围 if (frame.contains(scrolledxint, scrolledyint)) // 4、判断发生该事件坐标是否在该child坐标范围内 // offset the event to the view's coordinate system final float xc = scrolledxfloat - child.mleft; final float yc = scrolledyfloat - child.mtop; ev.setlocation(xc, yc); child.mprivateflags &= ~cancel_next_up_event; if (child.dispatchtouchevent(ev)) // 5、child处理该事件 // event handled, we have a target now. mmotiontarget = child; return true; } // the event didn't get handled, try the next view. // don't reset the event's location, it's not // necessary here. } } } } } boolean isuporcancel = (action == motionevent.action_up) || (action == motionevent.action_cancel); if (isuporcancel) { // note, we've already copied the previous state to our local // variable, so this takes effect on the next event mgroupflags &= ~flag_disallow_intercept; } // the event wasn't an action_down, dispatch it to our target if // we have one. final view target = mmotiontarget; if (target == null) { // we don't have a target, this means we're handling the // event as a regular view. ev.setlocation(xf, yf); if ((mprivateflags & cancel_next_up_event) != 0) { ev.setaction(motionevent.action_cancel); mprivateflags &= ~cancel_next_up_event; } return super.dispatchtouchevent(ev); } // if have a target, see if we're allowed to and want to intercept its // events if (!disallowintercept && onintercepttouchevent(ev)) { final float xc = scrolledxfloat - (float) target.mleft; final float yc = scrolledyfloat - (float) target.mtop; mprivateflags &= ~cancel_next_up_event; ev.setaction(motionevent.action_cancel); ev.setlocation(xc, yc); if (!target.dispatchtouchevent(ev)) { // target didn't handle action_cancel. not much we can do // but they should have. } // clear the target mmotiontarget = null; // don't dispatch this event to our own view, because we already // saw it when intercepting; we just want to give the following // event to the normal ontouchevent(). return true; } if (isuporcancel) { mmotiontarget = null; } // finally offset the event to the target's coordinate system and // dispatch the event. final float xc = scrolledxfloat - (float) target.mleft; final float yc = scrolledyfloat - (float) target.mtop; ev.setlocation(xc, yc); if ((target.mprivateflags & cancel_next_up_event) != 0) { ev.setaction(motionevent.action_cancel); target.mprivateflags &= ~cancel_next_up_event; mmotiontarget = null; } return target.dispatchtouchevent(ev); }
这个函数代码比较长,我们只看上文中标注的几个关键点。首先在代码1处可以看到一个条件判断,如果disallowintercept和!onintercepttouchevent(ev)两者有一个为true,就会进入到这个条件判断中。disallowintercept是指是否禁用掉事件拦截的功能,默认是false,也可以通过调用requestdisallowintercepttouchevent方法对这个值进行修改。那么当第一个值为false的时候就会完全依赖第二个值来决定是否可以进入到条件判断的内部,第二个值是什么呢?onintercepttouchevent就是viewgroup对事件进行拦截的一个函数,返回该函数返回false则表示不拦截事件,反之则表示拦截。第二个条件是是对onintercepttouchevent方法的返回值取反,也就是说如果我们在onintercepttouchevent方法中返回false,就会让第二个值为true,从而进入到条件判断的内部,如果我们在onintercepttouchevent方法中返回true,就会让第二个值的整体变为false,从而跳出了这个条件判断。例如我们需要实现listview滑动删除某一项的功能,那么可以通过在onintercepttouchevent返回true,并且在ontouchevent中实现相关的判断逻辑,从而实现该功能。
进入代码1内部的if后,有一个for循环,遍历了当前viewgroup下的所有子child view,如果触摸该事件的坐标在某个child view的坐标范围内,那么该child view来处理这个触摸事件,即调用该child view的dispatchtouchevent。如果该child view是viewgroup类型,那么继续执行上面的判断,并且遍历子view;如果该child view不是viewgroup类型,那么直接调用的是view中的dispatchtouchevent方法,除非这个child view的类型覆写了该方法。我们看看view中的dispatchtouchevent函数:
view的touch事件分发
/** * pass the touch screen motion event down to the target view, or this * view if it is the target. * * @param event the motion event to be dispatched. * @return true if the event was handled by the view, false otherwise. */ public boolean dispatchtouchevent(motionevent event) { if (!onfiltertoucheventforsecurity(event)) { return false; } if (montouchlistener != null && (mviewflags & enabled_mask) == enabled && montouchlistener.ontouch(this, event)) { return true; } return ontouchevent(event); }
该函数中,首先判断该事件是否符合安全策略,然后判断该view是否是enable的 ,以及是否设置了touch listener,montouchlistener即我们通过setontouchlistener设置的。
/** * register a callback to be invoked when a touch event is sent to this view. * @param l the touch listener to attach to this view */ public void setontouchlistener(ontouchlistener l) { montouchlistener = l; }
如果montouchlistener.ontouch(this, event)返回false则继续执行ontouchevent(event);如果montouchlistener.ontouch(this, event)返回true,则表示该事件被消费了,不再传递,因此也不会执行ontouchevent(event)。这也验证了我们上文中留下的场景2,当ontouch函数返回true时,点击按钮,但我们的点击事件没有执行。那么我们还是先来看看ontouchevent(event)函数到底做了什么吧。
/** * implement this method to handle touch screen motion events. * * @param event the motion event. * @return true if the event was handled, false otherwise. */ public boolean ontouchevent(motionevent event) { final int viewflags = mviewflags; if ((viewflags & enabled_mask) == disabled) // 1、判断该view是否enable // 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)); } if (mtouchdelegate != null) { if (mtouchdelegate.ontouchevent(event)) { return true; } } if (((viewflags & clickable) == clickable || (viewflags & long_clickable) == long_clickable)) // 2、是否是clickable或者long clickable switch (event.getaction()) { case motionevent.action_up: // 抬起事件 boolean prepressed = (mprivateflags & prepressed) != 0; if ((mprivateflags & 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 (!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) { mperformclick = new performclick(); } if (!post(mperformclick)) // post performclick(); // 3、点击事件处理 } } } if (munsetpressedstate == null) { munsetpressedstate = new unsetpressedstate(); } if (prepressed) { mprivateflags |= pressed; refreshdrawablestate(); postdelayed(munsetpressedstate, viewconfiguration.getpressedstateduration()); } else if (!post(munsetpressedstate)) { // if the post failed, unpress right now munsetpressedstate.run(); } removetapcallback(); } break; case motionevent.action_down: if (mpendingcheckfortap == null) { mpendingcheckfortap = new checkfortap(); } mprivateflags |= prepressed; mhasperformedlongpress = false; postdelayed(mpendingcheckfortap, viewconfiguration.gettaptimeout()); break; case motionevent.action_cancel: mprivateflags &= ~pressed; refreshdrawablestate(); removetapcallback(); break; case motionevent.action_move: final int x = (int) event.getx(); final int y = (int) event.gety(); // be lenient about moving outside of buttons 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; } return true; } return false; }
我们看到,在ontouchevent函数中就是对action_up、action_down、action_move等几个事件进行处理,而最重要的就是up事件了,因为这个里面包含了对用户点击事件的处理,或者是说对于用户而言相对重要一点,因此放在了第一个case中。在action_up事件中会判断该view是否enable、是否clickable、是否获取到了焦点,然后我们看到会通过post方法将一个performclick对象投递给ui线程,如果投递失败则直接调用performclick函数执行点击事件。
/** * causes the runnable to be added to the message queue. * the runnable will be run on the user interface thread. * * @param action the runnable that will be executed. * * @return returns true if the runnable was successfully placed in to the * message queue. returns false on failure, usually because the * looper processing the message queue is exiting. */ public boolean post(runnable action) { handler handler; if (mattachinfo != null) { handler = mattachinfo.mhandler; } else { // assume that post will succeed later viewroot.getrunqueue().post(action); return true; } return handler.post(action); }
我们看看performclick类吧。
private final class performclick implements runnable { public void run() { performclick(); } }
可以看到,其内部就是包装了view类中的performclick()方法。再看performclick()方法:
/** * register a callback to be invoked when this view is clicked. if this view is not * clickable, it becomes clickable. * * @param l the callback that will run * * @see #setclickable(boolean) */ public void setonclicklistener(onclicklistener l) { if (!isclickable()) { setclickable(true); } monclicklistener = l; } /** * call this view's onclicklistener, if it is defined. * * @return true there was an assigned onclicklistener that was called, false * otherwise is returned. */ public boolean performclick() { sendaccessibilityevent(accessibilityevent.type_view_clicked); if (monclicklistener != null) { playsoundeffect(soundeffectconstants.click); monclicklistener.onclick(this); return true; } return false; }
代码很简单,主要就是调用了monclicklistener.onclick(this);方法,即执行用户通过setonclicklistener设置进来的点击事件处理listener。
总结
用户触摸屏幕产生一个触摸消息,系统底层将该消息转发给viewroot ( viewrootimpl ),viewroot产生一个dispatche_pointer的消息,并且在handlemessage中处理该消息,最终会通过deliverpointerevent(motionevent event)来处理该消息。在该函数中会调用mview.dispatchtouchevent(event)来分发消息,该mview是一个viewgroup类型,因此是viewgroup的dispatchtouchevent(event),在该函数中会遍历所有的child view,找到该事件的触发的左边与每个child view的坐标进行对比,如果触摸的坐标在该child view的范围内,则由该child view进行处理。如果该child view是viewgroup类型,则继续上一步的查找过程;否则执行view中的dispatchtouchevent(event)函数。在view的dispatchtouchevent(event)中首先判断该控件是否enale以及montouchlistent是否为空,如果montouchlistener不为空则执行montouchlistener.ontouch(event)方法,如果该方法返回false则再执行view中的ontouchevent(event)方法,并且在该方法中执行monclicklistener.onclick(this, event) ;方法; 如果montouchlistener.ontouch(event)返回true则不会执行ontouchevent方法,因此点击事件也不会被执行。
粗略的流程图如下 :
相信本文所述对大家进一步深入掌握android程序设计有一定的借鉴价值。