想要搞定自定义View,就要先搞定事件分发机制,彻底搞懂事件分发机制,看本文就够了
前言
自定义View是用来衡量一个安卓开发者实力的重要指标之一,而如果搞懂事件分发机制,对于理解自定义View可以说是很大帮助,而且面试过程中事件分发机制也是高频出现,所以务必要搞懂这块知识点。
提示:以下是本篇文章正文内容
一、基本知识点
当我们用手点屏幕上的某一点时,整个事件从产生到一直传递是这样的过程:
手机屏幕此时导电传到传感器里,传感器将电转为电频,电频以波的形式传到电路板,电路板又传到Linux层,通过调用接口方法传到WindowManagerService,WindowManagerService再将这个x/y坐标封装成MotionEvent传到Activity里:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
在Activity的dispatchTouchEvent()方法里可以看到MotionEvent是通过phoneWindow对象从DecorView开始,然后到里面子控件一级一级往下传。
所以分发事件的组件,即分发事件者,包括Activity、ViewGroup和View:
不过这点击事件也不可能说一直传递啊,所以分两种传递情况,第一种是L型链传递:
L型链的特点是最终会有一个组件对事件进行消费(执行点击事件逻辑)。如果没有组件消费,最终事件会重回Activity,变成U型链,也就是第二种情况:
这两种情况就是事件分发时会出现的情况了,而事件分发次数也会根据不同的触发场景而不一样:
二、模拟事件分发过程
接下来会按照安卓的事件分发流程源码重新写一套实例流程代码去讲解安卓的事件分发机制,这样对于理解整个事件分发机制就事半功倍了。
首先定义MotionEvent类:
public class MotionEvent{
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;
private int actionMasked;
private int x;
private int y;
public MotionEvent() {
}
public MotionEvent(int x, int y) {
this.x = x;
this.y = y;
}
...
public void setActionMasked(int actionMasked) {
this.actionMasked = actionMasked;
}
}
里面封装的是x/y坐标,还有我们上面讲到的四种事件类型MotionEvent.ACTION_DOWN等,最后就是再定义actionMasked来记录当前事件类型。然后定义这些属性的setter和getter方法。这个类就是我们的点击事件类了。
然后我们再定义容器组件ViewGroup,在里面定义拦截事件方法和分发事件方法:
public class ViewGroup extends View {
...
public boolean onInterceptTouchEvent(MotionEvent event) {
return false;
}
public boolean dispatchTouchEvent(MotionEvent event) {
boolean handled = false;
boolean intercepted = onInterceptTouchEvent(event);
}
}
onInterceptTouchEvent()方法是拦截事件方法,表示该ViewGroup组件可以对传下来的事件进行拦截,返回true,则表示拦截,返回false则表示不拦截,dispatchTouchEvent()方法就是分发事件方法,我们在里面定义了hanled,用来记录当前事件是否要拦截,而这个值由onInterceptTouchEvent()方法来决定。如果要拦截的话,肯定就是执行自己的处理事件方法,也就是onTouchEvent()方法,这个方法我们在View里定义,因为ViewGroup属于容器类组件,负责分发和拦截事件,它真正的处理事件方法写在它继承的View里:
public class View {
...
private boolean onTouchEvent(MotionEvent event) {
if (onClickListener != null) {
onClickListener.onClick(this);
return true;
}
return false;
}
}
当事件通过onClickListener的onClick()方法消费后,就返回true,表示事件消费了,返回false则表示没有消费事件。现在ViewGroup的分发事件方法就可以这样写:
public class ViewGroup extends View {
...
public boolean onInterceptTouchEvent(MotionEvent event) {
return false;
}
public boolean dispatchTouchEvent(MotionEvent event) {
boolean handled = false;
boolean intercepted = onInterceptTouchEvent(event);
if(intercepted){
onTouchEvent(event);
}
}
}
这样写虽然可以,但我们从细节以及架构上方面来考虑,参照安卓的源码来写:
public class ViewGroup extends View {
...
public boolean onInterceptTouchEvent(MotionEvent event) {
return false;
}
public boolean dispatchTouchEvent(MotionEvent event) {
boolean handled = false;
boolean intercepted = onInterceptTouchEvent(event);
if(!intercepted){
//分发事件给子View
}
dispatchTransformedTouchEvent(event, null);
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, View child) {
if (child == null) {
super.dispatchTouchEvent(event);
}
}
}
不拦截就分发事件给子View,拦截则走dispatchTransformedTouchEvent()方法,传的child参数为空,所以走为空的逻辑,也就是调用父类的dispatchTouchEvent()方法。
其实这个dispatchTransformedTouchEvent()方法的作用有两个,第一个就是当传的child为空时,就表示当前容器组件拦截事件后要走自己的消费事件方法了。而另一个情况就是当传的child不为空的时候,就表示当前这个事件还是要走分发给子View的逻辑,不仅如此,同时还会在里面将这个事件的坐标值进行转化,也就是计算这个事件是属于哪个组件的区域,将它从原先所属的区域转为当前所传到的View区域里。
那现在传的child为空,因此就调用组件的父类一个dispatchTouchEvent()方法,这个方法是View里定义的(如果还是不理解为什么不在ViewGroup里定义处理事件方法的话,不妨这样去理解,容器用来分发和拦截,而非容器View则用来写处理事件的逻辑,容器刚好继承View,那这样写就自然能用到继承的优点):
public class View {
public boolean dispatchTouchEvent(MotionEvent event) {
onTouchEvent(event);
}
private boolean onTouchEvent(MotionEvent event) {
if (onClickListener != null) {
onClickListener.onClick(this);
return true;
}
return false;
}
}
这样就很好理解,真正处理的方法还是onTouchEvent()方法,而dispatchTouchEvent()是View的分发事件方法,只不过现在因为它被ViewGroup所继承,所以该方法可以作为View本身的分发事件方法,也可以是作为其他子控件的父类分发事件方法。
intercepted为true则表示拦截,走dispatchTransformedTouchEvent()方法,intercepted为false则不拦截,分发事件给子View,所以我们的ViewGroup就这样写:
...
public boolean dispatchTouchEvent(MotionEvent event) {
boolean handled = false;
boolean intercepted = onInterceptTouchEvent(event);
int actionMasked = event.getActionMasked();
if (actionMasked != MotionEvent.ACTION_CANCEL && !intercepted) {
//遍历每个子控件
for (int i = children.length-1; i >= 0; i--) {
View child = mChildren[i];
// 分发事件给子View
}
}
dispatchTransformedTouchEvent(event, null);
}
除了要判断该事件不被拦截(!intercepted)之外,还要判断当前这个事件不为取消事件MotionEvent.ACTION_CANCEL才行,然后就是遍历该GroupView里的所有子控件,这里遍历是从最后一个被添加进GroupView的子控件先开始遍历先,因为后面添加进的子view会覆盖先添加的子view的:
我们还要判断这个事件是不是在被遍历的子控件里,所以在View类里定义边界值属性以及事件是否在控件里的方法:
public class View {
private int left;
private int top;
private int right;
private int bottom;
...
public boolean isContained(int x, int y) {
if (x >= left && x < right && y >= top && y < bottom) {
return true;
}
return false;
}
...
实际上很好理解,就是通过点击事件MotionEvent的x、y坐标值比较它们是不是同时都在四个边界值之内:
...
public boolean dispatchTouchEvent(MotionEvent event) {
boolean handled = false;
boolean intercepted = onInterceptTouchEvent(event);
int actionMasked = event.getActionMasked();
if (actionMasked != MotionEvent.ACTION_CANCEL && !intercepted) {
//遍历每个子控件
for (int i = children.length-1; i >= 0; i--) {
View child = mChildren[i];
if (!child.isContainer(event.getX(), event.getY())) {
continue;
}
// 分发事件给子View
dispatchTransformedTouchEvent(event, child);
}
}
dispatchTransformedTouchEvent(event, null);
}
当事件是在子View里,那这时候就可以调用dispatchTransformedTouchEvent()方法,这次传进去的child就不再是null了,所以来完善一下dispatchTransformedTouchEvent()方法:
private boolean dispatchTransformedTouchEvent(MotionEvent event, View child) {
boolean handled = false;
if (child != null) {
handled = child.dispatchTouchEvent(event);
}else {
handled = super.dispatchTouchEvent(event);
}
return handled;
}
child不为空,就调用子View本身的dispatchTouchEvent()方法,因为来完善一下View里的dispatchTouchEvent()方法:
public class View {
private int left;
private int top;
private int right;
private int bottom;
...
public boolean isContained(int x, int y) {
if (x >= left && x < right && y >= top && y < bottom) {
return true;
}
return false;
}
public boolean dispatchTouchEvent(MotionEvent event) {
//事件有无消费
boolean result = false;
//onTouchListener.onTouch
if (onTouchListener != null&& onTouchListener.onTouch(this, event)) {
result = true;
}
//onClickListener.onClick
if(!result&& onTouchEvent(event)){
result = true;
}
return result;
}
private boolean onTouchEvent(MotionEvent event) {
if (onClickListener != null) {
onClickListener.onClick(this);
return true;
}
return false;
}
}
现在我们的dispatchTouchEvent()方法里会多了onTouchListener.onTouch()方法去先消费它,如果它消费了,就直接返回true,表示View已经消费了事件,如果onTouch没消费,则再走onClickListener.onClick方法去消费它,而这里的onClickListener.onClick是封装在onTouchEvent()方法里,最后也是根据有无消费而返回true或false。这也是参考源码而写的,View是持有这些消费事件的接口来给外界回调的:
public interface OnClickListener {
void onClick(View v);
}
public interface OnTouchListener {
boolean onTouch(View v, MotionEvent event);
}
...
再次返回到ViewGroup完善我们的dispatchTouchEvent()方法:
...
public boolean dispatchTouchEvent(MotionEvent event) {
boolean handled = false;
boolean intercepted = onInterceptTouchEvent(event);
int actionMasked = event.getActionMasked();
if (actionMasked != MotionEvent.ACTION_CANCEL && !intercepted) {
//遍历每个子控件
for (int i = children.length-1; i >= 0; i--) {
View child = mChildren[i];
if (!child.isContainer(event.getX(), event.getY())) {
continue;
}
// 分发事件给子View
if (dispatchTransformedTouchEvent(event, child)) {
handled = true;
break;
}
}
}
if(intercepted){
handled = dispatchTransformedTouchEvent(event, null);
}
return handled;
}
当dispatchTransformedTouchEvent()方法返回true时,这是容器里肯定是有子控件消费了事件,所以把handled设为true,而既然有子控件消费了事件了,那就不用再遍历之后的子控件了,返回结果handled为true。这就是actionMasked != MotionEvent.ACTION_CANCEL && !intercepted都成立时的逻辑,那么如果ViewGroup自己拦截事件则走intercepted为true的逻辑,也就是调用dispatchTransformedTouchEvent()方法,child参数要传null。最后两种情况都走完之后,返回最终的handled结果。
我们现在这样想,当事件从Activity传到顶层ViewGroup时,这时候走到遍历它所有子控件这一步,然后判断事件是否处在这个子View里,如果是则调用child.dispatchTouchEvent(event),那如果这个子View本身也是个容器ViewGroup呢,那这个dispatchTouchEvent()此时是ViewGroup的dispatchTouchEvent()方法,还是View的dispatchTouchEvent()方法?其实是ViewGroup的dispatchTouchEvent()方法。我们再来看看ViewGroup的dispatchTouchEvent()方法:
private boolean dispatchTransformedTouchEvent(MotionEvent event, View child) {
boolean handled = false;
if (child != null) {
handled = child.dispatchTouchEvent(event);
}else {
handled = super.dispatchTouchEvent(event);
}
return handled;
}
现在顶层ViewGroup的子View在调用dispatchTransformedTouchEvent(),因此肯定是走child.dispatchTouchEvent(event),而这个子View是个容器ViewGroup时,它还是走child.dispatchTouchEvent(event)方法,那么也就是相当于执行这个此时是作为顶层ViewGroup的子类View(但它还是child来的,那就走它本身的dispatchTransformedTouchEvent()方法,而它本身是ViewGroup,那就走ViewGroup的dispatchTransformedTouchEvent()方法,也就是再走一次:
private boolean dispatchTransformedTouchEvent(MotionEvent event, View child) {
boolean handled = false;
if (child != null) {
handled = child.dispatchTouchEvent(event);
}else {
handled = super.dispatchTouchEvent(event);
}
return handled;
}
但此时要记住,顶层ViewGroup的dispatchTransformedTouchEvent方法还没走完呢,它现在要等它这个子View的ViewGroup控件走完它自己的dispatchTransformedTouchEvent()方法才能继续执行,然后又是一样的逻辑,一直到最后那个不是容器的子View执行它的onTouchEvent方法是否消费事件,然后再一层一层把处理事件结果返回给父组件。这个过程就是一个递归行为:
一开始从ViewGroup执行它的dispatchTouchEvent()方法开始把事件往下传,最后传到View去处理,然后把处理结果往上递归:
所以所谓的U型链并不是说真的是View调完onTouchEvent()方法后就紧接着调用ViewGroup的onTouchEvent()方法,而是View调完onTouchEvent()方法后会递归回退到ViewGroup的dispatchTouchEvent()方法,然后在ViewGroup的dispatchTouchEvent()方法里,继续执行ViewGroup的onTouchEvent()方法,执行完后又递归回Activity的dispatchTouchEvent()方法里,以此类推。
平时我们没有为一个控件设置点击事件时,你用手点,屏幕没有任何反应,那是因为我们的事件传到这个控件里的onTouchEvent()方法里,没有作任何处理,因此返回false,而就这样又一层层往上传递给Activity,最后返回false,再次看看View的onTouchEvent()方法你就明白了:
public class View {
...
public boolean dispatchTouchEvent(MotionEvent event) {
//事件有无消费
boolean result = false;
//onTouchListener.onTouch
if (onTouchListener != null&& onTouchListener.onTouch(this, event)) {
result = true;
}
//onClickListener.onClick
if(!result&& onTouchEvent(event)){
result = true;
}
return result;
}
private boolean onTouchEvent(MotionEvent event) {
if (onClickListener != null) {
onClickListener.onClick(this);
return true;
}
return false;
}
}
不过这里还有一个知识点要讲的,在源码中可以看到:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
...
// 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;
}
...
//我们自定义的那套代码里没有这段
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
...
}
...
这里已为大家列出最关键的代码,并且可以看出我们那套代码跟源码的区别在于,源码这里可以看到出现了一个mFirstTouchTarget对象,它表示第一个消费了事件的view,它还有个作用就是能让传递move事件时能快速定位到是哪个View在接收和处理这个事件,大家应该还记得我们在分发事件时要遍历每个控件的:
...
//遍历每个子控件
for (int i = children.length-1; i >= 0; i--) {
View child = mChildren[i];
if (!child.isContainer(event.getX(), event.getY())) {
continue;
}
// 分发事件给子View
if (dispatchTransformedTouchEvent(event, child)) {
handled = true;
break;
}
}
...
那么如果这个事件是move事件时,难道还要这样遍历每个控件吗,因为我们知道当手指触摸一个控件时,然后拖着它移动,这个过程是会产生down事件和一系列(多个)move事件,那么这些move事件肯定还是属于这个控件的,所以我们就不用再遍历控件这么耗损性能,直接继续让move事件分发给这个控件便可,再看回安卓源码是这样执行的:
...
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
...
当我们第一次是down事件,mFirstTouchTarget肯定为空,所以就执行dispatchTransformedTouchEvent()方法,继续分发事件和处理事件,如若mFirstTouchTarget不为空,则此时肯定是已经产生了down事件,不是新的事件,那这里我们可以以move事件为例,当我们点击某个组件然后拖动它移动,那么此时down事件产生后,就是产生一系列move事件,那么此时就走else块里的逻辑,首先看看这个TouchTarget类:
private static final class TouchTarget {
private static final int MAX_RECYCLED = 32;
private static final Object sRecycleLock = new Object[0];
private static TouchTarget sRecycleBin;
private static int sRecycledCount;
public static final int ALL_POINTER_IDS = -1; // all ones
// The touched child view.
public View child;
// The combined bit mask of pointer ids for all pointers captured by the target.
public int pointerIdBits;
// The next target in the target list.
public TouchTarget next;
...
}
它里面又会有TouchTarget变量,然后还有View变量,很明显它是一个链表结构,从链中取对象的obtain方法如下:
private static final class TouchTarget {
...
public static TouchTarget obtain(@NonNull View child, int pointerIdBits) {
if (child == null) {
throw new IllegalArgumentException("child must be non-null");
}
final TouchTarget target;
synchronized (sRecycleLock) {
if (sRecycleBin == null) {
target = new TouchTarget();
} else {
target = sRecycleBin;
sRecycleBin = target.next;
sRecycledCount--;
target.next = null;
}
}
target.child = child;
target.pointerIdBits = pointerIdBits;
return target;
}
...
}
当一个控件一旦产生down事件后被控件执行onTouchEvent()方法完后,会调用addTouchTarget()方法将该组件缓存在里面:
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
然后当产生move事件时,就会从该链表TouchTarget直接取里面的控件View,进行dispatchTransformedTouchEvent()方法然后进而调用onTouchEvent方法,不用再走遍历所有子控件那一步了。
那我们也来模拟一下源码,自定义一个TouchTarget类:
private static final class TouchTarget {
public View child;//缓存的View
//回收池
private static TouchTarget sRecycleBin;
private static final Object sRecycleLock = new Object[0];//锁对象
public TouchTarget next;
private static int sRecycledCount;
}
同样给TouchTarget使用缓存区(回收池(源码采用单向链表))来进行对象管理,防止频繁new对象引起内存抖动而造成卡顿。接着就是从链表去取view控件了:
private static final class TouchTarget {
public View child;//缓存的View
//回收池
private static TouchTarget sRecycleBin;//头部
private static final Object sRecycleLock = new Object[0];//锁对象
public TouchTarget next;
private static int sRecycledCount;
public static TouchTarget obtain(View child) {
TouchTarget target;
synchronized (sRecycleLock) {
if (sRecycleBin == null) {
target = new TouchTarget();
}else {
target = sRecycleBin;
}
}
}
}
先构造链表对象:
当前这个view被取走后,记得把sRecycleBin头部引用重新指向下一个元素:
private static final class TouchTarget {
...
public static TouchTarget obtain(View child) {
TouchTarget target;
synchronized (sRecycleLock) {
if (sRecycleBin == null) {
target = new TouchTarget();
}else {
target = sRecycleBin;
}
sRecycleBin = target.next;
sRecycledCount--;
target.next = null;
}
target.child = child;
return target;
}
}
sRecycleBin头部引用指向变成要被取出来的当前这个TouchTarget对象的下一个对象后,当前TouchTarget对象的下一个指向变成空,它里面的存储的view对象就变成外界要指定缓存的view对象,然后单链表sRecycledCount也要自减1,最后把当前这个TouchTarget对象返回给调用方:
调用方怎么去构造,模仿源码那样,在ViewGroup里定义这个方法:
...
private TouchTarget addTouchTarget(View child) {
final TouchTarget target = TouchTarget.obtain(child);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
...
那现在我们就可以在ViewGroup里遍历子View时也就是在down事件产生后有子View对它处理后,就调用addTouchTarget()去保存这个处理过down事件的子View:
...
TouchTarget firstTouchTarget = null;
int actionMasked = event.getActionMasked();
if (actionMasked != MotionEvent.ACTION_CANCEL && !intercepted) {
//Down事件
if (actionMasked == MotionEvent.ACTION_DOWN) {
final View[] children = mChildren;
//遍历耗时
for (int i = children.length-1; i >= 0; i--) {
View child = mChildren[i];
if (!child.isContainer(event.getX(), event.getY())) {
continue;
}
//child
if (dispatchTransformedTouchEvent(event, child)) {
handled = true;
firstTouchTarget = addTouchTarget(child);
break;
}
}
}
...
这样把子View添加到mFirstTouchTarget里面去,这样当产生move事件时,就会去判断mFirstTouchTarget是否为空,不为空则去ToucTarget里面取出这个控件去处理move事件,从而不用走for循环了:
...
TouchTarget firstTouchTarget = null;
int actionMasked = event.getActionMasked();
if (actionMasked != MotionEvent.ACTION_CANCEL && !intercepted) {
//Down事件
if (actionMasked == MotionEvent.ACTION_DOWN) {
final View[] children = mChildren;
//遍历耗时
for (int i = children.length-1; i >= 0; i--) {
View child = mChildren[i];
if (!child.isContainer(event.getX(), event.getY())) {
continue;
}
//child
if (dispatchTransformedTouchEvent(event, child)) {
handled = true;
firstTouchTarget = addTouchTarget(child);
break;
}
}
}
if(mFirstTouchTarget==null) {
handled = dispatchTransformedTouchEvent(event, null);
}else{
//则从mFirstTouchTarget里取出view,然后将move事件继续分发给它处理
}
}
...
跟源码一样的思路,这样大家去看源码的时候就可以以这种思路去看,这样就轻松很多,而且我们也可以看出只有这个down事件也就是第一次产生事件时,才要这么消耗性能遍历每个控件,一旦有控件消费了事件,就会把这个控件缓存在单链表中。整个安卓的事件分发机制基本上就是这样,想要以上这个模拟过程的代码可以关我公号领取:Pingred