上一章说了view的触摸事件的传递机制,这一章就讲讲ViewGrou的事件传递机制,ViewGroup 是 View 的子类,也就是说View包含的功能,ViewGroup 都有,并且做了相应的修改和扩展。
ViewGroup 比着 View 多了一个方法 onInterceptTouchEvent(MotionEvent event)。我们平时的焦点触摸事件,都是通过ViewGroup容器传给View的,比较重要的方法有三个,入口也是dispatchTouchEvent(MotionEvent event),用于事件的分发,不管是ViewGroup还是View; onInterceptTouchEvent(MotionEvent event) 这个方法是拦截事件的方法,根据它返回的值决定是否往下一层view中分发,默认是false,不会拦截,如果为true,则不会传递给子view,会把焦点事件由自身处理。view中是没有这个方法的;onTouchEvent(MotionEvent event)这个是处理事件的方法,它在 dispatchTouchEvent() 方法里调用,如果返回true则表示消耗当前事件,如果返回false则是不消耗当前事件。这三个方法的简单关系可以理解如下
public boolean dispatchTouchEvent(MotionEvent event){
boolean handle = false;
if(onInterceptTouchEvent(event)){
handle = onTouchEvent(event);
}else{
handle = child.dispatchTouchEvent(event);
// if(!handle){
// handle = onTouchEvent(event); // 这一部分是隐藏的,为了理解添加的,如果扰乱了思路,可以忽略这部分
// }
}
return handle;
}
通俗来说,从它的上一层容器也就是父View中接收到事件,也就是 dispatchTouchEvent(MotionEvent event) 方法为入口,然后再入口中会先判断 onInterceptTouchEvent(MotionEventevent) 中返回的值,如果是true,标识拦截了该事件,会把它交给自身的 onTouchEvent(MotionEvent event) 来处理,onTouchEvent(MotionEvent event) 中返回的值即是dispatchTouchEvent 方法返回的值;如果 onInterceptTouchEvent 方法返回为false,标识不拦截事件,会把它传递给子类view,这里就是上一章中view接收事件的入口,根据子view返回的值决定下一步,如果子view返回的是true,说明消费了,此时它的值就是 dispatchTouchEvent 方法的值,如果子view返回为false,则会把事件交给自身的 onTouchEvent(MotionEvent event) 事件来处理,大体上是这样,但还有一些细节需要处理。 写了个例子
public class TouTestFrameLayout extends FrameLayout {
private final static String TAG = "TouTestView";
public TouTestFrameLayout(Context context) {
this(context, null);
}
public TouTestFrameLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public TouTestFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initLister();
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.e(TAG, "TouTestFrameLayout dispatchTouchEvent: " + event.getAction());
return super.dispatchTouchEvent(event);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
Log.e(TAG, "TouTestFrameLayout onInterceptTouchEvent: "+ event.getAction());
return super.onInterceptTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e(TAG, "TouTestFrameLayout onTouchEvent: " + event.getAction());
return super.onTouchEvent(event);
}
private void initLister() {
setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.e(TAG, "TouTestFrameLayout onTouchListener: " + event.getAction() + " " + isClickable() +" " + isEnabled());
return false;
}
});
}
}
public class TouTestView extends View {
private final static String TAG = "TouTestView";
public TouTestView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public TouTestView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.e(TAG, "dispatchTouchEvent: " + event.getAction());
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e(TAG, "onTouchEvent: " + event.getAction());
return super.onTouchEvent(event);
}
}
android:layout_height="150dp"
android:background="@color/main_red_day">
android:layout_height="100dp"
android:background="@color/cardview_dark_background" />
TouTestView 为上一章开头的自定义的布局,我们使用没有添加点击事件和长按点击事件的view,只保留 dispatchTouchEvent 和 onTouchEvent 方法,我们的布局中也可以看出来,TouTestFrameLayout 比着 TouTestView 大,有一部分空余的布局,我们试着点击空余部分和滑动,看一下打印的日志,发现都是一样的
E/TouTestView: TouTestFrameLayout dispatchTouchEvent: 0
E/TouTestView: TouTestFrameLayout onInterceptTouchEvent: 0
E/TouTestView: TouTestFrameLayout onTouchEvent: 0
onInterceptTouchEvent(MotionEvent event) 默认为false,这也验证了上一章的内容,ACTION_DOWN 时,dispatchTouchEvent 方法收到的值为false,事件只触发一次,后面的ACTION_MOVE 和 ACTION_UP 没有触发。如果加入点击事件或者把 onTouch(View v, MotionEvent event) 或 onTouchEvent(MotionEvent event) 返回值为true,则有后续的触摸事件,这一点上,ViewGroup 和 View 是一样的。
我们在如果在 TouTestView 点击一下,或者滑动一下,打印的日志一样
E/TouTestView: TouTestFrameLayout dispatchTouchEvent: 0
E/TouTestView: TouTestFrameLayout onInterceptTouchEvent: 0
E/TouTestView: dispatchTouchEvent: 0
E/TouTestView: onTouchEvent: 0
E/TouTestView: TouTestFrameLayout onTouchEvent: 0
由于 TouTestView 的 onTouchEvent 方法返回为false,所以最终把事件交给了 TouTestFrameLayout 的 onTouchEvent 方法,这个与上面咱们的分析也能对上。如果给TouTestView添加一个 setOnClickListener 点击事件的回调,然后在 TouTestView 点击一下,打印的日志如下
E/TouTestView: TouTestFrameLayout dispatchTouchEvent: 0
E/TouTestView: TouTestFrameLayout onInterceptTouchEvent: 0
E/TouTestView: dispatchTouchEvent: 0
E/TouTestView: onTouchEvent: 0
E/TouTestView: TouTestFrameLayout dispatchTouchEvent: 1
E/TouTestView: TouTestFrameLayout onInterceptTouchEvent: 1
E/TouTestView: dispatchTouchEvent: 1
E/TouTestView: onTouchEvent: 1
滑动一下,
E/TouTestView: TouTestFrameLayout dispatchTouchEvent: 0
E/TouTestView: TouTestFrameLayout onInterceptTouchEvent: 0
E/TouTestView: dispatchTouchEvent: 0
E/TouTestView: onTouchEvent: 0
E/TouTestView: TouTestFrameLayout dispatchTouchEvent: 2
E/TouTestView: TouTestFrameLayout onInterceptTouchEvent: 2
E/TouTestView: dispatchTouchEvent: 2
E/TouTestView: onTouchEvent: 2
E/TouTestView: TouTestFrameLayout dispatchTouchEvent: 2
E/TouTestView: TouTestFrameLayout onInterceptTouchEvent: 2
E/TouTestView: dispatchTouchEvent: 2
E/TouTestView: onTouchEvent: 2
E/TouTestView: TouTestFrameLayout dispatchTouchEvent: 1
E/TouTestView: TouTestFrameLayout onInterceptTouchEvent: 1
E/TouTestView: dispatchTouchEvent: 1
E/TouTestView: onTouchEvent: 1
从上面打印的日志,能验证我们最上面简化的那个方法代码,同时也验证了,如果down的时候,子view没有返回true,则没有了move和up的后续事件的触发,注意 onInterceptTouchEvent方法,如果down的时候子view为false,则 onInterceptTouchEvent 方法只会触发一次;如果子view返回为true,则 onInterceptTouchEvent 方法在move和up都会调用。如果把
TouTestFrameLayout 的 onInterceptTouchEvent(MotionEvent event) 方法修改为 return true,我们再次点击 TouTestView 试试
E/TouTestView: TouTestFrameLayout dispatchTouchEvent: 0
E/TouTestView: TouTestFrameLayout onInterceptTouchEvent: 0
E/TouTestView: TouTestFrameLayout onTouchEvent: 0
我们发现,一旦拦截了,本身的 onTouchEvent(MotionEvent event) 返回的是false,导致后续 move 和 up 都没有传递进去,这时候,从焦点事件传递来看,ViewGroup和View是一样的。有兴趣的可以给 TouTestFrameLayout 再添加个 setOnClickListener 点击事件试试日志。下面开始分析ViewGroup的事件传递源码
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
onInterceptTouchEvent(MotionEvent ev) 的默认返回值是false, ViewGroup 中没有重写 onTouchEvent(MotionEvent event) 方法,它用的是父类 View 的 onTouchEvent(MotionEventevent)方法, setOnTouchListener 、 setOnClickListener 方法也都是继承 View的,没有重写,那么咱么就重点看看 dispatchTouchEvent(MotionEvent ev) 方法吧
由于代码太长,我们分段来看代码,我们知道事件一般是 down-up 或者 down-move-up,不管是哪个,肯定是以down开始
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
resetTouchState() 中会把一个成员变量 mFirstTouchTarget 置空为null,我们先记住这点,继续往下看 重点1
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 {
intercepted = true;
}
如果是 ACTION_DOWN 或 mFirstTouchTarget != null,我们会执行是否拦截的操作, disallowIntercept 是不拦截,如果这个属性时false时,说明允许拦截,这时候会调用onInterceptTouchEvent(ev) 方法,在这个方法中决定是否最终拦截,intercepted 意思是拦截,根据 disallowIntercept 和 onInterceptTouchEvent(ev) 决定 intercepted 的值,disallowIntercept 有什么用,最后再分析。 mFirstTouchTarget 这个变量在这里看不出是干什么的,如果往后看,会发现如果子View的down消费了事件,mFirstTouchTarget会被赋值,不为null,也就是说,想要触发 onInterceptTouchEvent(ev) 方法:1、在down的时候触发;2、down的时候子View消费了事件,返回true,mFirstTouchTarget 被赋值。一旦 mFirstTouchTarget 被赋值,在本次 down-move-up 事件没结束前,不会被指控,它会在下一次事件触发时的开始使被清空重新赋值。 继续往下看 重点2
if (!canceled && !intercepted) {
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
...
if (newTouchTarget == null && childrenCount != 0) {
...
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
...
if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
...
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
...
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
...
}
}
简化后的代码, if (!canceled && !intercepted) 意思是没有取消,也没有被拦截才能走到if的判断语句里面,紧接着又是一个if判断,actionMasked == MotionEvent.ACTION_DOWN,常规的 down-move-up 中,只有down能走进去, move 和 up 靠边站,根本进不来。继续往下看,这个时候会遍历容器里面的子View,注意 canViewReceivePointerEvents 和
isTransformedTouchPointInView 方法,
private static boolean canViewReceivePointerEvents(View child) {
return (child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null;
}
protected boolean isTransformedTouchPointInView(float x, float y, View child,
PointF outLocalPoint) {
final float[] point = getTempPoint();
point[0] = x;
point[1] = y;
transformPointToViewLocal(point, child);
final boolean isInView = child.pointInView(point[0], point[1]);
if (isInView && outLocalPoint != null) {
outLocalPoint.set(point[0], point[1]);
}
return isInView;
}
一个是说子视图是否可以接收触摸事件,另一个是手指按下的位置是否在子view的范围内,如果不满足这两个条件,就continue找下一个,如果满足,继续往下看
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
dispatchTransformedTouchEvent() 方法中,简化后如下
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
handled = child.dispatchTouchEvent(transformedEvent);
}
return handled;
}
如果传入的子view不为空,就调用子view的 dispatchTouchEvent 方法,否则就调用自己的 dispatchTouchEvent 方法; addTouchTarget() 方法如下
private TouchTarget addTouchTarget(View child, int pointerIdBits) {
TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
这个意思是给 mFirstTouchTarget 赋值,此时 mFirstTouchTarget 不为空,在此也证明了 down 时,如果子view消费了事件,mFirstTouchTarget 不为空,如果mFirstTouchTarget 为null,说明 down 时候,没有消费,这是再看 if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) ,就能明白这句话的含义了。alreadyDispatchedToNewTouchTarget = true; break; 赋值一个局部变量,记录子view已经消费了down的时间,然后break跳出for循环,已经被消费了,就没必要继续找了。上面一大段代码,仅仅是down并且 intercepted 没被拦截时的分析,继续看后续代码 重点3
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
...
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
...
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
...
}
}
return handled;
如果子view消费了事件,mFirstTouchTarget 有值,则执行while里面的操作,注意,此时alreadyDispatchedToNewTouchTarget为true,则直接返回return handled,handled 为true,Down事件到此为止;如果 intercepted 为true,被拦截了,或者 intercepted 为false,走进了if (!canceled && !intercepted) 代码中,但没有子view消费触摸事件,mFirstTouchTarget 值为 null,此时,执行 dispatchTransformedTouchEvent 方法,这个方法上面有,此时传进去的子view参数为null,说明里面只能执行 super.dispatchTouchEvent(event),也就是基类的事件分发方法,在基类的 dispatchTouchEvent(event) 中,会调用到ViewGroup 自身的 onTouch(View v, MotionEvent event) 和 onTouchEvent(MotionEventevent) 方法,流程与View一样,最终handled的值就由它两个方法决定了。
down事件就结束了,假设返回值为 handled1。我们假设不管是true或false,都会执行完整的down-move-up事件,实际上非顶层的ViewGrouop外,都是只有为true时才会执行move和up事件。然后分析move, 如果 handled1 为 false, 重点1 中的代码 mFirstTouchTarget 为null,if语句不会执行,onInterceptTouchEvent 方法自然也不会执行;如果 handled1 为 true,分为子View消费和本身消费,如果是本身消费了,mFirstTouchTarget 为null,同样不会执行 onInterceptTouchEvent 方法;如果是子View消费,mFirstTouchTarget 不为null,可以走到if判断语句,默认是可以执行 onInterceptTouchEvent 方法,这一点也与之前的log日志相对应。 重点2 中的代码,不管 handled1 值是多少,都不会执行,因为if的语句判断,把move和up排除在外了,我们还是重点看看 重点3 的代码回归正题,只有down的时候被消费了,踩会执行move和up。分析move和up: 一、如果是子View消费了事件,mFirstTouchTarget 不为null,走到else里面,此时 alreadyDispatchedToNewTouchTarget为false,因为是局部变量不是成员变量,这时候会执行 dispatchTransformedTouchEvent 方法,此时传进去子view不为空,执行子view的 dispatchTouchEvent 方法,进而执行 onTouchEvent 方法;二、如果子View没有消费事件,是被ViewGroup消费掉了,那么 mFirstTouchTarget 为null,直接执行dispatchTransformedTouchEvent 方法,传入 View child 参数为null,此时执行super.dispatchTouchEvent(event) 即父类的 dispatchTouchEvent 方法,进而执行ViewGroup的onTouchEvent 方法。也就是说,如果down的时候子view中没有消费事件,那么接下来move和down就不会传给子view了,而是直接调用基类的 dispatchTouchEvent 方法,在它里面调用
onTouchEvent 方法。
最后看一下 重点1 中的一行代码, final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; 这个是是否不允许拦截,如果父类的这个属性为true,则父View肯定不会拦截子view,ViewGroup 中有个方法 requestDisallowInterceptTouchEvent(boolean disallowIntercept) 可以决定这个值
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
return;
}
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
我们发现,一旦子view拿到它的父View,然后调用了这个方法,这个方法是遍历往上层掉用,一直到根节点位置。一点调用这个方法,比如说子View中执行getParent().requestDisallowInterceptTouchEvent(true); 在眼下这个例子中,disallowIntercept 值为true,则 disallowIntercept 为true,intercepted = false; 表示不拦截焦点触发事件;反之,传入 false,则标识允许拦截,会执行 onInterceptTouchEvent 方法,由这个方法决定是否拦截。我们发现, onInterceptTouchEvent 方法返回false表示不拦截,
requestDisallowInterceptTouchEvent 方法传入true表示不拦截,这两个方法是相对的。这个方法常在子View需要处理焦点事件时调用这个方法,告知父View不要拦截。