上一篇我们介绍了View的事件分发机制,不熟悉的可以先了解一下
上一篇:Android事件分发机制(View篇)
引言
本篇我们接着上一篇,来继续学习一下Android ViewGroup的事件分发机制
本来View的事件分发机制和ViewGroup的事件分发机制是紧密联系在一起的,但是因为其中的原理不是三两句能够说清楚的,也为了方便理解,就先拆开来讲,然后融合起来统一归纳总结,这样结构更清晰,好了,废话不多,我们进入正文。
正文
本篇ViewGroup的事件分发机制的场景和上一篇Android事件分发机制(View篇)开始的场景是一样的,这里不再重复。
上篇也提到过对于ViewGroup我们关注三个方法:
ViewGroup 三个方法:
- dispatchTouchEvent (
MotionEvent event
)//负责事件分发 - onInterCeptTouchEvent(
MotionEvent event
)//处理是否拦截当前事件 - onTouchEvent(
MotionEvent event
)//当前View自己处理当前事件
我们先新建一个project: ViewGroupDemo
- 新建一个MyLinearLayout
继承LinearLayout
重写dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent
三个方法并打印log
/**
* @author Charay
* @data 2017/10/31
*/
public class MyLinearLayout extends LinearLayout {
private static final String TAG = MyLinearLayout.class.getSimpleName();
public MyLinearLayout(Context context) {
super(context);
}
public MyLinearLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
Log.e(TAG,"--MyLinearLayout--dispatchTouchEvent---ACTION_DOWN---");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG,"--MyLinearLayout--dispatchTouchEvent---ACTION_MOVE---");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG,"--MyLinearLayout--dispatchTouchEvent---ACTION_UP---");
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
Log.e(TAG,"--MyLinearLayout--onInterceptTouchEvent---ACTION_DOWN---");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG,"--MyLinearLayout--onInterceptTouchEvent---ACTION_MOVE---");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG,"--MyLinearLayout--onInterceptTouchEvent---ACTION_UP---");
break;
}
return super.onInterceptTouchEvent(ev);
// return true;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
Log.e(TAG,"--MyLinearLayout--onTouchEvent---ACTION_DOWN---");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG,"--MyLinearLayout--onTouchEvent---ACTION_MOVE---");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG,"--MyLinearLayout--onTouchEvent---ACTION_UP---");
break;
}
return super.onTouchEvent(event);
}
}
- 新建一个MyButton
继承Button
重写dispatchTouchEvent、onTouchEvent
两个方法并打印log
/**
* @author Charay
* @data 2017/10/31
*/
public class MyButton extends Button {
private static final String TAG = MyButton.class.getSimpleName();
public MyButton(Context context) {
super(context);
}
public MyButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
Log.e(TAG,"--MyButton--dispatchTouchEvent---ACTION_DOWN---");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG,"--MyButton--dispatchTouchEvent---ACTION_MOVE---");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG,"--MyButton--dispatchTouchEvent---ACTION_UP---");
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
Log.e(TAG,"--MyButton--onTouchEvent---ACTION_DOWN---");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG,"--MyButton--onTouchEvent---ACTION_MOVE---");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG,"--MyButton--onTouchEvent---ACTION_UP---");
break;
}
return super.onTouchEvent(event);
}
}
在布局文件中添加 MyLinearLayout
和 MyButton
activity_main.xml
在MainActivity
中初始化 MyLinearLayout
和 MyButton
并添加setOnTouchListener
,然后打印log
public class MainActivity extends Activity {
private static final String TAG = MainActivity.class.getSimpleName();
private MyLinearLayout mMyLinearLayout;
private MyButton mMyButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mMyLinearLayout = (MyLinearLayout) findViewById(R.id.my_linearlayout);
mMyButton = (MyButton) findViewById(R.id.my_button);
mMyLinearLayout.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
switch (motionEvent.getAction()){
case MotionEvent.ACTION_DOWN:
Log.e(TAG,"--MyLinearLayout--onTouch---ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG,"--MyLinearLayout--onTouch---ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG,"--MyLinearLayout--onTouch---ACTION_UP");
break;
}
return false;
}
});
mMyButton.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
switch (motionEvent.getAction()){
case MotionEvent.ACTION_DOWN:
Log.e(TAG,"--mMyButton--onTouch---ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG,"--mMyButton--onTouch---ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG,"--mMyButton--onTouch---ACTION_UP");
break;
}
return false;
}
});
}
}
为了能完整看到事件分发拦截的整个流程,我们在上面代码中没有更改任何一个方法的返回值,只是打印了log
现在我们运行程序,点击MyButton
,打印日志log如下:
11-01 11:54:02.993 E/MyLinearLayout: --MyLinearLayout--dispatchTouchEvent---ACTION_DOWN---
11-01 11:54:02.993 E/MyLinearLayout: --MyLinearLayout--onInterceptTouchEvent---ACTION_DOWN---
11-01 11:54:02.993 E/MyButton: --MyButton--dispatchTouchEvent---ACTION_DOWN---
11-01 11:54:02.994 E/MainActivity: --mMyButton--onTouch---ACTION_DOWN
11-01 11:54:02.994 E/MyButton: --MyButton--onTouchEvent---ACTION_DOWN---
11-01 11:54:03.004 E/MyLinearLayout: --MyLinearLayout--dispatchTouchEvent---ACTION_MOVE---
11-01 11:54:03.004 E/MyLinearLayout: --MyLinearLayout--onInterceptTouchEvent---ACTION_MOVE---
11-01 11:54:03.004 E/MyButton: --MyButton--dispatchTouchEvent---ACTION_MOVE---
11-01 11:54:03.005 E/MainActivity: --mMyButton--onTouch---ACTION_MOVE
11-01 11:54:03.005 E/MyButton: --MyButton--onTouchEvent---ACTION_MOVE---
11-01 11:54:03.028 E/MyLinearLayout: --MyLinearLayout--dispatchTouchEvent---ACTION_UP---
11-01 11:54:03.028 E/MyLinearLayout: --MyLinearLayout--onInterceptTouchEvent---ACTION_UP---
11-01 11:54:03.028 E/MyButton: --MyButton--dispatchTouchEvent---ACTION_UP---
11-01 11:54:03.028 E/MainActivity: --mMyButton--onTouch---ACTION_UP
11-01 11:54:03.029 E/MyButton: --MyButton--onTouchEvent---ACTION_UP---
虽然日志比较长,但是不要怕,上面把ACTION_DOWN、ACTION_MOVE、ACTION_UP
都打印了出来,
我们只需要看一组ACTION_DOWN
即可,因为一个View
一旦消费了ACTION_DOWN
事件,那么其他两个事件一定都是这个View
消费。
从log
中我们发现ACTION_DOWN、ACTION_MOVE、ACTION_UP
三个手势动作规律是一样的,执行顺序都是从最外层ViewGroup
向内层View
(或ViewGroup
传递):先是MyLinearLayout
的dispatchTouchEvent
在这个方法中先执行onInterceptTouchEvent
判断事都拦截这个事件,如果不拦截(默认不拦截)就传递给MyButton
,然后事件分发给MyButton
的dispatchTouchEvent
,执行MyButton
的dispatchTouchEvent
,由于MyButton
及其父View
没有onInterceptTouchEvent
方法,所以直接在dispatchTouchEvent
中先判断onTouch
的返回值,默认为false
,再执行MyButton
的onTouchEvent
如果我们在MyLinearLayout中拦截了这个事件结果将是怎样呢?
下面我们在MyLinearLayout中的重写的onInterceptTouchEvent中把返回值改为true
onInterceptTouchEvent
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
Log.e(TAG,"--MyLinearLayout--onInterceptTouchEvent---ACTION_DOWN---");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG,"--MyLinearLayout--onInterceptTouchEvent---ACTION_MOVE---");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG,"--MyLinearLayout--onInterceptTouchEvent---ACTION_UP---");
break;
}
// return super.onInterceptTouchEvent(ev);
return true;
}
然后运行程序分别点击MyButton和MyLinearLayout,发现打印的log是一样的:
//点击MyButton
11-02 11:07:14.207 E/MyLinearLayout: --MyLinearLayout--dispatchTouchEvent---ACTION_DOWN---
11-02 11:07:14.207 E/MyLinearLayout: --MyLinearLayout--onInterceptTouchEvent---ACTION_DOWN---
11-02 11:07:14.207 E/MainActivity: --MyLinearLayout--onTouch---ACTION_DOWN
11-02 11:07:14.207 E/MyLinearLayout: --MyLinearLayout--onTouchEvent---ACTION_DOWN---
//点击MyLinearLayout
11-02 11:07:21.667 E/MyLinearLayout: --MyLinearLayout--dispatchTouchEvent---ACTION_DOWN---
11-02 11:07:21.677 E/MyLinearLayout: --MyLinearLayout--onInterceptTouchEvent---ACTION_DOWN---
11-02 11:07:21.677 E/MainActivity: --MyLinearLayout--onTouch---ACTION_DOWN
11-02 11:07:21.677 E/MyLinearLayout: --MyLinearLayout--onTouchEvent---ACTION_DOWN---
一分钟思考一下下面两个问题:
- 为什么
MyButton
没有任何有关log
,而且这次还执行了MyLinearLayout的onTouch
和onTouchEvent
方法? - 为什么我们把返回值改为true之前,只执行了
MyLinearLayout的dispatchTouchEvent
和onInterceptTouchEvent
,而没有执行onTouch
和onTouchEvent
方法?
下面我们来看一下 ViewGroup
中的源码(android-10,即2.3.3的源码)
dispatchTouchEvent
/**
* {@inheritDoc}
*/
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (!onFilterTouchEventForSecurity(ev)) {
return false;
}
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) {
// this is weird, we got a pen down, but we thought it was
// already down!
// XXX: We should probably send an ACTION_UP to the current
// target.
mMotionTarget = null;
}
// If we're disallowing intercept or if we're allowing and we didn't
// intercept
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
// reset this event's action (just to protect ourselves)
ev.setAction(MotionEvent.ACTION_DOWN);
// We know we want to dispatch the event down, find a child
// who can handle it, start with the front-most child.
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)) {
// offset the event to the view's coordinate system
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)) {
// Event handled, we have a target now.
mMotionTarget = child;
return true;
}
// The event didn't get handled, try the next view.
// Don't reset the event's location, it's not
// necessary here.
}
}
}
}
}
boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
(action == MotionEvent.ACTION_CANCEL);
if (isUpOrCancel) {
// Note, we've already copied the previous state to our local
// variable, so this takes effect on the next event
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// The event wasn't an ACTION_DOWN, dispatch it to our target if
// we have one.
final View target = mMotionTarget;
if (target == null) {
// We don't have a target, this means we're handling the
// event as a regular view.
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 have a target, see if we're allowed to and want to intercept its
// events
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)) {
// target didn't handle ACTION_CANCEL. not much we can do
// but they should have.
}
// clear the target
mMotionTarget = null;
// Don't dispatch this event to our own view, because we already
// saw it when intercepting; we just want to give the following
// event to the normal onTouchEvent().
return true;
}
if (isUpOrCancel) {
mMotionTarget = null;
}
// finally offset the event to the target's coordinate system and
// dispatch the event.
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);
}
代码比较多,我们抛开干扰,只看对我们有用的
先看这行boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
disallowIntercept
的含义是禁用拦截事件,下面看 if (disallowIntercept || !onInterceptTouchEvent(ev))
判断,
进入判断条件的情况有两种:
-
disallowIntercept
为true
,即禁用拦截事件,这时候即使拦截事件onInterceptTouchEvent(ev)
返回值为true
,也不会拦截 -
disallowIntercept
为false
,即允许拦截,但是不拦截onInterceptTouchEvent(ev)
返回值为false
默认情况下是允许拦截的,即disallowIntercept
为false
,只有当我们调用mMyLinearLayout.requestDisallowInterceptTouchEvent(true);
的时候,即禁止拦截,disallowIntercept
的值才为true
.
进入到if判断中后,我们看
for (int i = count - 1; i >= 0; i--) {
final View child = children[i];
......
if (child.dispatchTouchEvent(ev)) {
// Event handled, we have a target now.
mMotionTarget = child;
return true;
}
......
}
这时遍历子View,把事件传给子View
的dispatchTouchEvent
,如果子View
是ViewGroup
,那就继续遍历,直到遍历到子View
是View
类型的,然后调用调用这个子View
的dispatchTouchEvent
,之后的判断就干我们上一篇的Android事件分发机制(View篇)原理一致了。
而我们刚才的代码中把onInterceptTouchEvent(ev)
返回值改为true
,则不符合上面的两种情况,因此进入不到if判断中,事件被拦截了,因此事件到不了MyButton
的dispatchTouchEvent
方法,也就不会有MyButton
的任何有log
。
下面我们看这几行代码:
// The event wasn't an ACTION_DOWN, dispatch it to our target if
// we have one.
final View target = mMotionTarget;
if (target == null) {
// We don't have a target, this means we're handling the
// event as a regular view.
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);
}
虽然我们在onInterceptTouchEvent(ev)
中返回true
拦截了这个事件,但这并不代表我们当前MyLinearLayout
就是消费当前事件的消费者,因为我们没有target
,所以进入了上面代码中,最后一行return super.dispatchTouchEvent(ev);
即进入了View
的dispatchTouchEvent(ev)
,同样跟我们上一篇Android View的事件分发机制(上)原理一致,虽然和上面for循环中一样也是在View
中,但是是有区别的,这个是MyLinearLayout
这个(ViewGroup)
继承的View
,其操作是针对MyLinearLayout
的,即之后所走的onTouch
和onTouchEvent
也都是针对MyLinearLayout
的;而for
循环中的是MyButton
这个(View)
继承的View
,其操作是针对MyMutton
的,即之后所走的onTouch
和onTouchEvent
也都是针对MyBUtton
的。
既然我们在onInterceptTouchEvent(ev)
中返回true
拦截了这个事件,拦截后,就走到了MyLinearLayout的onTouch
方法,默认返回false
,然后执行了MyLinearLayout
的onTouchEvent
方法,所以刚好符合我们上面的log
。
总结
下面我们总结一下ViewGroup
的事件分发机制的关键点,以及和View的事件分发机制的异同
ViewGroup比View多了一个拦截事件的方法onInterceptTouchEvent(ev)
ViewGroup比View中最先执行的方法,都是dispatchTouchEvent方法,然后View执行了onTouch方法
但是ViewGroup中在执行onTouch方法之前多了一个onInterceptTouchEvent(ev)判断,这个判断决定了事件在本ViewGroup中消费,还是将事件继续分发给它的子View.
而View是没有onInterceptTouchEvent(ev)的,所以没有拦截,事件能不能在View中消费掉,关键是看这个View中的onTouch方法或者onTouchEvent的返回值,如果返回值都是false,那就会将事件返回给其父View的onTouch和onTouchEvent方法,直到找到一个能消费此次事件的ViewGroup。
好了关与Android中View和ViewGroup的事件分发机制两篇都讲完了,如果有不足或者不理解的地方可在评论中回复,我们共同进步。
最后附上ViewGroupDemo源码:github下载
上一篇:Android View的事件分发机制(上)