前言:其实一直想写一篇关于事件分发的文章,平时上班也忙,一直没有时间,正好周末休息可以有时间了,发现这个不是很复杂的东西老是遗忘,实际网上有很多关于这样的文章,View的事件分发,ViewGroup的事件分发,其实很多都是说的很笼统,配置案例演示下就结束了,没有真正说的底层原理的东西,比如事件传替Activity—–>PhoneWindow—->ViewGroup—->View,整个流程说的不是很详细,我想如果要想说清这些事情,绝非三言两语就能说清的,我必须首先跟你说关于View的事件处理机制,接着会分析ViewGroup的事件分发机制,最后得说说window这个东西,包括我们都知道事件是由activity最先的dispatchTouchEvent来分发的,那么activity的事件到底又是怎么传替给他的?接下来我将会带领大家一起梳理下,再讲ViewGroup事件分发之前呢,先来了解下View的事件分发。
新建项目TouchEvent。代码如下:
新建MyChildButton继承自Button
/**
*
* @author xyy
*
*/
public class MyChildButton extends Button {
public MyChildButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.v(MainActivity.TAG,"MyChildButton:dispatchTouchEvent = ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.v(MainActivity.TAG,"MyChildButton:dispatchTouchEvent = ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.v(MainActivity.TAG,"MyChildButton:dispatchTouchEvent = ACTION_UP");
break;
}
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.v(MainActivity.TAG, "MyChildButton:onTouchEvent = ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.v(MainActivity.TAG, "MyChildButton:onTouchEvent = ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.v(MainActivity.TAG, "MyChildButton:onTouchEvent = ACTION_UP");
break;
}
return super.onTouchEvent(event);
}
}
布局文件:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/scr"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#0000FF"
android:gravity="center" >
<com.xyy.demo.MyChildButton
android:id="@+id/button"
android:layout_height="100dp"
android:layout_width="100dp"
android:background="#00FF00"
/>
</LinearLayout>
主Activity
/**
*
* @author xyy
*
*/
public class MainActivity extends Activity {
private MyChildButton image;
public static final String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_main);
image = (MyChildButton)findViewById(R.id.button);
}
}
可以看到代码异常的简单,自定义了一个Button继承自Button,进行了Touch事件信息大打印,最后在MainActivity里面加载。最终效果:
手指轻轻的点击了下屏幕上的绿色按钮,在控制台打印了如下信息:
09-03 16:17:29.497: V/MainActivity(2027): MyChildButton:dispatchTouchEvent = ACTION_DOWN
09-03 16:17:29.497: V/MainActivity(2027): MyChildButton:onTouchEvent = ACTION_DOWN
09-03 16:17:29.517: V/MainActivity(2027): MyChildButton:dispatchTouchEvent = ACTION_UP
09-03 16:17:29.517: V/MainActivity(2027): MyChildButton:onTouchEvent = ACTION_UP
刚刚我没有滑动,如果有滑动操作那么应该会有一系列的Move事件:
09-03 16:19:14.377: V/MainActivity(2027): MyChildButton:dispatchTouchEvent = ACTION_DOWN
09-03 16:19:14.377: V/MainActivity(2027): MyChildButton:onTouchEvent = ACTION_DOWN
09-03 16:19:14.487: V/MainActivity(2027): MyChildButton:dispatchTouchEvent = ACTION_MOVE
09-03 16:19:14.487: V/MainActivity(2027): MyChildButton:onTouchEvent = ACTION_MOVE
09-03 16:19:14.507: V/MainActivity(2027): MyChildButton:dispatchTouchEvent = ACTION_MOVE
09-03 16:19:14.507: V/MainActivity(2027): MyChildButton:onTouchEvent = ACTION_MOVE
09-03 16:19:14.537: V/MainActivity(2027): MyChildButton:dispatchTouchEvent = ACTION_MOVE
09-03 16:19:14.537: V/MainActivity(2027): MyChildButton:onTouchEvent = ACTION_MOVE
09-03 16:19:14.567: V/MainActivity(2027): MyChildButton:dispatchTouchEvent = ACTION_MOVE
09-03 16:19:14.567: V/MainActivity(2027): MyChildButton:onTouchEvent = ACTION_MOVE
09-03 16:19:14.597: V/MainActivity(2027): MyChildButton:dispatchTouchEvent = ACTION_UP
09-03 16:19:14.597: V/MainActivity(2027): MyChildButton:onTouchEvent = ACTION_UP
就结果分析,首先是当前View(MyChildButton)的dispatchTouchEvent方法被调用,捕获了down事件,看来和我们之前说的没错,接着onTouchEvent方法也被打印,就结果我们开始分析源码,不带着大家兜圈子了,我们最终在基类View里面找到dispatchTouchEvent方法。代码如下:
/**
* 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);
}
可以发现代码不是很多,就几行,我们也需要找到我们需要的代码就行了,也就是说当我们点击按钮的时候最先被调用的就是当前方法,接下来一起看看这个方法,
首先在13行判断mOnTouchListener是否为null 并且判断当前View是否为enable的,默认情况下控件都是enable的,再者又判断mOnTouchListener.onTouch(this, event)它的返回值,如果两者都为true那么成立,首先看看mOnTouchListener是个什么鬼?继续查看View源码
private OnTouchListener mOnTouchListener;
/**
* 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 是View内部一个监听,赋值是在setOnTouchListener(OnTouchListener l)方法里面,很显然我们没有对其进行赋值,很显然这里的判断是不成立的,那么直接走
return onTouchEvent(event);
再来看看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) {
// 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)) {
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)) {
performClick();
}
}
}
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;
}
从代码可以看出,想比较前面的dispatchTouchEvent方法要多的多,但是没关系,我们只需要寻找我们需要的代码就行了
前面是一些初始化变量信息的部分不用看,直接看在第23行判断当前View是否可以点击或者长点击,很显然我们的按钮默认是肯定可以点击的,那么进入方法体内部,下面是一系列的action_up,action_move,action_down事件的判断,最后可以看到代码最终返回个true结束了,注意代码的括号。
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
......
......
......
break;
}
return true;
}
可以发现只要进入这个判断内部,那么最终一定会返回true回去,这里跟你提一点,在Android事件分发之中,必须要前一个action返回true情况下,后面action才会响应。返回true代表当然View将此次事件进行自己响应消费掉了,这个事件也就没有了,如果返回false代表对此次事件不响应也不消费,那么事件将会回传,一般是由父控件的onTouchEvent来接受。到这里相信你已经了解的差不多了对打印结果应该也没有什么迷惑 了,在我们点击按钮的时候控制台打印了,action_down和action_up,很显然方法最终走到onTouchEvent方法中,又判断控件是可点击的,紧接着一系列的各种action判断,如果有滑动操作,那么action_move事件将会被打印。
接着我们给我们的Button按钮注册一个点击事件如下:
public class MainActivity extends Activity {
private MyChildButton button;
public static final String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_main);
button = (MyChildButton)findViewById(R.id.button);
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
}
});
}
}
再看控制台打印结果:
09-03 17:10:55.557: V/MainActivity(12466): MyChildButton:dispatchTouchEvent = ACTION_DOWN
09-03 17:10:55.557: V/MainActivity(12466): MyChildButton:onTouchEvent = ACTION_DOWN
09-03 17:10:55.627: V/MainActivity(12466): MyChildButton:dispatchTouchEvent = ACTION_MOVE
09-03 17:10:55.627: V/MainActivity(12466): MyChildButton:onTouchEvent = ACTION_MOVE
09-03 17:10:55.637: V/MainActivity(12466): MyChildButton:dispatchTouchEvent = ACTION_MOVE
09-03 17:10:55.637: V/MainActivity(12466): MyChildButton:onTouchEvent = ACTION_MOVE
09-03 17:10:55.637: V/MainActivity(12466): MyChildButton:dispatchTouchEvent = ACTION_UP
09-03 17:10:55.637: V/MainActivity(12466): MyChildButton:onTouchEvent = ACTION_UP
09-03 17:10:55.647: D/MainActivity(12466): MainActivity onclick
在点击按钮的时候我滑了下屏幕所以有很多action_move事件打印,从打印结果中可以看到onclick事件是最后被打印的,也就是说在up之后,好大家先带着这个疑问继续向下看代码
回过头来在上面我们提到在dispatchTouchEvent方法中因为mOnTouchListener我们没有赋值,为空才走的判断把,大家还记得吧?那么现在问题来了,如果不为空呢?那么赋值又是在哪里赋值的呢?相信你应该也比较熟悉,看代码:
public class MainActivity extends Activity {
private MyChildButton button;
public static final String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_main);
button = (MyChildButton)findViewById(R.id.button);
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Log.d(TAG, "MainActivity onclick");
}
});
button.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d(TAG, "MainActivity onTouch");
return false;
}
});
}
}
可以看到在activity中我们设置touch事件,也对它进行赋值了,很显然这里new 的OnTouchEventListener匿名内部类传给了mOnTouchListener这个变量。这个方法也是用来处理事件的,那么onTouchEvent也是用来处理事件的,会不会多余呢?两个方法又有什么不同?其实从android设计框架来说,对于dispatchTouchEvent,TouchEvent这些方法是系统默认的,但是如果我们想建立自己的事件分发逻辑了,那么android设计者又给我们预留了OnTouchListener这个接口,让我们在这个方法中我们对事件的处理,从而不用系统的onTouchEvent方法。
这个时候可以发现,我已经在主activity中将OnTouchListener进行赋值了,下面看打印结果:
09-03 17:20:39.517: V/MainActivity(14023): MyChildButton:dispatchTouchEvent = ACTION_DOWN
09-03 17:20:39.517: D/MainActivity(14023): MainActivity Button onTouch
09-03 17:20:39.517: V/MainActivity(14023): MyChildButton:onTouchEvent = ACTION_DOWN
09-03 17:20:39.557: V/MainActivity(14023): MyChildButton:dispatchTouchEvent = ACTION_UP
09-03 17:20:39.557: D/MainActivity(14023): MainActivity Button onTouch
09-03 17:20:39.557: V/MainActivity(14023): MyChildButton:onTouchEvent = ACTION_UP
09-03 17:20:39.557: D/MainActivity(14023): MainActivity Button onclick
就打印结果来分析:
这时候我们在主页面给按钮设置了点击事件,也设置了setOnTouchListsner事件方法监听,
很显然Button的dispatchTouchEvent方法会被第一打印,接着可以看到activity里面设置的touch事件onTouch方法被打印,然后再打印了Button的TouchEvent方法,Button 单击事件最后打印,为什么会是这样现象?那么我们就来分析下代码:
首先在dispatchTouchEvent方法里面我们对其mOnTouchListener已经进行赋值了所以它不为null,然后看mOnTouchListener.onTouch(this, event)返回值,可以发现这里的onTouch方法优先于Button的onTouchEvent方法先执行,也就证明了打印结果。然后这里默认是返回false,那么也就是还是不成立,又会走入Button的onTouchEvent方法中,接着又和之前分析的一样了,走到这里还有个疑问,那就是Button的点击事件是什么时候调用的?虽然我们已经从打印结果看出是在action_up以后。
我们继续分析代码,这时候我把onTouch方法默认值改为true,
button.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d(TAG, "MainActivity Button onTouch");
return true;
}
});
这时候再看打印结果:
09-03 17:33:09.837: V/MainActivity(16708): MyChildButton:dispatchTouchEvent = ACTION_DOWN
09-03 17:33:09.837: D/MainActivity(16708): MainActivity Button onTouch
09-03 17:33:09.867: V/MainActivity(16708): MyChildButton:dispatchTouchEvent = ACTION_UP
09-03 17:33:09.867: D/MainActivity(16708): MainActivity Button onTouch
可以看到这时候点击事件没有了,这是怎么回事?
我们继续分析代码,当onTouch方法返回true的时候,这个if判断也就返回了true,这时候直接return true出去了,很显然后面的onTouchEvent方法就不在执行了,到这里位置我们可以结合打印结果不难猜出onclick方法是在onTouchEvent方法中调用的,下面我直接找到这里代码;
可以看到在Button的action_up的时候有个 performClick();方法
public boolean performClick() {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
if (mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
mOnClickListener.onClick(this);
return true;
}
return false;
}
一切都是那么清晰了,在Buttonaction_up的时候,会进入此方法,并且判断mOnClickListener 是否为null,如果不为null则执行 mOnClickListener.onClick(this);
/**
* 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;
}
很显然,这里的monClickListener正是我们在主页面调用setOnClickListener方法设置的,紧接着就回回调我们事先设置的onClick方法。到此结束,可以发现此方法里面会判断当前View是否可点击,因为在Android系统里面很多控件默认情况下是不可以点击的,比如ImageView,TextView,但是没事,如果不能点击系统会自动给你设置点击,也就是说只要设置了此方法,那么控件都是可以点击的。
到此为止,你应该有这样一个疑问,我们之前在onTouch方法中返回了false和返回了true,最后发现都能够响应按下,抬起,移动事件,按理说返回false应该是不响应事件的,又是为什么呢?接着上面,我们说如果onTouch方法返回了false,那么就会进行onTouchEvent方法判断之中,然后在23行判断控件是否可点击,如果可点击那么最后必定返回的是true,虽然我们在onTouch里面返回了false,但是系统最后还是给我们返回了true。
比如说这时候我将按钮换成ImageView,并且将onTouch方法返回false,这时候打印结果是:
09-03 17:58:25.197: D/MainActivity(22595): MyChildImageView:dispatchTouchEvent = ACTION_DOWN
09-03 17:58:25.197: D/MainActivity(22595): MainActivity imageView onTouch
09-03 17:58:25.197: D/MainActivity(22595): MyChildImageView:onTouchEvent = ACTION_DOWN
可以发现后面一系列的action都不会触发了,因为在在onTouchEvent方法中第23行判断当前View是否可点击,很显然图片默认是不可以点击的,这时候直接跳出去,直接返回了false。对于这样的现象你要么直接在onTouch方法中返回true,或者我们可以在布局文件中将图片的clickable属性设置为true也可以了,或者给图片设置上点击事件,因为在上面我们分析到,给控件设置click事件的时候如果该控件不可点击,那么系统会默认给你设置上可点击;
public class MainActivity extends Activity {
private MyChildImageView image;
public static final String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_main);
image = (MyChildImageView)findViewById(R.id.image);
image.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Log.d(TAG, "MainActivity imageView onclick");
}
});
image.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d(TAG, "MainActivity imageView onTouch");
return false;
}
});
}
}
或者
public class MainActivity extends Activity {
private MyChildImageView image;
public static final String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_main);
image = (MyChildImageView)findViewById(R.id.image);
image.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d(TAG, "MainActivity imageView onTouch");
return true;
}
});
}
}
打印结果:
09-03 18:05:05.267: D/MainActivity(24601): MyChildImageView:dispatchTouchEvent = ACTION_DOWN
09-03 18:05:05.267: D/MainActivity(24601): MainActivity imageView onTouch
09-03 18:05:05.307: D/MainActivity(24601): MyChildImageView:dispatchTouchEvent = ACTION_UP
09-03 18:05:05.307: D/MainActivity(24601): MainActivity imageView onTouch
至此关于View的事件分发可以得到以下几个结论:
关于onTouch和onTouchEvent区别,onTouch优先于onTouchEvent执行,如果在onTouch里面返回了true,那么onTouchEvent将不再执行,道理很简单事件讲究层级传替,在onTouch方法里被onTouch方法里被消费了也就没有了,自然不会再传替了,onTouchEvent得到执行的依据在于我们没有设置mOnTouchListener。
只要是可点击的事件那么他的dispatchTouchEvent必定返回true,那么一定就可以消费事件,对于不可点击的控件我们可以给它设置上clickable属性即可。
至此。View的事件分发也就结束了。