您的位置:首页 > 移动开发 > Android开发

Android ViewGroup事件分发机制学习笔记

2017-08-21 15:54 561 查看

前言

上篇文章我对View的事件分发机制进行了一次学习总结:

Android View事件分发机制学习笔记

今天我们继续对ViewGroup的事件分发机制进行总结。

首先我们来探讨一下,什么是ViewGroup?它和普通的View有什么区别?

顾名思义,ViewGroup就是一组View的集合,它包含很多的子View和子VewGroup,是android中所有布局的父类或间接父类,像LinearLayout、RelativeLayout等都是继承自ViewGroup的。但ViewGroup实际上也是一个View,只不过比起View,它多了可以包含子View和定义布局参数的功能。ViewGroup继承结构示意图如下所示:



可以看到,我们平时项目里经常用到的各种布局,全都属于ViewGroup的子类。

此处提问:事件分发是从View传入ViewGroup还是从ViewGroup传入View?

示例代码

自定义一个布局,如下:

public class MyLinearLayout extends LinearLayout {

public MyLinearLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
}


两个按钮,分别为MyLearLayout和按钮注册点击事件,如下:

public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
MyLinearLayout layout = (MyLinearLayout) findViewById(R.id.layout);
Button button1 = (Button) findViewById(R.id.button1);
Button button2 = (Button) findViewById(R.id.button2);
layout.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
Log.d(TAG, "onTouch: "+motionEvent.getAction());
return false;
}
});
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Log.d(TAG, "onClick:点击了button1");
}
});
button2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Log.d(TAG, "onClick:点击了button2");
}
});

}
}


分别点击一下Button1、Button2和空白区域,打印结果如下所示:



你会发现,当点击按钮的时候,MyLinearLayout注册的onTouch方法并不会执行,只有点击空白区域的时候才会执行该方法。

在上篇文章view事件分发机制中,onClick是最后一个分发事件,事件分发是想不会逆转的,你可以先理解成Button的onClick方法将事件消费掉了,因此事件不会再继续向下传递。

那是不是就说明Android中的touch事件是先传递到View,再传递到ViewGroup的?

我们看一段源码,再下结论吧!

源码分析

ViewGroup中有一个onInterceptTouchEvent方法,我们来看一下这个方法的源码:

注释特别多,我们只看关键的方法部分。

public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}


我们在MyLayout中重写这个方法,然后返回一个true试试,代码如下所示:

public class MyLinearLayout extends LinearLayout {

public MyLinearLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return true;
}
}


现在再次运行项目,然后分别Button1、Button2和空白区域,打印结果如下所示:



你会发现,不管你点击哪里,永远都只会触发MyLearLayout的touch事件了,按钮的点击事件完全被屏蔽掉了!

这是为什么呢?

如果Android中的touch事件是先传递到View,再传递到ViewGroup的,那么MyLearLayout又怎么可能屏蔽掉Button的点击事件呢?

所以提问中的从View传递到ViewGroup不攻自破!

Android中touch事件的传递,绝对是先传递到ViewGroup,再传递到View的。

记得在Android View事件分发机制学习笔记 中说过,只要你触摸了任何控件,就一定会调用该控件的dispatchTouchEvent方法。

这个说法没错,只不过还不完整而已。实际情况是,当你点击了某个控件,首先会去调用该控件所在布局的dispatchTouchEvent方法,然后在布局的dispatchTouchEvent方法中找到被点击的相应控件,再去调用该控件的dispatchTouchEvent方法。

我们点击了MyLearLayout中的按钮,会先去调用MyLearLayout的dispatchTouchEvent方法,可是你会发现MyLearLayout中并没有这个方法。那就再到它的父类LinearLayout中找一找,发现也没有这个方法。那只好继续再找LinearLayout的父类ViewGroup,你终于在ViewGroup中看到了这个方法,按钮的dispatchTouchEvent方法就是在这里调用的。示意图如下所示:



ViewGroup中的dispatchTouchEvent方法的源码

public boolean dispatchTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
final float xf = ev.getX();
final float yf = ev.getY();
final float scrolledXFloat = xf + mScrollX;
final float scrolledYFloat = yf + mScrollY;
final Rect frame = mTempRect;
boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (action == MotionEvent.ACTION_DOWN) {
if (mMotionTarget != null) {
mMotionTarget = null;
}
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
ev.setAction(MotionEvent.ACTION_DOWN);
final int scrolledXInt = (int) scrolledXFloat;
final int scrolledYInt = (int) scrolledYFloat;
final View[] children = mChildren;
final int count = mChildrenCount;
for (int i = count - 1; i >= 0; i--) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null) {
child.getHitRect(frame);
if (frame.contains(scrolledXInt, scrolledYInt)) {
final float xc = scrolledXFloat - child.mLeft;
final float yc = scrolledYFloat - child.mTop;
ev.setLocation(xc, yc);
child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
if (child.dispatchTouchEvent(ev))  {
mMotionTarget = child;
return true;
}
}
}
}
}
}
boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
(action == MotionEvent.ACTION_CANCEL);
if (isUpOrCancel) {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
final View target = mMotionTarget;
if (target == null) {
ev.setLocation(xf, yf);
if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
}
return super.dispatchTouchEvent(ev);
}
if (!disallowIntercept && onInterceptTouchEvent(ev)) {
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
ev.setAction(MotionEvent.ACTION_CANCEL);
ev.setLocation(xc, yc);
if (!target.dispatchTouchEvent(ev)) {
}
mMotionTarget = null;
return true;
}
if (isUpOrCancel) {
mMotionTarget = null;
}
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
ev.setLocation(xc, yc);
if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
mMotionTarget = null;
}
return target.dispatchTouchEvent(ev);
}


首先在第13行可以看到一个条件判断,如果disallowIntercept和!onInterceptTouchEvent(ev)两者有一个为true,就会进入到这个条件判断中。disallowIntercept是指是否禁用掉事件拦截的功能,默认是false,也可以通过调用requestDisallowInterceptTouchEvent方法对这个值进行修改。那么当第一个值为false的时候就会完全依赖第二个值来决定是否可以进入到条件判断的内部,第二个值是什么呢?竟然就是对onInterceptTouchEvent方法的返回值取反!也就是说如果我们在onInterceptTouchEvent方法中返回false,就会让第二个值为true,从而进入到条件判断的内部,如果我们在onInterceptTouchEvent方法中返回true,就会让第二个值为false,从而跳出了这个条件判断。

在第19行通过一个for循环,遍历了当前ViewGroup下的所有子View,然后在第24行判断当前遍历的View是不是正在点击的View,如果是的话就会进入到该条件判断的内部,然后在第29行调用了该View的dispatchTouchEvent,之后的流程就和 Android事件分发机制完全解析,带你从源码的角度彻底理解(上) 中讲解的是一样的了。我们也因此证实了,按钮点击事件的处理确实就是在这里进行的。

然后需要注意一下,调用子View的dispatchTouchEvent后是有返回值的。我们已经知道,如果一个控件是可点击的,那么点击该控件时,dispatchTouchEvent的返回值必定是true。因此会导致第29行的条件判断成立,于是在第31行给ViewGroup的dispatchTouchEvent方法直接返回了true。这样就导致后面的代码无法执行到了,也是印证了我们前面打印的结果,如果按钮的点击事件得到执行,就会把MyLearLayout的touch事件拦截掉。

那如果我们点击的不是按钮,而是空白区域呢?这种情况就一定不会在第31行返回true了,而是会继续执行后面的代码。那我们继续往后看,在第44行,如果target等于null,就会进入到该条件判断内部,这里一般情况下target都会是null,因此会在第50行调用super.dispatchTouchEvent(ev)。这句代码会调用到哪里呢?当然是View中的dispatchTouchEvent方法了,因为ViewGroup的父类就是View。

再看一下整个ViewGroup事件分发过程的流程图吧,相信可以帮助大家更好地去理解:



总结

Android事件分发是先传递到ViewGroup,再由ViewGroup传递到View的。

在ViewGroup中可以通过onInterceptTouchEvent方法对事件传递进行拦截,onInterceptTouchEvent方法返回true代表不允许事件继续向子View传递,返回false代表不对事件进行拦截,默认返回false。

子View中如果将传递的事件消费掉,ViewGroup中将无法接收到任何事件。即如果ViewGroup找到了能够处理该事件的View,则直接交给子View处理,自己的onTouchEvent不会被触发;

可以通过复写onInterceptTouchEvent(ev)方法,拦截子View的事件(即return true),把事件交给自己处理,则会执行自己对应的onTouchEvent方法

子View可以通过调用getParent().requestDisallowInterceptTouchEvent(true); 阻止ViewGroup对其MOVE或者UP事件进行拦截;

参考

http://blog.csdn.net/guolin_blog/article/details/9153747

http://blog.csdn.net/lmj623565791/article/details/39102591
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: