上一篇博客android事件分发教程(一):View我们以一种最简单易懂的方式学习了View的事件分发机制,那么趁热(火)打铁(劫),今天来个进阶- -,学习哈ViewGroup中的事件分发。
首先要引进一个方法:onInterceptTouchEvent;这个事ViewGroup独有的,从字面意思可以理解到是个拦截事件;既然本篇是研究ViewGroup的,那么新建一个MyLayout.java,继承自RelativeLayout,用于测试用:
package com.cjt.event;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.RelativeLayout;
/**
* Created by mvp-cjt on 2016/6/14.
* Email:879309896@qq.com
*/
public class MyLayout extends RelativeLayout implements View.OnTouchListener{
public final static String TAG = "mvp-cjt";
public MyLayout(Context context, AttributeSet attrs) {
super(context, attrs);
setOnTouchListener(this);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
Log.i(TAG, "MyLayout dispatchTouchEvent: ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.i(TAG, "MyLayout dispatchTouchEvent: ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.i(TAG, "MyLayout dispatchTouchEvent: ACTION_UP");
break;
default:
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
Log.i(TAG, "MyLayout onInterceptTouchEvent: ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.i(TAG, "MyLayout onInterceptTouchEvent: ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.i(TAG, "MyLayout onInterceptTouchEvent: ACTION_UP");
break;
default:
break;
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouch(View view, MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
Log.i(TAG, "MyLayout onTouch: ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.i(TAG, "MyLayout onTouch: ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.i(TAG, "MyLayout onTouch: ACTION_UP");
break;
default:
break;
}
return false;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
Log.i(TAG, "MyLayout onTouchEvent: ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.i(TAG, "MyLayout onTouchEvent: ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.i(TAG, "MyLayout onTouchEvent: ACTION_UP");
break;
default:
break;
}
return super.onTouchEvent(ev);
}
}
然后把上篇的两个空间MyButton、MyImage添加进来,最终的测试界面activity_main.xml则是:
<?xml version="1.0" encoding="utf-8"?>
<com.cjt.event.MyLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.cjt.event.MyButton
android:id="@+id/my_button"
android:layout_centerInParent="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="button"/>
<com.cjt.event.MyImage
android:layout_below="@+id/my_button"
android:layout_centerHorizontal="true"
android:src="@mipmap/ic_launcher"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</com.cjt.event.MyLayout>
还是先点击一下MyImage,观测控制台输出:
06-15 21:49:40.906 32285-32285/com.cjt.event I/mvp-cjt: MyLayout dispatchTouchEvent: ACTION_DOWN
06-15 21:49:40.915 32285-32285/com.cjt.event I/mvp-cjt: MyLayout onInterceptTouchEvent: ACTION_DOWN
06-15 21:49:40.915 32285-32285/com.cjt.event I/mvp-cjt: MyImage dispatchTouchEvent: ACTION_DOWN
06-15 21:49:40.916 32285-32285/com.cjt.event I/mvp-cjt: MyImage onTouch: ACTION_DOWN
06-15 21:49:40.916 32285-32285/com.cjt.event I/mvp-cjt: MyImage onTouchEvent: ACTION_DOWN
06-15 21:49:40.917 32285-32285/com.cjt.event I/mvp-cjt: MyLayout onTouch: ACTION_DOWN
06-15 21:49:40.917 32285-32285/com.cjt.event I/mvp-cjt: MyLayout onTouchEvent: ACTION_DOWN
然后点击一下MyButton,同样观测控制台输出:
06-15 21:52:26.965 19602-19602/com.cjt.event I/mvp-cjt: MyLayout dispatchTouchEvent: ACTION_DOWN
06-15 21:52:26.965 19602-19602/com.cjt.event I/mvp-cjt: MyLayout onInterceptTouchEvent: ACTION_DOWN
06-15 21:52:26.965 19602-19602/com.cjt.event I/mvp-cjt: MyButton dispatchTouchEvent: ACTION_DOWN
06-15 21:52:26.965 19602-19602/com.cjt.event I/mvp-cjt: MyButton onTouch: ACTION_DOWN
06-15 21:52:26.965 19602-19602/com.cjt.event I/mvp-cjt: MyButton onTouchEvent: ACTION_DOWN
06-15 21:52:27.014 19602-19602/com.cjt.event I/mvp-cjt: MyLayout dispatchTouchEvent: ACTION_MOVE
06-15 21:52:27.014 19602-19602/com.cjt.event I/mvp-cjt: MyLayout onInterceptTouchEvent: ACTION_MOVE
06-15 21:52:27.014 19602-19602/com.cjt.event I/mvp-cjt: MyButton dispatchTouchEvent: ACTION_MOVE
06-15 21:52:27.014 19602-19602/com.cjt.event I/mvp-cjt: MyButton onTouch: ACTION_MOVE
06-15 21:52:27.014 19602-19602/com.cjt.event I/mvp-cjt: MyButton onTouchEvent: ACTION_MOVE
06-15 21:52:27.015 19602-19602/com.cjt.event I/mvp-cjt: MyLayout dispatchTouchEvent: ACTION_UP
06-15 21:52:27.015 19602-19602/com.cjt.event I/mvp-cjt: MyLayout onInterceptTouchEvent: ACTION_UP
06-15 21:52:27.015 19602-19602/com.cjt.event I/mvp-cjt: MyButton dispatchTouchEvent: ACTION_UP
06-15 21:52:27.015 19602-19602/com.cjt.event I/mvp-cjt: MyButton onTouch: ACTION_UP
06-15 21:52:27.015 19602-19602/com.cjt.event I/mvp-cjt: MyButton onTouchEvent: ACTION_UP
可能log有点多,不要慌,慢慢看。根据这个log,能够初拟出事件在MyLayout、MyImage、MyButton中各个方法的传递,先看看第一次点击MyImage的时候事件传递方向:
注意这种情况无法触发一个完整的事件DOWN、MOVE、UP,在DOWN事件沿着图上的方向传递完后就结束了。
再看看点击MyButton的时候事件传递方向:
很明显的是在事件返回最上层(MyLayout)的时候,在MyButton这里就被消费掉了,不再向外分发。另外,这种情况能够触发一个完整的事件DOWN、MOVE、UP。
这个“干货”未免也太干了,没点理论支持是站不住脚的,那追溯下ViewGroup的dispatchTouchEvent的源码,还是截取部分,关键的代码:
public boolean dispatchTouchEvent(MotionEvent event) {
/*-------------------------- action为DOWN ----------------------------*/
if (action == MotionEvent.ACTION_DOWN) {
// disallowIntercept:是否不允许拦截,默认false,可通过viewGroup.requestDisallowInterceptTouchEvent(boolean);进行设置
// onInterceptTouchEvent:可重写,默认返回false
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
final View[] children = mChildren;
final int count = mChildrenCount;
// 遍历子View
for (int i = count - 1; i >= 0; i--) {
final View child = children[i];
// 如果touch事件在该子View上,就继续分发
if (frame.contains(scrolledXInt, scrolledYInt)) {
if (child.dispatchTouchEvent(ev)) {
// 子View的dispatchTouchEvent返回true的话,代表具有消费能力,return true,跳出遍历子View循环
mMotionTarget = child;
return true;
}
}
}
}
}
/*-------------------------- action为MOVE、UP、CANCEL ----------------------------*/
boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) || (action == MotionEvent.ACTION_CANCEL);
final View target = mMotionTarget;
if (target == null) {
// target为null说明子View都不具有消费能力,这里的super就是ViewGroup的父类View
return super.dispatchTouchEvent(event);
}
if (!disallowIntercept && onInterceptTouchEvent(event)) {
mMotionTarget = null;
return true;
}
if (isUpOrCancel) {
mMotionTarget = null;
}
// 执行最靠近外层且具有消费能力View的dispatchTouchEvent方法(类似递归,如果子View是ViewGroup又会嵌套执行)
return target.dispatchTouchEvent(event);
}
这个已经是精简的核心代码,一定要认真仔细地弄清楚。这样就不难解释上述得出的两幅图了。结合源码,待我细细道来(⊙o⊙)。
点击MyImage;先进入MyLayout的dispatchTouchEvent,作出action判断,进入DOWN方法体,然后判断是否拦截if (disallowIntercept || !onInterceptTouchEvent(ev)),接着遍历子View,遍历至MyImage时,frame.contains(scrolledXInt, scrolledYInt)
为true,进入child.dispatchTouchEvent(ev),child就会接着执行dispatchTouch中的onTouch和onTouchEvent;结合View的事件分发,返回false,退出for循环,mMotionTarget 为null,然后执行super.dispatchTouchEvent(event); ViewGroup的父类就是View,又回到View的dispatchTouchEvent中了,接着执行MyLayout的onTouch和onTouchEvent方法。
点击MyButton;与上面MyImage不同的是,MyButton的dispatchTouchEvent返回的是true,然后执行mMotionTarget = child; return true;
这样直接返回true了,就没有后面MyLayout的onTouch和onTouchEvent事件了。
好了,又到了文末总结的时候了:
1:消费能力,就是在ACTION为DOWN的时候,child的dispatchTouchEvent返回true,这只是针对ViewGroup中来说的,这样就能够触发一个完整的事件(DOWN、MOVE、UP);
2:是否拦截onInterceptTouchEvent的返回值并不是决定性的,它与另外一个disallowIntercept共同决定,disallowIntercept 可以在子类通过getParent.requestDisallowInterceptTouchEvent设置。但若onInterceptTouchEvent在down返回true的话,mMotionTarget 为null,这样子View是无法获取到事件的;