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

Android事件分发机制源码解析(二)-ViewGroup的事件分发机制

2017-07-28 10:30 651 查看
概述

我们在上一篇中讲解了Android事件分发机制源码解析(一)-View的事件分发机制主要讲解的是View的事件分发机制,如果有了上一篇的基础的话再来学习这一篇的内容的话就会好理解很多,本篇内容会讲解ViewGroup的事件分发机制

ViewGroup是什么呢?ViewGroup是一组View的集合,包括很多的子类View和ViewGroup,是所有布局文件的父类或者是间接的父类,但是ViewGroup也是继承自View的,不过相比View来说它多了可以包含自View和定义布局参数的功能;

我们平时使用的各种布局都是继承自ViewGroup的。

下面我们来写一个例子,写一个布局文件继承自LinearLayout,代码如下

public class CustomerLayout extends LinearLayout {
public CustomerLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
}
布局文件如下

<com.oman.touchevent.viewgroup.CustomerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/mylayout"
tools:context="com.oman.touchevent.viewgroup.ViewGroupActivity">

<Button
android:id="@+id/btn1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World1!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:src="@mipmap/ic_launcher"/>

<Button
android:id="@+id/btn2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World2!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
</com.oman.touchevent.viewgroup.CustomerLayout>
我们在布局文件中设置了两个按钮,分别注册监听事件:

mLayout.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
Log.i(TAG, "MyLayout onTouch 执行了 ");
return false;
}
});
mButton1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Log.i(TAG, "mButton1 onClick1 执行了");
}
});
mButton2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Log.i(TAG, "mButton2 onClick2 执行了");
}
});
我们给布局文件注册了onTouch方法,给两个Button都注册了点击监听,并且都打印了一句话便于查看,运行后分别点击Button1,Button2和CustomerLayout打印结果如下





我们看到点击Button后CustomerLayout并没有打印信息,只有点击自定义布局时候才会打印;这里不了解ViewGroup事件分发机制的童靴可能会想是不是事件先传递到Button再传递到ViewGroup?其实不是这样的,要特别注意一点,在上一篇中也提到了,如果我们点击一个控件的话,是首先会调用所在布局的dispatchTouchEvent方法的,这里不是先由Button再传递到ViewGroup的;

我们看一下和ViewGroup事件分发机制有关的一个方法onInterceptTouchEvent方法,查看源码如下

/**
* Implement this method to intercept all touch screen motion events.  This
* allows you to watch events as they are dispatched to your children, and
* take ownership of the current gesture at any point.
*
* <p>Using this function takes some care, as it has a fairly complicated
* interaction with {@link View#onTouchEvent(MotionEvent)
* View.onTouchEvent(MotionEvent)}, and using it requires implementing
* that method as well as this one in the correct way.  Events will be
* received in the following order:
*
* <ol>
* <li> You will receive the down event here.
* <li> The down event will be handled either by a child of this view
* group, or given to your own onTouchEvent() method to handle; this means
* you should implement onTouchEvent() to return true, so you will
* continue to see the rest of the gesture (instead of looking for
* a parent view to handle it).  Also, by returning true from
* onTouchEvent(), you will not receive any following
* events in onInterceptTouchEvent() and all touch processing must
* happen in onTouchEvent() like normal.
* <li> For as long as you return false from this function, each following
* event (up to and including the final up) will be delivered first here
* and then to the target's onTouchEvent().
* <li> If you return true from here, you will not receive any
* following events: the target view will receive the same event but
* with the action {@link MotionEvent#ACTION_CANCEL}, and all further
* events will be delivered to your onTouchEvent() method and no longer
* appear here.
* </ol>
*
* @param ev The motion event being dispatched down the hierarchy.
* @return Return true to steal motion events from the children and have
* them dispatched to this ViewGroup through onTouchEvent().
* The current target will receive an ACTION_CANCEL event, and no further
* messages will be delivered here.
*/
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
&& ev.getAction() == MotionEvent.ACTION_DOWN
&& ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
&& isOnScrollbarThumb(ev.getX(), ev.getY())) {
return true;
}
return false;
}
代码很少,但是注释很多,这里有个boolean类型的返回值,如果我们重写这个方法将返回值直接改为true的话,看看打印结果:

public class CustomerLayout extends LinearLayout {
public CustomerLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}

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



不管你点击哪个按钮或者怎么点击,始终都是打印的CustomerLayout注册的监听,由此可以看出证明了我们上面的说法,事件分发是先传递到CustomerLayout的,然后才传递到Button的,想知道到底怎么实现的,我们得看dispatchTouchEvent的源码,通过向上查找在ViewGroup中找到了这个方法,我们看源码

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;
}
}
}
c749

}
}
}
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);
}
源码很多,我们之间看重点3行有一个判断if(disallowIntercept || !onInterceptTouchEvent),其中第一个是是否禁用事件拦截的功能,默认是false;那么就主要是看第二个条件了,第二个条件正好是我们刚才重写的onInterceptTouchEvent方法返回的值,我们将返回值改为true之后就将所有的按钮点击事件屏蔽掉了,所以我们相信具体的逻辑就是在判断内部进行的,继续往里看

在第19行有一个for循环,遍历了当前ViewGroup下的所有子View,然后在第24行判断当前遍历的View是不是正在点击的View,如果是的话就会进入到该条件判断的内部,然后在第29行调用了该View的dispatchTouchEvent,之后的流程就和 Android事件分发机制是一样的了。说明按钮的点击事件的处理确实是在里面进行的。

需要特别强调一下,调用子View的dispatchTouchEvent后是有返回值的。如果一个控件是可点击的,调用dispatchTouchEvent的返回值必定是true。因此会导致第29行的条件判断成立,于是在第31行给ViewGroup的dispatchTouchEvent方法直接返回了true。这样就导致后面的代码无法执行到了,所以就验证了前面打印的结果,如果按钮的点击事件得到执行,就会把MyLayout的touch事件拦截掉。

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

下面我们来总结一下整个Android事件分发机制吧

如果你点击了布局中的一个按钮首先会调用当前布局的dispatchTouchEvent(ev)方法

a)如果当前布局的onInterceptTouchEvent(ev)返回值为false的话,进入ViewGroup的dispatchTouchEvent(ev)方法内部,因为!onInterceptTouchEvent(ev)就为true,循环遍历当前布局的View找到正在点击的控件,如果当前控件是可点击的,那么view.dispatchTouchEvent(ev)方法返回值就为true,接下来会执行onTouch和onClick等监听,如果onTouch(ev)的返回值为true就不会进入onTouchEvent方法内部就不能执行onClick监听了;

b)如果当前布局的onInterceptTouchEvent(ev)返回值为true的话,进入ViewGroup的dispatchTouchEvent(ev)方法内部,因为!onInterceptTouchEvent(ev)就为false,就会执行后面的super.dispatchTouchEvent(ev)方法,就是View.dispatchTouchEvent(ev)

好了,到此,Android的事件分发机制源码解析全部结束,相信大家也会有所收获的。

Android时间分发机制源码解析(一)-View的事件分发机制
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息