最近在开发中遇到view滑动冲突的问题,由于一开始就知道这个问题与view事件分发有关,之后在网上看了几篇关于事件分发的资料后,开发中遇到的问题很快便得到解决。
在这里总结一下我对view事件分发的理解。
首先,看下事件分发流程图:
Button事件演示
在对view的事件分发机制进行分析前,我们可以通过一个demo看看Button的事件处理的流程。
在布局文件中添加一个button控件,然后在代码中实现Button的setOnClickListener和setOnTouchListener方法,注册Click监听和Touch监听。
/** * button事件 */private void showButtonTouch() { mBtn = (Button) findViewById(R.id.btn); mBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Log.e(TAG, "Button onClick"); } }); mBtn.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_UP: Log.e(TAG, "Button onTouch " + "ACTION_UP"); break; case MotionEvent.ACTION_MOVE: Log.e(TAG, "Button onTouch " + "ACTION_MOVE"); break; case MotionEvent.ACTION_DOWN: Log.e(TAG, "Button onTouch " + "ACTION_DOWN"); break; } return false; } });}
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <Button android:id="@+id/btn" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Button事件测试"/></LinearLayout>
demo运行起来之后点击button,看下log日志(在点击时移动一下保证ACTION_MOVE事件能被触发):
在这里通过日志可以看出事件处理的流程。
View事件分发源码解析
在View的源码中,我们可以看到dispatchTouchEvent方法,这个方法可以理解为是View事件分发的入口。(代码基于2.2即API 8,建议大家在理解源码时不要看太高的版本,高版本源码会有过多的优化,会妨碍我们对于代码主要功能逻辑的理解)
dispatchTouchEvent方法解析
以下是View中dispatchTouchEvent方法代码:
public boolean dispatchTouchEvent(MotionEvent event) { if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && mOnTouchListener.onTouch(this, event)) { return true; } return onTouchEvent(event); }
在这整个方法中,事件分发的关键就在这个判断中 if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&mOnTouchListener.onTouch(this, event)) 。
如果这个判断为true,那么该方法返回true;否则,执行onTouchEvent()方法并根据onTouchEvent方法的返回值返回具体结果。
mOnTouchListener != null:是否为空,mOnTouchListener 对象就是我们通过mBtn.setOnTouchListener(new View.OnTouchListener() {})设置的,所以这里不为空。
(mViewFlags & ENABLED_MASK) == ENABLED:判断控件是否是enable,很显然为true。
mOnTouchListener.onTouch(this, event):最后就是判断onTouch方法中返回值是否为true,onTouch方法就是我们在Button控件注册Touch监听时@Override的onTouch方法。
在前面的demo中,因为mOnTouchListener.onTouch(this, event)方法的返回值为false,我们在log中看到button先执行touch事件在执行click事件。在这里我们将onTouch方法的返回值改为true,可以看到以下log:
在log中可以看到click方法没有被执行,这又是为什么呢?
其实在这里大家通过阅读dispatchTouchEvent方法代码可以想到,因为onTouch返回值为true,所以这个判断条件成立即dispatchTouchEvent方法返回true。if (onTouchEvent(event))->onTouchEvent方法没有被执行,会不会是由于这个原因导致click方法没有被执行呢? 很显然我们要看看onTouchEvent方法的源码。
onTouchEvent源码解析:
以下是onTouchEvent方法的源码:
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; }
对于onTouchEvent方法,主要的功能逻辑我们只需要从第17行代码开始阅读便可。
首先判断View是否是可点击的,因为Button默认是可点击的,所以这个条件成立,我们可以进入到switch分支判断中。在这里给大家留下一个疑问,如果这个条件不成立即就是控件是不可点击的,会出现什么样的情况呢?
MotionEvent.ACTION_UP:在这个分支判断中我们可以看到在43行有个performClick()方法,我们进入到这个方法体中可以看到:
public boolean performClick() { sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); if (mOnClickListener != null) { playSoundEffect(SoundEffectConstants.CLICK); mOnClickListener.onClick(this); return true; } return false; }
在该方法中可以看到mOnClickListener.onClick(this)被调用了,到此处我们可以确认Button的click事件就是在onTouchEvent方法中调用的。
TextView和Button比较
在onTouchEvent中曾留下一个疑问,如果判断控件是否可点击为false,会出现什么样的情况呢?
为了验证这个问题我们可以通过TextView和Button进行比较一下,因为TextView默认不可点击,Button默认可点击的。
首先在xml中添加TextView和Button,在代码中分别为他们注册setOnTouchListener监听,代码如下:
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <Button android:id="@+id/btn" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Button事件测试"/> <TextView android:id="@+id/tv" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@android:color/holo_green_light" android:text="TextView事件测试" /></LinearLayout>
/** * button事件 */private void showButtonTouch() { mBtn = (Button) findViewById(R.id.btn); mBtn.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_UP: Log.e(TAG, "Button onTouch " + "ACTION_UP"); break; case MotionEvent.ACTION_MOVE: Log.e(TAG, "Button onTouch " + "ACTION_MOVE"); break; case MotionEvent.ACTION_DOWN: Log.e(TAG, "Button onTouch " + "ACTION_DOWN"); break; } return false; } });}/** * TextView事件 */private void showTextTouch() { mTv = (TextView) findViewById(R.id.tv); mTv.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_UP: Log.e(TAG, "TextView onTouch " + "ACTION_UP"); break; case MotionEvent.ACTION_MOVE: Log.e(TAG, "TextView onTouch " + "ACTION_MOVE"); break; case MotionEvent.ACTION_DOWN: Log.e(TAG, "TextView onTouch " + "ACTION_DOWN"); break; } return false; } });}
因为我们需要进入到onTouchEvent方法中,所以必须让setOnTouchListener.onTouch()方法的返回值为false,在这里我们看看demo运行起来后分别点击Button和TextView打印的log:
发现Button的log和之前演示的一样,但是TextView在点击->移动->抬起的过程中只打印一次ACTION_DOWN的log,这是什么原因呢?
在之前的onTouchEvent方法中,我们可以看到:
- 当控件可点击时,方法会进入到switch分支判断中,当switch执行完成后在129行代码会 return true。
而当控件不可点击时,方法会直接到132行,return false。
这里需要指明onTouchEvent方法的返回值是touch事件层级传递的关键,说明如下:
- return true,那么表示该方法消费了此次事件,可以开始下一个事件
- return false,那么表示该方法并未处理完全,该事件仍然需要以某种方式传递下去继续等待处理。
现在就能解释上面TextView为什么只打印一句log了,因为onTouchEvent返回值为false,系统会认为ACTION_DOWN事件没有被执行完成,那么其他的touch事件就不能被触发了。
现在大家再去看事件分发流程图中的View部分想必就能看懂了吧!!!