学而时习之,不亦说乎。学过的东西常温习、实践,并领略其中的深意,才能变得越发专业。今天来温习一下Android的事件分发机制。
一、事件分发机制原理
如下图所示:
三层View(包括Activity),从外到内依次为: EventActivity、ViewGroupA、ViewB。
之前总结的如下结论,可以根据代码具体验证下:
/**
* 事件分发机制基本原理概述:
* 顶层View先逐层分发事件、再逐层选择消费事件、如果最底层View未消费事件,则从最底层开始逐步上报,最终可形成一个循环事件链路.
*
* 具体事件分发流程如下:
* --EventActivity 调用dispatchTouchEvent分发事件(if返回true,该事件分发到此结束) if返回false(默认)--继续往下分发事件至ViewGroupA
* --ViewGroupA 调用dispatchTouchEvent分发事件(if返回true,该事件分发到此结束) if返回false(默认)--调用onInterceptTouchEvent事件(if返回true,则调用自己的onTouchEvent来进行消费) if返回false--继续往下分发事件至ViewB
* --ViewB 调用dispatchTouchEvent分发事件(if返回true,该事件分发到此结束) if返回false(默认)--调用onTouchEvent事件(如果返回true,则在此 ViewB 消费该事件)if返回false--开始上报至ViewGroupA
* --ViewGroupA 中调用onTouchEvent事件(if返回true,则在此 ViewGroupA 消费该事件) if返回false--上报至EventActivity
* --EventActivity 中调用onTouchEvent事件,无论返回true or false都最终由该 EventActivity 消费该事件。
*
* 遍历是为了寻找真正消费事件的View(ViewGroup):
* 当EventActivity收到Touch事件时,将遍历子View进行Down事件的分发,ViewGroup的遍历可以看成是递归的。分发的目的是为了能够真正找到要消费该事件的View。
* 当已经找到消费该事件的ViewGroup之后,该ViewGroup的子View将不会再收到Down事件的触发,即递归遍历只走一遍,找到能消费此事件的ViewGroup(or View)为止。
*
* 分发时onInterceptTouchEvent事件并不一定会调用:
* 当已经确认(如ViewGroupA)已经拦截并消费了该事件,那么以后不会在调用其onInterceptTouchEvent方法(在已经拦截并消费的情况下,就没有必要每次都询问了),但是dispatchTouchEvent无论如何是每次都需要调用的。
*
*/
源码(返回值均是默认值)如下:
EventActivity源码示例:
public class EventActivity extends Activity {
private static final String TAG = "EventActivity";
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
this.getWindow().getDecorView().setBackgroundColor(Color.YELLOW);
setContentView(R.layout.main);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
Logger.d(TAG + "--dispatchTouchEvent--DOWN");
break;
case MotionEvent.ACTION_MOVE:
Logger.d(TAG + "--dispatchTouchEvent--MOVE");
break;
case MotionEvent.ACTION_UP:
Logger.d(TAG + "--dispatchTouchEvent--UP");
break;
default:
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public void onUserInteraction() {
super.onUserInteraction();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
Logger.d(TAG + "--onTouchEvent--DOWN");
break;
case MotionEvent.ACTION_MOVE:
Logger.d(TAG + "--onTouchEvent--MOVE");
break;
case MotionEvent.ACTION_UP:
Logger.d(TAG + "--onTouchEvent--UP");
break;
default:
break;
}
return super.onTouchEvent(event);
}
}
ViewGroupA源码示例:
package com.xiaoqlu.motionevent.touchview;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.LinearLayout;
import com.xiaoqlu.motionevent.util.Logger;
/**
* @author hongri
*/
public class ViewGroupA extends LinearLayout {
private final String TAG = "ViewGroupA";
public ViewGroupA(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
Logger.d(TAG + "--dispatchTouchEvent--DOWN");
break;
case MotionEvent.ACTION_MOVE:
Logger.d(TAG + "--dispatchTouchEvent--MOVE");
break;
case MotionEvent.ACTION_UP:
Logger.d(TAG + "--dispatchTouchEvent--UP");
break;
default:
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
Logger.d(TAG + "--onInterceptTouchEvent--DOWN");
break;
case MotionEvent.ACTION_MOVE:
Logger.d(TAG + "--onInterceptTouchEvent--MOVE");
break;
case MotionEvent.ACTION_UP:
Logger.d(TAG + "--onInterceptTouchEvent--UP");
break;
default:
break;
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
Logger.d(TAG + "--onTouchEvent--DOWN");
break;
case MotionEvent.ACTION_MOVE:
Logger.d(TAG + "--onTouchEvent--MOVE");
break;
case MotionEvent.ACTION_UP:
Logger.d(TAG + "--onTouchEvent--UP");
break;
default:
break;
}
return super.onTouchEvent(ev);
}
}
ViewB源码示例:
package com.xiaoqlu.motionevent.touchview;
import android.content.Context;
import android.support.v7.widget.AppCompatTextView;
import android.util.AttributeSet;
import android.view.MotionEvent;
import com.xiaoqlu.motionevent.util.Logger;
/**
* @author hongri
*/
public class ViewB extends AppCompatTextView {
private final String TAG = "ViewB";
public ViewB(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
Logger.d(TAG + "--dispatchTouchEvent--DOWN");
break;
case MotionEvent.ACTION_MOVE:
Logger.d(TAG + "--dispatchTouchEvent--MOVE");
break;
case MotionEvent.ACTION_UP:
Logger.d(TAG + "--dispatchTouchEvent--UP");
break;
default:
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
Logger.d(TAG + "--onTouchEvent--DOWN");
break;
case MotionEvent.ACTION_MOVE:
Logger.d(TAG + "--onTouchEvent--MOVE");
break;
case MotionEvent.ACTION_UP:
Logger.d(TAG + "--onTouchEvent--UP");
break;
default:
break;
}
return super.onTouchEvent(ev);
}
}
日志如下:
通过查看源码与Log对比下上面的结论,是不是已经理解了呢。
二、解决滑动事件冲突
我们都知道解决滑动冲突的方式主要有两种:
1、外部拦截法:
外部拦截法重写父容器的onInterceptTouchEvent方法,在内部做相应的拦截即可,如上事件分析中已做介绍,这里不再说明。
2、内部拦截法:
内部拦截法,是指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交由父容器进行处理,需要配合 requestDisallowInterceptTouchEvent 方法才能正常工作。
现在我们修改ViewGroupA代码如下:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
Logger.d(TAG + "--onInterceptTouchEvent--DOWN");
break;
case MotionEvent.ACTION_MOVE:
Logger.d(TAG + "--onInterceptTouchEvent--MOVE");
break;
case MotionEvent.ACTION_UP:
Logger.d(TAG + "--onInterceptTouchEvent--UP");
break;
default:
break;
}
//此处有修改
if (action == MotionEvent.ACTION_DOWN) {
return super.onInterceptTouchEvent(ev);
} else {
return true;
}
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
Logger.d(TAG + "--onTouchEvent--DOWN");
break;
case MotionEvent.ACTION_MOVE:
Logger.d(TAG + "--onTouchEvent--MOVE");
break;
case MotionEvent.ACTION_UP:
Logger.d(TAG + "--onTouchEvent--UP");
break;
default:
break;
}
//此处有修改
return true;
}
现在我们修改ViewB代码如下:
@Override
public boolean onTouchEvent(MotionEvent ev) {
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
Logger.d(TAG + "--onTouchEvent--DOWN");
mLastTouchY = ev.getY();
//此处有修改
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
Logger.d(TAG + "--onTouchEvent--MOVE");
mDragDistance = ev.getY() - mLastTouchY;
//此处有修改
/**
* 当Y轴滑动距离大于20px时,由父类拦截事件
*/
if (Math.abs(mDragDistance) > 20) {
Logger.d(TAG + "--允许父类拦截--");
getParent().requestDisallowInterceptTouchEvent(false);
} else {
getParent().requestDisallowInterceptTouchEvent(true);
}
break;
case MotionEvent.ACTION_UP:
Logger.d(TAG + "--onTouchEvent--UP");
break;
default:
break;
}
return true;
}
以上修改表示:当滑动Y轴距离小于20px的时候,子ViewB消费该事件,当滑动Y轴距离大于20px的时候,父ViewGroupA消费该事件(此时的核心点就是需要将 requestDisallowInterceptTouchEvent 置位false,即表示允许父类拦截该事件)。
打印日志如下,是不是很清晰呢。
需要源码对其他场景做测试的可以直接去github主页下载源码:查看源码
三、典型问题记录:
Q:对常用的Touch或click事件进行一个优先级排序?
A:优先级:onTouchListener > onTouchEvent > onLongClickListener > onClickListener。
Q:onTouch和onTouchEvent有什么区别,又该如何使用?
A:这两个方法都是在View的dispatchTouchEvent中调用的,onTouch优先于onTouchEvent执行。如果在onTouch方法中通过返回true将事件消费掉,onTouchEvent将不会再执行。
另外,onTouch能够得到执行需要两个前提条件,第一mOnTouchListener的值不能为空,第二当前点击的控件必须是enable的。因此如果你有一个控件是非enable的,那么给它注册onTouch事件将永远得不到执行。对于这一类控件,如果我们想要监听它的touch事件,就必须通过在该控件中重写onTouchEvent方法来实现。
Q:requestDisallowInterceptTouchEvent 可以在子元素中干扰父元素的事件分发吗?如果可以,是全部都可以干扰吗?
A:肯定可以,但是down事件干扰不了。
Q:一旦有事件传递给View,View的onTouchEvent一定会被调用吗?
A:View没有onInterceptTouchEvent方法,一旦有事件传递给View,View的onTouchEvent就一定会被调用。
Q:dispatchTouchEvent每次都会被调用吗?
A:是的,onInterceptTouchEvent则不会。
Q:ACTION_CANCEL 事件触发时机?
A:ACTION_CANCEL 事件是收到前驱事件后,后续事件被父控件拦截的情况下产生,onTouchEvent的事件回传到父控件只会发生在ACTION_DOWN事件中。
Q:父亲Down事件拦截后,还能传递给子View吗?
A:不能
Q:requestDisallowInterceptTouchEvent 发生在 onInterceptTouchEvent 之前还是之后?
A:之前
在 DispatchTouchEvent 源码中,有如下代码,即可得出答案 (查看):
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
参考: