Android的事件分发源码分析,告别事件冲突。
一、前言
android的事件分发,大多数人都是似懂非懂,很多时候就卡在事件冲突这一步。比如在按钮上不能滑动出侧边栏,比如说ViewPager和banner冲突。我之前也是这样,然后狠下心去看了一遍源码,并且看了很多大神的博客,然后以我自身的理解配合源码来查看一个事件的传递过程。源码用的是API-8的,因为版本越高,健壮性越好,代码阅读性越差。
因为篇幅比较长,所以更底层的代码我也不准备写了,日后有机会再研究。在看博客之前,我们需要先来了解一些事件分发的基本流程,然后再一步步的深入去研究。
这其中有三个关键方法,首先我们先来理解几个方法的方法名和它们的返回值所代表的意义。
- dispatchTouchEvent: 简单的理解就是分发Touch事件,如果return true,表示事件已经被消费,不继续分发。return false表示没有被消费,继续分发。
- onInterceptTouchEvent: 拦截Touch事件,ViewGroup在dispatchTouchEvent返回之前就会调用这个方法,根据onInterceptTouchEvent的返回值决定事件是否继续往下分发。onInterceptTouchEvent的默认返回值都是false,表示不拦截。想要拦截的话需要开发者自己去重载这个方法。
- onTouchEvent: 处理Touch事件,如果return ture,那么这个时间就消费掉了。
细心的网友发现了,dispatch无论return true还是false,都不往下继续分发,不对吧!
dispatchTouchEvent一般不会直接return true或false。而是将事件抛给onInterceptTouchEvent和onTouchevent处理,问它们需不需要,最后再由dispatchTouchEvent进行最终的返回。这是很多新手开发者的理解误区,包括以前的我……
二、事件分发的基本过程
事件是怎么产生的这种底层问题我们先不管,我们只从知道的地方开始说起——Activity。
当一个事件开始传递后,最先接收到的是当前的Activity。然后Activity调用public boolean dispatchTouchEvent(MotionEvent ev)
开始分发事件。内部又会调用PhoneWindow的内部对象DecorView的superdispatchKeyEvent,也就是ViewGroup的dispatchTouchEvent开始事件分发,DecorView是最顶层的View。
在dispatchTouchEvent方法中,ViewGroup会遍历自身的child(move事件和up事件有点不同,不会遍历child,下面会另外说明)
,再去调用子child的dispathTouchEvent方法,直到该事件被消费。传递途中还有onInterceptTouchEvent和onTouchEvent方法参与。
事件是由最外层的View开始传递,然后结果从最底层往外层返回。
如下图:ViewGroupA,ViewGroupB ,View的关系: A 是B的parent, B是C的parent。
图中的call是“调用”
我们再用代码来看下大致流程。
我们先建一个项目,然后写几个类继承FrameLayout,重写dispatchEvnet、onInterceptTouchEvent,onTouchEvent几个方法,看log输出。
ViewGroupA,B一样。
package com.aitsuki.touchevent;import android.content.Context;import android.util.AttributeSet;import android.util.Log;import android.view.MotionEvent;import android.widget.FrameLayout;/** * Created by AItsuki on 2015/12/30. */public class ViewGroupA extends FrameLayout { public ViewGroupA(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: Log.e("Event","===ViewGroup:A======onTouchEvent===============Down"); break; case MotionEvent.ACTION_MOVE: Log.e("Event","===ViewGroup:A======onTouchEvent===============Move"); break; case MotionEvent.ACTION_UP: Log.e("Event","===ViewGroup:A======onTouchEvent===============Up"); break; } return super.onTouchEvent(event); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: Log.e("Event","===ViewGroup:A======onInterceptTouchEvent======Down"); break; case MotionEvent.ACTION_MOVE: Log.e("Event","===ViewGroup:A======onInterceptTouchEvent======Move"); break; case MotionEvent.ACTION_UP: Log.e("Event","===ViewGroup:A======onInterceptTouchEvent======Up"); break; } return super.onInterceptTouchEvent(ev); } @Override public boolean dispatchTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: Log.e("Event","===ViewGroup:A======dispatchTouchEvent========Down"); break; case MotionEvent.ACTION_MOVE: Log.e("Event","===ViewGroup:A======dispatchTouchEvent========Move"); break; case MotionEvent.ACTION_UP: Log.e("Event","===ViewGroup:A======dispatchTouchEvent========Up"); break; } return super.dispatchTouchEvent(ev); }}
MyView
package com.aitsuki.touchevent;import android.content.Context;import android.util.AttributeSet;import android.util.Log;import android.view.MotionEvent;import android.view.View;/** * Created by AItsuki on 2015/12/30. */public class MyView extends View { public MyView(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: Log.e("Event","===View=============onTouchEvent===============Down"); break; case MotionEvent.ACTION_MOVE: Log.e("Event","===View=============onTouchEvent===============Move"); break; case MotionEvent.ACTION_UP: Log.e("Event","===View=============onTouchEvent===============Up"); break; } return super.onTouchEvent(event); } @Override public boolean dispatchTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: Log.e("Event","===View=============dispatchTouchEvent=========Down"); break; case MotionEvent.ACTION_MOVE: Log.e("Event","===View=============dispatchTouchEvent=========Move"); break; case MotionEvent.ACTION_UP: Log.e("Event","===View=============dispatchTouchEvent=========Up"); break; } return super.dispatchTouchEvent(event); }}
布局activity_main.xml
<?xml version="1.0" encoding="utf-8"?><FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <com.aitsuki.touchevent.ViewGroupA android:layout_width="300dp" android:layout_height="300dp" android:background="#FF6A6A" android:layout_gravity="center"> <com.aitsuki.touchevent.ViewGroupB android:layout_width="200dp" android:layout_height="200dp" android:layout_gravity="center" android:background="#9ACD32"> <com.aitsuki.touchevent.MyView android:layout_width="100dp" android:layout_height="100dp" android:background="#1E90FF" android:layout_gravity="center"/> </com.aitsuki.touchevent.ViewGroupB> </com.aitsuki.touchevent.ViewGroupA></FrameLayout>
布局预览:
红色:ViewGroupA
绿色:ViewGroupB
蓝色:View
现在我点击一下蓝色区域(View),看Log输出。
我们给ViewGroupA加上拦截之后再看看(让onInterceptTouchEvent返回true)
好了,基本流程和我们的图是一致的。
但是要注意 一、前言 的那段红字
三、 View的源码走读
大概理解了事件的传递过程之后,我们来看一下源码。
为什么先看View的源码而不看ViewGroup的原因有两点:
1. View是ViewGroup的父类,ViewGroup的onTouchEvent方法继承自View, 并没有重写。
2. View没有child,事件传递简单,不会打消各位的阅读源码积极性。
那么,开始吧。
先来看View的dispatchTouchView方法,我直接在上面注释。
mViewFlags:一种通过位运算记录开关的方式。mViewFlags一个32位的int值,用每一位的0或1记录属性。比如第1位的0和1记录focusable(是否可以获取焦点)
/** * 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) { // 如果这个View设置了触摸监听onTouchListener并且View是可用的,并且onTouch返回的是true // 那么事件就消费掉了,传递结束。 if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && mOnTouchListener.onTouch(this, event)) { return true; } // 否则,交给onTouchEvent处理(View中没有onInterceptTouchEvent方法,因为它没有 // child了,不需要有拦截方法) return onTouchEvent(event); }// 注意看谷歌工程师的注释,它们称消费事件的View为target view,如果这里的dispatch或者说// onTouchEvent返回true,那么这个View就是target View了。先记下来
继续看onTouchEvent
/** * 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; // 如果View不可用,但它却是可点击的(clickable属性),那么仍然消费这个事件(但是不执行任何 // 操作,也就是不会响应)。也就是说,只要有clickable属性,那么这个点击事件就必然被消费掉。 if ((viewFlags & ENABLED_MASK) == DISABLED) { // 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)); } // 这个是触摸代理,就是点击另一个View,这个view会响应点击事件。默认是null,开发者可以通过 // setTouchDelegate设置。详情请自行查看TouchDelegate if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } } // 如果View是可点击的,那么消费掉这个事件,否则返回给上层处理(parent) if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) { //...... 此处省略N行代码,没有return语句,我们pass。 return true; } return false; }
好了,View处理事件的源码就这么点。一会就看完了,挺简单的。
从上面这两段代码可以得出的结论:
结论1:onTouch优先于onTouchEvent执行,并且onTouch消费掉事件后,onTouEvent不会再执行。
结论2:如果View是可点击的(clickable),那么事件一定会被消费掉,不会再继续传递。
我们来验证一下结论,用的还是 二、事件分发的基本过程 的那个项目。
我们给MyView设置touchListener并return true,然后点击蓝色区域看看log输出
activity代码:
public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); MyView view = (MyView) findViewById(R.id.view); view.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: Log.e("Event","===View=============onTouch====================Down"); break; case MotionEvent.ACTION_MOVE: Log.e("Event","===View=============onTouch====================Move"); break; case MotionEvent.ACTION_UP: Log.e("Event","===View=============onTouch====================Up"); break; } return true; } }); }}
结论1:onTouchEvent没有执行,验证正确。
将MainActivity设置侦听的代码注释掉,给View设置clickable属性,测试结论3
public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); MyView view = (MyView) findViewById(R.id.view); view.setClickable(true);// view.setOnTouchListener(new View.OnTouchListener() {// @Override// public boolean onTouch(View v, MotionEvent event) {// switch (event.getAction()) {// case MotionEvent.ACTION_DOWN:// Log.e("Event","===View=============onTouch====================Down");// break;// case MotionEvent.ACTION_MOVE:// Log.e("Event","===View=============onTouch====================Move");// break;// case MotionEvent.ACTION_UP:// Log.e("Event","===View=============onTouch====================Up");// break;// }// return true;// }// }); }}
View执行了onTouchEvent,消费掉了事件,不再传递,结论3验证正确
请各位网友理解了View的事件传递的结论消化之后再继续往下看,因为ViewGroup中多次调用到super.dispatchTouchEvent, 其实也就是调用View的dispatchTouchEvent,因为View就是ViewGroup的父类。
四、ViewGroup源码走读
因为注释比较多,所以有点影响阅读性,最好可以配合没有注释的源码(api-8 Android2.2)一起阅读。同时思考一下我的分析是否和你的一样,有什么错误也可以在评论中指出。
我们只需要看dispatch的源码就可以了,onInterceptTouchEvent默认都是return false,没有onTouchEvent。
和View一样,也是使用注释的方式来说明。
@Override public boolean dispatchTouchEvent(MotionEvent ev) { // 获取事件的类型和触摸坐标 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; // mTempRect初始是null的,这个Rect对象主要是用来记录child的可点击范围。 final Rect frame = mTempRect; // mGroupFlags和mViewFlags一样是记录当前控件的状态。这里记录的是“是否允许拦截”这个属性。 // 可以通过requestDisallowInterceptTouchEvent这个方法进行设置,如果设置了这个属性, // 那么ViewGroup就不会拦截child的事件了。 boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; // 如果是down事件,就遍历child进行分发。 if (action == MotionEvent.ACTION_DOWN) { // 两位谷歌工程师在聊天么=。=,大概意思就是: // 为什么一个View响应完down事件后还没有消失,继续响应了第二次down事件。 // xxx: 我们可能应该发送一个up事件,而不是down…… // target:在之前也说过了,响应了down事件的那个View就是target,而在up或者cancel // 事件执行后,这个target应该会被重置为null。 // 但是这里居然不是空的。博主我也不知道什么回事=。= // 如果不是null,那么就让它重置为null。 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 // 这里判断disallowIntercept有点多余,因为执行up或者cancel之后,这个属性就会被重 // 置为false(往下找)。而requestDisallowInterceptTouchEvent方法一般在child的 // dispatchTouchEvent中调用,但是事件还没有传到chid,那么这个方法也就不会执行, // disallowIntercept肯定也只能是false。 // 我们无视掉这里的disallowIntercept,这里判断是否拦截child的事件。 if (disallowIntercept || !onInterceptTouchEvent(ev)) { // reset this event's action (just to protect ourselves) // 重置这个事件为donw事件,只是为了保证健壮性。上面那个disallowIntercept也是 // 为了健壮性么=。= 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. // 不得不说谷歌工程师的注释也是很详细的=。= // 我们想要分发这个down事件,遍历子View看谁可以持有它,从最上层的子View开始。 final int scrolledXInt = (int) scrolledXFloat; final int scrolledYInt = (int) scrolledYFloat; final View[] children = mChildren; final int count = mChildrenCount; // 这里开始遍历所有child for (int i = count - 1; i >= 0; i--) { final View child = children[i]; // 如果child是可见的,或者child正在进行动画(动画中的View这个我没细看 // ,无视掉好了)。 if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) { // 获得child的有效点击范围,判断点击事件是否点在此child中。 child.getHitRect(frame); if (frame.contains(scrolledXInt, scrolledYInt)) { // offset the event to the view's coordinate system // 获取到事件的坐标是屏幕的绝对坐标,要转成child的相对坐标。 final float xc = scrolledXFloat - child.mLeft; final float yc = scrolledYFloat - child.mTop; ev.setLocation(xc, yc); // 如果点击事件在此child中,重置属性:CANCEL_NEXT_UP_EVENT // 该属性的描述:Indicates whether the view is temporarily // detached。 // 标记哪一个View是暂时和parent分离,就是和当前这个ViewGroup分 // 离。 // 有什么应用场景我也不知道,不过abslistView中是用到了,那边的源码 // PS:位运算没忘吧=。=,比如CANCEL_NEXT_UP_EVENT = 0000 1000 // 取反 1111 0111。 与上这个数就是取消 CANCEL_NEXT_UP_EVENT // 这个属性…… child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT; // 然后这里调用child的事件分发 // 这里会出现几种情况 // 1:child是View,那么回想一下View的源码吧(三、View源码走读) // 如果child消费了此down事件,那么这个child就是target了。 // 如果不消费,那么返回给当前的ViewGroup的onTouchEvent消费。 // 2:child是ViewGroup,那么继续调用child的dispatch, // 继续遍历child的child(噗,孙子),直到有响应这个事件的。 // 如果最底层的child也是ViewGroup,那么请直接跳过这个遍历, // 往下看…… if(target == null)那里。 // 调用super.dispatchEvent。 // 也就是说,如果最底层的child是ViewGroup,那么将它作为View处理。 if (child.dispatchTouchEvent(ev)) { // Event handled, we have a target now. // 如果响应了down事件,那么这个child就是target 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. } } } } }// 思考:dispatch中调用child的dispatch方法,这看起来是不是像递归遍历。直到有child响应down事件,// 否则将所有child遍历完后这个事件流产。// 注意:child有可能是View,也有可能是ViewGroup。它们两个的dispatch方法是不同的。如果是View,// 就代表这个事件分发已经到了最底下的View了。//=================================================================================== // 判断事件是否是up或者cancel boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) || (action == MotionEvent.ACTION_CANCEL); // 如果是up或者cancel事件,将FLAG_DISALLOW_INTERCEPT(disallowIntercept是通过 // 这个值计算的)这个属性移除。 // 就是说,既然是up事件和cancel事件已经接收到,那么就代表这在target上的事件结束了, // 重置这个属性,不然会影响到下一个事件。 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. // 如果traget存在,并且这个事件不是down,那么就将事件交给target处理 // (谁响应了down事件,后续事件就交给谁处理) final View target = mMotionTarget; if (target == null) { // We don't have a target, this means we're handling the // event as a regular view. // 如果target为null,那么代表down事件没有被响应(也可能是target的后续事件被拦截, // 那么target也会被重置为null) ev.setLocation(xf, yf); // 如果当前的View或者说ViewGroup已经从parent中分离,那么将事件改为cancel if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) { ev.setAction(MotionEvent.ACTION_CANCEL); mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT; } // 这里关键了,如果这个ViewGroup没有child,那么它就会走到这里,然后调用View的事件 // 分发, 将自己作为一个View处理。 // 当返回dispatch返回true之后,这个ViewGroup本身就是target了,不过这个target引用 // 是在parent中,不是当前的=。= return super.dispatchTouchEvent(ev); } // 代码能走到这里的话就代表,这不是一个down事件。 // if have a target, see if we're allowed to and want to intercept its // events // 如果我们有一个target,我们就会将后续的事件都交给target处理(跳过这个if判断直接往 // 下看)。 // 结合前面分析,从这里看出,如果某个child请求了requestDisallow,那么child就肯定能 // 接收到move和up事件,前提是没有拦截down事件。 // 假设我们拦截了后续事件(不拦截child的down事件,但是拦截了child的其他事件) 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); // 将事件改成cancel,然后交给target的dispatch处理。(这里可以看出,如果有 // target响应的down事件,但是target后续事件被拦截,那么就会传一个cancel给target) // 不管它有没有响应,我们都将target置为空,就是说,后续事件当做没有target来处理。 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; } // 如果事件是up或者cancel,那么将target重置为null。因为响应down事件的就是target, // 既然已经抬起手指了,那target就没了。 if (isUpOrCancel) { mMotionTarget = null; } // finally offset the event to the target's coordinate system and // dispatch the event. // 将坐标转成target的相对坐标。 final float xc = scrolledXFloat - (float) target.mLeft; final float yc = scrolledYFloat - (float) target.mTop; ev.setLocation(xc, yc); // 如果target已经被分离出去(相当于remove),那么将事件改成cancel。 if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) { ev.setAction(MotionEvent.ACTION_CANCEL); target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT; mMotionTarget = null; } // 将所有后续事件都交给target处理。 return target.dispatchTouchEvent(ev); }
注释太多看起来有点乱,不过没关系,大家配合无注释的代码一起看就行了。我只是将每一行的代码的作用注释出来,各种结论还是需要推测。
五、结论
现在我们来总结一下分析源码得出的一些结论:
结论1:onTouch优先于onTouchEvent执行,并且onTouch消费掉事件后,onTouEvent不会再执行。
结论2:如果View是可点击的(clickable),那么事件一定会被消费掉,不会再继续传递。
结论3: 如果ViewGroup在onInterceptTouchEvent中拦截了child的事件,那么这个事件会交给ViewGroup的onTouchEvent处理。
结论4:onInterceptTouchEvent方法在一次事件序列(down到up或者cancel的过程)中,只要返回true就不会再调用,或者说只要拦截过一次之后就不会再调用,直到下一次down事件开始前。
结论5:响应了down事件的View被称为target,Android会将后续事件都交给target处理。
结论6:在结论5的基础上,如果onInterceptTouchEvent中拦截了target的其他事件,比如move或up,那么target就会接受到一个cancel事件,并且将target置为null,后续的事件交给target的parent处理。(比如,我可以让child响应down事件,然后只拦截它的move事件,那么我就可以接受到除了down的所有后续事件了,而child则会接收到一个cancel事件,这样就可以解决滑动事件和按钮冲突的问题了。)
结论7:在结论6的基础上,child可以通过requestDisallowInterceptTouchEvent请求parent不拦截他的事件,前提是child能响应到down事件。(例如:parent在onIntercept中拦截了事件,child就没机会请求了。再例如:parent不拦截down事件,但是拦截了move和up事件,这时候requestDisallowInterceptTouchEvent就派上用场了)
结论8: 如果一个View在处理一个事件序列(down到up或者cancel的过程)的时候,parent将他remove掉了,那么这个View会接收到一个cancel事件。
结论的验证过程都很简单,因为文章篇幅已经太长了不打算贴出来,请自行验证结论的正确性,也可惜选择相信我的验证结果。
六、写在后面
这博客真的很难写,花了两天多的时间。不知道应该怎么写才能更加简洁易懂,最后干脆以注释的方式了。
虽然写了七八个结论,但是这并不是全部,我也很难将所有结论一个一个列出来。
分析源码,读懂源码的好处就是,当你发现事件冲突的时候,不会像无头苍蝇一样在百度胡乱搜方案,最后还搞得一头雾水。而是让你自己有能力解决冲突,能找到冲突的源头。
至于解决事件冲突的一些案例,我有空的话可能会整理几个出来,但是写着博客有种身心疲惫的感觉_(:з」∠)_
,休息一段时间再说,就这样,下次见。
更多相关文章
- android 短信接收流程分析——为更好的拦截短信做准备
- 键盘按下和抬起事件(keydown,keyup)——原创
- 关于Android滑动冲突的解决方法(二)
- Android:分析onXXX事件监听器中的两个参数position和id
- android HOME、back(按钮、事件)截取获得,综合解决方案和分析,包含an
- Android事件管理源码剖析
- Android小部件布局大小和点击事件
- Android(安卓)API Guides---Drag and Drop
- 滑轮控件研究二、GestureDetector的深入研究