Android ViewGroup的事件分发机制-源码分析

2023-05-22,,

为了更好的理解ViewGroup的事件分发机制,我们在自定义一个MyLinerLayout.

public class MyLinearLayout extends LinearLayout
{
private static final String TAG = MyLinearLayout.class.getSimpleName(); public MyLinearLayout(Context context, AttributeSet attrs)
{
super(context, attrs);
} @Override
public boolean dispatchTouchEvent(MotionEvent ev)
{
int action = ev.getAction();
switch (action)
{
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "dispatchTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "dispatchTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "dispatchTouchEvent ACTION_UP");
break; default:
break;
}
return super.dispatchTouchEvent(ev);
} @Override
public boolean onTouchEvent(MotionEvent event)
{ int action = event.getAction(); switch (action)
{
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "onTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "onTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "onTouchEvent ACTION_UP");
break; default:
break;
} return super.onTouchEvent(event);
} @Override
public boolean onInterceptTouchEvent(MotionEvent ev)
{ int action = ev.getAction();
switch (action)
{
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "onInterceptTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "onInterceptTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "onInterceptTouchEvent ACTION_UP");
break; default:
break;
} return super.onInterceptTouchEvent(ev);
} @Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept)
{
Log.e(TAG, "requestDisallowInterceptTouchEvent ");
super.requestDisallowInterceptTouchEvent(disallowIntercept);
} }
点击Button的时候可以看到打印信息:

E/MyLinearLayout(959): dispatchTouchEvent ACTION_DOWN
 E/MyLinearLayout(959): onInterceptTouchEvent ACTION_DOWN
 E/MyButton(959): dispatchTouchEvent ACTION_DOWN
 E/MyButton(959): onTouchEvent ACTION_DOWN
 E/MyButton(959): onTouchEvent ACTION_MOVE
 E/MyLinearLayout(959): dispatchTouchEvent ACTION_MOVE
 E/MyLinearLayout(959): onInterceptTouchEvent ACTION_MOVE
 E/MyButton(959): dispatchTouchEvent ACTION_MOVE
 E/MyButton(959): onTouchEvent ACTION_MOVE
 E/MyLinearLayout(959): dispatchTouchEvent ACTION_UP
 E/MyLinearLayout(959): onInterceptTouchEvent ACTION_UP
 E/MyButton(959): dispatchTouchEvent ACTION_UP
 E/MyButton(959): onTouchEvent ACTION_UP


可以看到大体的事件流程为:


MyLinearLayout的dispatchTouchEvent -> MyLinearLayout的onInterceptTouchEvent -> MyButton的dispatchTouchEvent ->Mybutton的onTouchEvent

可以看出,在View上触发事件,最先捕获到事件的为View所在的ViewGroup,然后才会到View自身。

下面来看下ViewGroup的dispatchTouchEvent的源码

代码较长,先看下down事件:

 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) {
//触发down的时候把mMotionTarget重置为null
mMotionTarget = null;
}
// 如果不允许拦截或者允许拦截了但是没有拦截就会循环遍历里面的View
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;
//如果找到点击的View并且返回值==true,那么mMotionTarget就会被赋值成当前的子View
//并且返回true,down事件分发结束
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.
}
}
}
}
}

move的源码:

    @Override
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; //获取down事件时的mMotionTarget
final View target = mMotionTarget; //允许拦截并且拦截了
if (!disallowIntercept && onInterceptTouchEvent(ev)) {
//放在下面讲拦截的源码
} final float xc = scrolledXFloat - (float) target.mLeft; final float yc = scrolledYFloat - (float) target.mTop;
ev.setLocation(xc, yc);
//没有拦截就会调用子view的分发方法
return target.dispatchTouchEvent(ev);
}
up事件同样的道理,就不贴出了。

1、ACTION_DOWN中,ViewGroup捕获到事件,然后判断是否拦截,如果没有拦截,则找到包含当前x,y坐标的子View,赋值给mMotionTarget,然后调用 mMotionTarget.dispatchTouchEvent

2、ACTION_MOVE中,ViewGroup捕获到事件,然后判断是否拦截,如果没有拦截,则直接调用mMotionTarget.dispatchTouchEvent(ev)

3、ACTION_UP中,ViewGroup捕获到事件,然后判断是否拦截,如果没有拦截,则直接调用mMotionTarget.dispatchTouchEvent(ev)

关于ViewGroup的拦截方法:

public boolean onInterceptTouchEvent(MotionEvent ev)
{
int action = ev.getAction();
switch (action)
{
case MotionEvent.ACTION_DOWN:
//如果你觉得需要拦截
return true ;
case MotionEvent.ACTION_MOVE:
//如果你觉得需要拦截
return true ;
case MotionEvent.ACTION_UP:
//如果你觉得需要拦截
return true ;
} return false;
}

默认是不拦截的,即返回false;如果你需要拦截,只要return true就行了,这要该事件就不会往子View传递了,并且如果你在DOWN retrun true ,

则DOWN,MOVE,UP子View都不会捕获事件;如果你在MOVE return true , 则子View在MOVE和UP都不会捕获事件

原因很简单,当onInterceptTouchEvent(ev) return true的时候,会把mMotionTarget 置为null ;

如果ViewGroup的onInterceptTouchEvent(ev) 当ACTION_MOVE时return true ,即拦截了子View的MOVE以及UP事件;

此时子View希望依然能够响应MOVE和UP时该咋办呢?

Android给我们提供了一个方法:requestDisallowInterceptTouchEvent(boolean) 用于设置是否允许拦截,我们在子View的dispatchTouchEvent中直接这么写

public boolean dispatchTouchEvent(MotionEvent event)
{
getParent().requestDisallowInterceptTouchEvent(true);
int action = event.getAction(); switch (action)
{
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "dispatchTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "dispatchTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "dispatchTouchEvent ACTION_UP");
break; default:
break;
}
return super.dispatchTouchEvent(event);
}
getParent().requestDisallowInterceptTouchEvent(true);
这样即使ViewGroup在MOVE的时候return true,子View依然可以捕获到MOVE以及UP事件。
看下ViewGroup的Move和Up的拦截源码:
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;
}
我们把disallowIntercept设置为true时,!disallowIntercept直接为false,于是拦截的方法体就被跳过了,也就失效了。

上面的流程都是正常的分发流程,如果没有找到合适的子View,或者子View的dispatchTouchEvent返回false怎么办?

这是上面down事件的部分代码

if (child.dispatchTouchEvent(ev)) {
  mMotionTarget = child;
  return true;
}
只有在child.dispatchTouchEvent(ev)返回true了,才会认为找到了能够处理当前事件的View,即mMotionTarget = child,否则仍旧是null

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);
}
我们没有一个能够处理该事件的目标元素,意味着我们需要自己处理

总结:

1.如果ViewGroup找到了能够处理该事件的View,则直接交给子View处理,自己的onTouchEvent不会被触发。

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

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

Android ViewGroup的事件分发机制-源码分析的相关教程结束。

《Android ViewGroup的事件分发机制-源码分析.doc》

下载本文的Word格式文档,以方便收藏与打印。