Android事件分发机制(下)

1、ViewGroup事件分发简介

ViewGroup,一组View的集合,是Android中所有布局的父类或间接父类,像LinearLayoutRelativeLayout等都是继承自ViewGroup的。ViewGroup实际上也是一个View,只不过比起View,它多了可以包含子View和定义布局参数的功能。

ViewGroup的事件分发主要涉及三个函数:

1)dispatchTouchEvent():判断事件是否进行分发

2)onInterceptTouchEvent():拦截的意思,用于判断事件要不要通知它的孩子。

3)onTouchEvent():处理事件,没有重写ViewonTouchEvent()

这里需要知道Android事件的传递流程:

当你点击了某个控件,首先会去调用该控件所在布局的dispatchTouchEvent方法,然后在布局的dispatchTouchEvent方法中找到被点击的相应控件,再去调用该控件的dispatchTouchEvent方法。

接下来我们对ViewGroup的事件分发涉及的三个主角的源码进行分析。

2.ViewGroup源码分析

首先让我们看看ViewGroupdispatchTouchEvent()的源码,只看关键部分:

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
	// .........
	<strong>// 1、第一步 判断是否要分发改触摸事件</strong>
	// onFilterTouchEventForSecurity()表示是否要分发该触摸事件。
	// 返回值为false表示该View不是位于顶部,并且有设置属性使该View不在顶部时不响应触摸事件,则不分发该触摸事件,进不了if代码块,dispatchTouchEvent返回false。
	// 否则,往下执行,即执行if代码块里的语句。
	boolean handled = false;
	if (onFilterTouchEventForSecurity(ev)) {
		final int action = ev.getAction();
		final int actionMasked = action & MotionEvent.ACTION_MASK;

		// 1.2、第二步 检查是否需要清空目标和状态
		// 如果是ACTION_DOWN(按下事件),则清空之前的触摸事件处理目标和状态。
		// 从源码的注释可以看出在开始新的触摸手势前清楚之前的状态
		// 主要是包括:
		// (01)清空mFirstTouchTarget链表,并设置mFirstTouchTarget为null。mFirstTouchTarget是"接受触摸事件的View"所组成的单链表
		// (02) 清空mGroupFlags的FLAG_DISALLOW_INTERCEPT标记
		if (actionMasked == MotionEvent.ACTION_DOWN) {

			cancelAndClearTouchTargets(ev);
			resetTouchState();
		}

		// 1.3、第三步 检查当前ViewGroup是否要拦截触摸事件
		// 1)如果是"按下事件(ACTION_DOWN)" 或者mFirstTouchTarget不为null;就执行if代码块里面的内容。
		// 2)否则的话,设置intercepted为true,即拦截。
		final boolean intercepted;
		if (actionMasked == MotionEvent.ACTION_DOWN
				|| mFirstTouchTarget != null) {
			// 检查禁止拦截标记:FLAG_DISALLOW_INTERCEPT
			final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
			if (!disallowIntercept) {
				// disallowIntercept默认恒为false
				// 所以默认情况下都会执行ViewGroup的onInterceptTouchEvent()方法,因为onInterceptTouchEvent()默认返回false,所以执行下面这行代码会使intercepted
				// 的值为false,不会对事件进行拦截。
				intercepted = onInterceptTouchEvent(ev);
				ev.setAction(action); // restore action in case it was
										// changed
			} else {
				intercepted = false;
			}
		} else {
			// There are no touch targets and this action is not an initial
			// down
			// so this view group continues to intercept touches.
			intercepted = true;
		}

		// ......

		// 1.4、第四步 检查当前触摸事件是否被取消
		// 说明:
		// 对于ACTION_DOWN而言,mPrivateFlags的PFLAG_CANCEL_NEXT_UP_EVENT位肯定是0;因此,canceled=false。
		final boolean canceled = resetCancelNextUpFlag(this)
				|| actionMasked == MotionEvent.ACTION_CANCEL;

		// Update list of touch targets for pointer down, if needed.
		final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
		TouchTarget newTouchTarget = null;
		boolean alreadyDispatchedToNewTouchTarget = false;
		// 1.5、第五步 将触摸事件分发给"当前ViewGroup的子View和子ViewGroup"
		// 说明:
		// 如果触摸"没有被取消",同时也"没有被拦截"的话,则将触摸事件分发给它的子View和子ViewGroup。
		// 如果当前ViewGroup的孩子有接受触摸事件的话,则将该孩子添加到mFirstTouchTarget链表中。(注意用的是如果不是否则)
		if (!canceled && !intercepted) {

			View childWithAccessibilityFocus = ev
					.isTargetAccessibilityFocus() ? findChildWithAccessibilityFocus()
					: null;

			if (actionMasked == MotionEvent.ACTION_DOWN
					|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
					|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
				// 这是获取触摸事件的序号 以及 触摸事件的id信息。
				// 1)从注释可以知道,对于ACTION_DOWN,actionIndex总是是0
				// 2)而getPointerId()是获取的该触摸事件的id,并将该id信息保存到idBitsToAssign中。
				// 这个触摸事件的id是为多指触摸而添加的;对于单指触摸,getActionIndex()返回的肯定是0;
				// 而对于多指触摸,第一个手指的id是0,第二个手指的id是1,第三个手指的id是2,...依次类推。
				final int actionIndex = ev.getActionIndex(); // always 0 for
																// down
				final int idBitsToAssign = split ? 1 << ev
						.getPointerId(actionIndex)
						: TouchTarget.ALL_POINTER_IDS;

				// Clean up earlier touch targets for this pointer id in
				// case they
				// have become out of sync.
				// 清空这个手指之前的TouchTarget链表。
				removePointersFromTouchTargets(idBitsToAssign);
				// 获取该ViewGroup包含的View和ViewGroup的数目,
				final int childrenCount = mChildrenCount;
				if (newTouchTarget == null && childrenCount != 0) {
					final float x = ev.getX(actionIndex);
					final float y = ev.getY(actionIndex);
					// Find a child that can receive the event.
					// Scan children from front to back.
					final ArrayList<View> preorderedList = buildOrderedChildList();
					final boolean customOrder = preorderedList == null
							&& isChildrenDrawingOrderEnabled();
					final View[] children = mChildren;
					// 递归遍历ViewGroup的孩子,对触摸事件进行分发。
					for (int i = childrenCount - 1; i >= 0; i--) {
						final int childIndex = customOrder ? getChildDrawingOrder(
								childrenCount, i) : i;
						final View child = (preorderedList == null) ? children[childIndex]
								: preorderedList.get(childIndex);

						// If there is a view that has accessibility focus
						// we want it
						// to get the event first and if not handled we will
						// perform a
						// normal dispatch. We may do a double iteration but
						// this is
						// safer given the timeframe.
						if (childWithAccessibilityFocus != null) {
							if (childWithAccessibilityFocus != child) {
								continue;
							}
							childWithAccessibilityFocus = null;
							i = childrenCount - 1;
						}

						if (!canViewReceivePointerEvents(child)
								|| !isTransformedTouchPointInView(x, y,
										child, null)) {
							ev.setTargetAccessibilityFocus(false);
							continue;
						}
						// 查找child是否存在于mFirstTouchTarget的单链表中。 //
						// 是的话,返回对应的TouchTarget对象;否则,返回null。
						newTouchTarget = getTouchTarget(child);
						if (newTouchTarget != null) {
							// Child is already receiving touch within its
							// bounds.
							// Give it the new pointer in addition to the
							// ones it is handling.
							newTouchTarget.pointerIdBits |= idBitsToAssign;
							break;
						}

						resetCancelNextUpFlag(child);
						// 调用dispatchTransformedTouchEvent()将触摸事件分发给child。
						if (dispatchTransformedTouchEvent(ev, false, child,
								idBitsToAssign)) {
							// 如果child能够接受该触摸事件,即child消费或者拦截了该触摸事件的话则调用addTouchTarget()将child添加到mFirstTouchTarget链表的表头.
							mLastTouchDownTime = ev.getDownTime();
							if (preorderedList != null) {
								// childIndex points into presorted list,
								// find original index
								for (int j = 0; j < childrenCount; j++) {
									if (children[childIndex] == mChildren[j]) {
										mLastTouchDownIndex = j;
										break;
									}
								}
							} else {
								mLastTouchDownIndex = childIndex;
							}
							mLastTouchDownX = ev.getX();
							mLastTouchDownY = ev.getY();
							newTouchTarget = addTouchTarget(child,
									idBitsToAssign);
							alreadyDispatchedToNewTouchTarget = true;
							break;
						}
						//......
				}

				if (newTouchTarget == null && mFirstTouchTarget != null) {
					// Did not find a child to receive the event.
					// Assign the pointer to the least recently added
					// target.
					newTouchTarget = mFirstTouchTarget;
					while (newTouchTarget.next != null) {
						newTouchTarget = newTouchTarget.next;
					}
					newTouchTarget.pointerIdBits |= idBitsToAssign;
				}
			}
		}

		// 1.6、 第六步 进一步的对触摸事件进行分发
		// 说明:
		// 如果mFirstTouchTarget为null,意味着还没有任何View来接受该触摸事件;此时,将当前ViewGroup看作一个View;将会调用"当前的ViewGroup的父类View的dispatchTouchEvent()"对触摸事件进行分发处理。即会将触摸事件交给当前ViewGroup的onTouch(), onTouchEvent()进行处理。
		// 否则mFirstTouchTarget不为null,意味着有ViewGroup的子View或子ViewGroup中,有可以接受触摸事件的。那么,就将触摸事件分发给这些可以接受触摸事件的子View或子ViewGroup。
		if (mFirstTouchTarget == null) {
			// No touch targets so treat this as an ordinary view.
			// 注意:这里的第3个参数是null
			handled = dispatchTransformedTouchEvent(ev, canceled, null,
					TouchTarget.ALL_POINTER_IDS);
		} else {
			// Dispatch to touch targets, excluding the new touch target if
			// we already
			// dispatched to it. Cancel touch targets if necessary.
			TouchTarget predecessor = null;
			TouchTarget target = mFirstTouchTarget;
			while (target != null) {
				final TouchTarget next = target.next;
				if (alreadyDispatchedToNewTouchTarget
						&& target == newTouchTarget) {
					handled = true;
				} else {
					final boolean cancelChild = resetCancelNextUpFlag(target.child)
							|| intercepted;
					if (dispatchTransformedTouchEvent(ev, cancelChild,
							target.child, target.pointerIdBits)) {
						handled = true;
					}
					//.......
				}
				predecessor = target;
				target = next;
			}
		}

		// 1.7 第七步 进行还原状态
		if (canceled || actionMasked == MotionEvent.ACTION_UP
				|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
			resetTouchState();
		} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
			final int actionIndex = ev.getActionIndex();
			final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
			removePointersFromTouchTargets(idBitsToRemove);
		}
	}
		//......
		return handled;
}

注意:第5ViewGroup尝试将触摸事件分发给它的孩子。该步骤只有在ACTION_DOWN的时候才执行。因为在执行该步骤后,会对mFirstTouchTarget赋值,作为第6步处理Touch事件方式的依据。不管该步骤结果如何即是否有孩子接受Touch事件,都会在第6步对Touch事件处理,处理方式受mFirstTouchTarget影响。如果它的孩子接受了触摸事件,则会调用addTouchTarget()将该孩子添加到mFirstTouchTarget链表中。那么在ACTION_DOWN之后,传递ACTION_MOVEACTION_UP时,ViewGroup就在第6步中直接遍历mFirstTouchTarget链表,查找之前接受ACTION_DOWN的孩子,并将触摸事件分配给这些孩子。所以说5步其实就是分发事件并判断ViewGroupTouch事件是否有孩子接受。
也就是说,如果ViewGroup的某个孩子没有接受ACTION_DOWN事件;那么,ACTION_MOVEACTION_UP等事件也一定不会分发给这个孩子!因为孩子没有接受ACTION_DOWN事件,从常识上讲其后面的ACTION事件肯定也接受不到,而从源码上分析就是孩子在执行dispatchTransformedTouchEvent()方法时返回false,即dispatchTouchEvent()ACTION_DOWN分发时返回false,所以ACTION事件不会传递下去。

通过以上源码分析能得出以下结论:

1)通过第三步知道,ViewGroupdispatchTouchEvent()方法的disallowIntercept默认是为false,所以一定会执行ViewGrouponInterceptTouchEvent(),而该方法默认返回false,所以intercepted就为false,表示不拦截事件,就会执行后面的事件分发程序。

根据该结论扩展:

扩展1:如果重写了onInterceptTouchEvent方法返回值为true,就会对事件拦截,则一定会执行到第六步的if (mFirstTouchTarget == null) 里的代码块,将ViewGroup当作View,执行ViewdispatchTouchEvent方法,并将事件交给ViewGrouponTouch()onTouchEvent()进行处理。而且ACTION_DOWN后面的一系列ACTION都不会执行(因为View默认是不可点击),因为在执行ACTION_DOWN的时候已经将事件给拦截掉了。

扩展2:在当前ViewGroup包含的子View或者子ViewGroup可以通过调用

requestDisallowInterceptTouchEvent(true)使disallowIntercep值为true,就不会执行ViewGrouponInterceptTouchEvent()方法,不会对事件进行拦截,就可以在子View或者子ViewGroup中对事件进行处理。

2)当执行完第六步后,会有两种情况:

一是mFirstTouchTarget值为空表示没有任何View来接受该触摸事件,此时将当前ViewGroup看作一个View;调用当前的ViewGroup的父类ViewdispatchTouchEvent()对触摸事件进行分发处理,即会将触摸事件交给当前ViewGrouponTouch(), onTouchEvent()进行处理(通过View的事件分发可知)。

另外一种情况则是mFirstTouchTarget值不为空,表示有ViewGroup的子View或子ViewGroup中可以接受触摸事件。那么就会将触摸事件分发给这些可以接受触摸事件的子View或子ViewGroup

3、ViewGroup事件分发总结

通过对ViewGroup的源码分析,对ViewGroup的事件分发用流程图表示如下:


4、Case演示

通过以上对ViewGroup事件分发的源码分析,相信我们对Viewgroup的事件分发的原理有了深刻的了解,那么接下来我们就通过一个例子来对ViewGroup的事件分发进行演示,以加深对其原理的掌握。

如下,自定义一个布局MyViewGroup,继承自LinearLayout,并重写三个方法:

public class MyViewGroup extends LinearLayout {

	private static final String tag = "MyViewGroup";

	public MyViewGroup(Context context, AttributeSet attrs) {
		super(context, attrs);
	}

	@Override
	public boolean dispatchTouchEvent(MotionEvent ev) {
		String actionName = EventUtils.getActionName(ev);
		Log.d(tag, "dispatchTouchEvent(start) " + actionName);
		boolean flag = super.dispatchTouchEvent(ev);//调用VIewGroup的dispatchTouchEvent()
		Log.d(tag, "dispatchTouchEvent(end) " + actionName+" flag="+flag);
		return flag;
	}

	@Override
	public boolean onInterceptTouchEvent(MotionEvent ev) {
		String actionName = EventUtils.getActionName(ev);
		Log.d(tag, "onInterceptTouchEvent(start) " + actionName);
		boolean flag = super.onInterceptTouchEvent(ev);//调用VIewGroup的onInterceptTouchEvent()
		Log.d(tag, "onInterceptTouchEvent(end) " + actionName+" flag="+flag);
		return flag;
	}

	@Override
	public boolean onTouchEvent(MotionEvent event) {
		String actionName = EventUtils.getActionName(event);
		Log.d(tag, "onTouchEvent(start) " + actionName);
		boolean flag = super.onTouchEvent(event);//调用VIewGroup的onTouchEvent()
		Log.d(tag, "onTouchEvent(end) " + actionName+" flag="+flag);
		return flag;
	}
}

自定义MyView继承View

public class MyView extends View {

	private static final String tag = MyView.class.getSimpleName();

	public MyView(Context context, AttributeSet attrs) {
		super(context, attrs);

	}

	@Override
	public boolean dispatchTouchEvent(MotionEvent ev) {
		String actionName = EventUtils.getActionName(ev);
		Log.d(tag, "dispatchTouchEvent(start) " + actionName);
		boolean flag = super.dispatchTouchEvent(ev);// 调用View的dispatchTouchEvent()
		Log.d(tag, "dispatchTouchEvent(end) " + actionName + " flag=" + flag);
		return flag;
	}

	@Override
	public boolean onTouchEvent(MotionEvent event) {
		String actionName = EventUtils.getActionName(event);
		Log.d(tag, "onTouchEvent(start) " + actionName);
		boolean flag = super.onTouchEvent(event);// 调用VIewGroup的onInterceptTouchEvent()
		Log.d(tag, "onTouchEvent(end) " + actionName + " flag=" + flag);
		return flag;
	}

}

在测试的项目的Activity的布局将两个自定义的View进行布局,并重写Activity如下:

public class MainActivity extends Activity {

	protected static final String tag = MainActivity.class.getSimpleName();

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);

	}
	
	@Override
	public boolean dispatchTouchEvent(MotionEvent ev) {
		String actionName = EventUtils.getActionName(ev);
		Log.d(tag, "dispatchTouchEvent(start) " + actionName);
		boolean flag = super.dispatchTouchEvent(ev);// 调用View的dispatchTouchEvent()
		Log.d(tag, "dispatchTouchEvent(end) " + actionName+" flag="+flag);
		return flag;
	}
	
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		String actionName = EventUtils.getActionName(event);
		Log.d(tag, "onTouchEvent(start) " + actionName);
		boolean flag = super.onTouchEvent(event);// 调用View的dispatchTouchEvent()
		Log.d(tag, "onTouchEvent(end) " + actionName+" flag="+flag);
		return flag;
	}

}

当点击MyView 所在区域,log日志如下:

 

在该示例中我们重写了MainActivitydispatchTouchEvent()onTouchEvent(),但都是调用的父类的方法。分析以上日志可知:

Touch事件到来时,MainActivity通过dispatchTouchEvent将事件分发给MyViewGroupMyViewGroup.dispatchTouchEvent()调用MyViewGroup.interceptTouchEvent()判断事件是否拦截,没有拦截将事件继续分发,然后事件分发给MyView,进入MyView.dispatchTouchEvent(),然后在调用MyView.onTouchEvent()返回false,然后将false值返回给MyViewdispatchTouchEvent(),使MyView.dispatchTouchEvent()也为false

退出MyView.dispatchTouchEvent()返回falseMyViewGroup.dispatchTouchEvent(),表示MyView没有接受该触摸事件。MyViewGroup则得知MyView没有接受该触摸事件之后,将自己当作一个View,调用View.dispatchTouchEvent()View.dispatchTouchEvent()接着就会进入MyViewGroup.onTouchEvent()MyViewGroup.onTouchEvent()没有消费该触摸事件,因此返回false 然后,View.dispatchTouchEvent()就会结束,并返回false。接着,MyViewGroup就会退出MyViewGroup.dispatchTouchEvent()并返回false
 MyActivity在得知MyViewGroup没有接受该触摸事件之后,就会调用进入MyActivity.onTouchEvent,并返回false。至此,MyActivity.dispatchTouchEvent()才结束。因此,会退出MyActivity.dispatchTouchEvent(),并返回false

之后由于MyViewGroupMyView都没有接受ACTION_DOWN事件,因此ACTION_MOVEACTION_UP事件就不会再分发给它们.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值