Android事件分发机制(上)

1、Android事件分发简介

运用的前提是掌握,掌握的前提是理解。只有对事件分发的原理理解了,才能在开发工程中熟练的运用事件分发机制。

1.1、两大基础控件类型

ViewViewGroupView即普通的控件,没有子布局的,如ButtonTextView.  ViewGroup继承自View,表示可以有子控件,如LinearlayoutListview等。

1.2、点击事件

Android中点击事件用MotionEvent类表示,最重要的有3:

(1)MotionEvent.ACTION_DOWN  按下View,是所有事件的开始

(2)MotionEvent.ACTION_MOVE   滑动事件

(3)MotionEvent.ACTION_UP       down对应,表示抬起

1.3、两个监听

事件传递机制的最终目的都是为了触发执行View的点击(onClick)监听和触摸(onTouch)监听。

2、View的事件分发

View的事件分发主要涉及两个函数

1)dispatchTouchEvent():Touch事件传递到目标View或者自己如果自己就是目标View。如果事件被该控件处理了返回true ,否则返回false

ViewdispatchTouchEvent方法将事件传递给自己的onTouch()onTouchEvent()处理。onTouch()View提供让用户自己处理Touch事件的接口,而onTouchEvent()Android系统提供处理Touch事件的接口。onTouch()优先级高于onTouchEvnet()

2onTouchEvent():用于处理触摸事件,如果事件被处理了返回true,否则false

接下来以一个小case来演示View事件分发并以View的源码分析其原理。

2.1、Case演示

如下,在Activity中定义了一个Button按钮,并对它设置了点击监听和Touch监听:并且onTouch监听里默认return false

mButton.setOnClickListener(new OnClickListener() {
	@Override
	public void onClick(View v) {
		Log.d(TAG, "onClick execute");
	}
});
mButton.setOnTouchListener(new OnTouchListener() {

	@Override
	public boolean onTouch(View v, MotionEvent event) {
		Log.d(TAG, "onTouch execute,action " + event.getAction());
		return false;
	}
});

 当点击Button按钮时,查看log日志如下:


可以看到onTouch()方法优先于onClick执行的,并且onTouch执行了两次,一次是ACTION_DOWN,一次是ACTION_UP(如果你手慢可能会有多次ACTION_MOVE的执行)。因此事件传递的顺序是先经过onTouch,再传递到onClick

当修改onTouch()方法里的返回值为true的时候,再点击Button按钮,你会发现log日志信息如下:

 

发现onCLick()方法不会执行了,可以理解为onTouch()方法因为返回ture,消费了该点击事件,为了验证解释该现象,接下来会从源码来分析。

2.2、View源码分析

1dispatchTouchEvent()

事件传递的入口ViewdispatchTouchEvent()函数,所以当点击button按钮后,就会去调用Button类里的dispatchTouchEvent方法,可是Button类里并没有这个方法,那么就到它的父类TextView里去找一找,而TextView里也没有这个方法,那没只好继续在TextView的父类View里找,发现View里的dispatchTouchEvent()方法源码如下:

public boolean dispatchTouchEvent(MotionEvent event) {
	if (mInputEventConsistencyVerifier != null) {
		mInputEventConsistencyVerifier.onTouchEvent(event, 0);
	}

	// 判断View是否被屏蔽,被屏蔽意思是该View不是位于顶部,有其他View在它之上
	// 被屏蔽即返回false,进不了if代码块,不会执行onTouch()和onTouchEvent()
	if (onFilterTouchEventForSecurity(event)) {
		ListenerInfo li = mListenerInfo;
		if (li != null && li.mOnTouchListener != null
				&& (mViewFlags & ENABLED_MASK) == ENABLED
				&& li.mOnTouchListener.onTouch(this, event)) {
			return true;
		}

		if (onTouchEvent(event)) {
			return true;
		}
	}

	if (mInputEventConsistencyVerifier != null) {
		mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
	}
	return false;
}

找到这个判断:

if (li != null && li.mOnTouchListener != null&& (mViewFlags & ENABLED_MASK) == ENABLED&& li.mOnTouchListener.onTouch(this, event)) 

{return true;}

里面有三个条件,如果mOnTouchListener != null且控件是enable(可用的)且onTouch()三个条件都为真,就直接返回true,否则就去执行onTouchEvent(event)方法,而且只要onTouchEvent()返回true,则dispatchTouchEvent恒返回true

接下来分别分析下上面的三个条件:

首先看看mOnTouchListener这个变量是在哪里赋值,继续查看View的源码发现如下方法:

public void setOnTouchListener(OnTouchListener l) {
    mOnTouchListener = l;
}

发现mOnTouchListener是在setOnTouchListener方法里赋值的,也就是说只要给控件注册了touch事件,mOnTouchListener就一定被赋值了。接着看第二个条件,判断当前点击的控件是否是enable(可用的)的,而针对本例来说,Button默认都是enable的,因此这个条件为true第三个条件会回调Button注册touch事件时的onTouch方法。如果onTouch()方法里返回true,就会让这三个条件全部成立,从而整个方法直接返回true。如果在onTouch方法里返回false,就会再去执onTouchEvent(event)方法

那么,接下来来看看View里的onTouchEvent()的源码:

2onTouchEvent()

public boolean onTouchEvent(MotionEvent event) {
	final float x = event.getX();
	final float y = event.getY();
	final int viewFlags = mViewFlags;

	<strong>// 第一步:判断View是否被禁用即Enable属性是false</strong>
	// return:如果被禁用则返回是否可点击
	if ((viewFlags & ENABLED_MASK) == DISABLED) {
		if (event.getAction() == MotionEvent.ACTION_UP
				&& (mPrivateFlags & PFLAG_PRESSED) != 0) {
			setPressed(false);
		}
		// A disabled view that is clickable still consumes the touch
		// events, it just doesn't respond to them.
		return (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
	}

	if (mTouchDelegate != null) {
		if (mTouchDelegate.onTouchEvent(event)) {
			return true;
		}
	}
	<strong>// 第二步:判断View是否可点击</strong>
	// desc:if代码块里面涉及到的主要是获取焦点,设置按下状态,触发onClick(), onLongClick()事件等等
	// return:如果可点击,执行此步onTouchEvent()恒返回true,则diapatchTouchEvent()也恒返回true
	// 否则onTouchEvent()返回false,则diapatchTouchEvent()也恒返回false
	if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
		switch (event.getAction()) {
		case MotionEvent.ACTION_UP:
			boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
			if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
				// take focus if we don't have it already and we should in
				// touch mode.
				boolean focusTaken = false;
				if (isFocusable() && isFocusableInTouchMode()
						&& !isFocused()) {
					focusTaken = requestFocus();
				}

				if (prepressed) {
					// The button is being released before we actually
					// showed it as pressed. Make it show the pressed
					// state now (before scheduling the click) to ensure
					// the user sees it.
					setPressed(true, x, y);
				}

				if (!mHasPerformedLongPress) {
					// This is a tap, so remove the longpress check
					removeLongPressCallback();

					// Only perform take click actions if we were in the
					// pressed state
					if (!focusTaken) {
						// Use a Runnable and post this rather than calling
						// performClick directly. This lets other visual
						// state
						// of the view update before click actions start.
						if (mPerformClick == null) {
							mPerformClick = new PerformClick();
						}
						if (!post(mPerformClick)) {
							performClick();
						}
					}
				}

				if (mUnsetPressedState == null) {
					mUnsetPressedState = new UnsetPressedState();
				}

				if (prepressed) {
					postDelayed(mUnsetPressedState,
							ViewConfiguration.getPressedStateDuration());
				} else if (!post(mUnsetPressedState)) {
					// If the post failed, unpress right now
					mUnsetPressedState.run();
				}

				removeTapCallback();
			}
			break;

		case MotionEvent.ACTION_DOWN:
			mHasPerformedLongPress = false;

			if (performButtonActionOnTouchDown(event)) {
				break;
			}

			// Walk up the hierarchy to determine if we're inside a
			// scrolling container.
			boolean isInScrollingContainer = isInScrollingContainer();

			// For views inside a scrolling container, delay the pressed
			// feedback for
			// a short period in case this is a scroll.
			if (isInScrollingContainer) {
				mPrivateFlags |= PFLAG_PREPRESSED;
				if (mPendingCheckForTap == null) {
					mPendingCheckForTap = new CheckForTap();
				}
				mPendingCheckForTap.x = event.getX();
				mPendingCheckForTap.y = event.getY();
				postDelayed(mPendingCheckForTap,
						ViewConfiguration.getTapTimeout());
			} else {
				// Not inside a scrolling container, so show the feedback
				// right away
				setPressed(true, x, y);
				checkForLongClick(0);
			}
			break;

		case MotionEvent.ACTION_CANCEL:
			setPressed(false);
			removeTapCallback();
			removeLongPressCallback();
			break;

		case MotionEvent.ACTION_MOVE:
			drawableHotspotChanged(x, y);

			// Be lenient about moving outside of buttons
			if (!pointInView(x, y, mTouchSlop)) {
				// Outside button
				removeTapCallback();
				if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
					// Remove any future long press/tap checks
					removeLongPressCallback();

					setPressed(false);
				}
			}
			break;
		}

		return true;
	}

	return false;
}

代码比较长,我们看关键代码段,找到如下判断,

if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {

通过if的两个判断条件,我们知道只要View是可点击CLICKABLE或者是LONG_CLICKABLE),便能进入该if语句,而且不管当前的action是什么,最终onTouchEvent()返回true

接着查看if里面的源码,可知如果当前的事件是抬起手指,则会进入到MotionEvent.ACTION_UP这个case当中。而且在该case语句中最终会执行到performClick()方法,那我们进入到这个方法里看看:

public boolean performClick() {
	sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

	ListenerInfo li = mListenerInfo;
	if (li != null && li.mOnClickListener != null) {
		playSoundEffect(SoundEffectConstants.CLICK);
		li.mOnClickListener.onClick(this);
		return true;
	}

	return false;
}

可以看到,只要mOnClickListener不是null,就会去调用它的onClick方法,而mOnClickListener又在View中的setOnClickListener()方法进行赋值,如下:

public void setOnClickListener(OnClickListener l) {
	if (!isClickable()) {
		setClickable(true);
	}
	getListenerInfo().mOnClickListener = l;
}

当我们通过调用setOnClickListener方法来给控件注册点击事件时,就会给mOnClickListener赋值。然后每当控件被点击时,都会在performClick()方法里回调被点击控件的onClick()方法。

现在结合前面的例子来分析一下,首先在dispatchTouchEvent中最先执行的就是onTouch方法,因此onTouch肯定是要优先于onClick执行的。而如果在onTouch方法里返回了true,就会让dispatchTouchEvent方法直接返回true,不会再继续往下执行。而打印结果也证实了如果onTouch返回trueonClick就不会再执行了。而如果onTouch()返回false,就会执行viewonTouchEvent()方法,而onClick()会在该方法里执行。

2.3、View事件分发总结

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

 

View事件分发流程图的说明:

1)事件分发是先对ACTION_DOWN进行分发的,如果其的dispatchTouchEvent返回false,后面的actionACTION_UP将都不会执行。

2)如果ACTION_DOWN分发成功,接下来就是对ACTION_MOVEACTION_UP进行分发。

3)在分发过程中只要有一个actiondispatchTouchEvent返回false,后面的action都不会触发了。

现在总结下View事件分发的结论:

1onTouch()onTouchEvent()两个方法都是在ViewdispatchTouchEvent中调用的。而onClick方法又在onTouchEvent()中调用的。

2onTouch()优先于onTouchEvent()的执行,且当onTouch()返回true将事件消费掉,onTouchEvent将不会再执行onTouch()能执行的前提是设置了TouchListener且该该控件是Enable,一般只要不人为修改,绝大部分View默认是Enable

3)当前控件(或是布局)是否可点击(CLICKABLE或者是LONG_CLICKABLE)直接决定onTouchEvent方法的返回值,从而影响着dispatchTouchEvent方法的返回值。

2.4、事件的层级传递

通过对Touch事件层级传递的分析,能进一步加深对事件分发流程的理解。

我们知道MotionEvent主要包括一系列的ACTION_DOWNACTION_MOVEACTION_UP等事件。因为View不像ViewGroup,它是没有子控件的,不存在事件在子控件的传递。因此,对于View来说,事件分发的本质就是对着一系列ACTION进行传递。

通过以上对View源码分析和对View事件分发的总结,接下来我们对ACTION传递进行总结:

默认处理方式下(即按照View源码处理方式),当View被点击时,ACTION_DOWN事件就会通过Activity传递给ViewdispatchTouchEvent方法。首先会调用ViewdispatchTouchEvent()ACTION_DOWN进行分发,然后View会调用onTouchEvent()Touch事件进行处理,如果onTouchEvent方法返回false则将false返回给dispatchTouchEvent方法,此时dispatchTouchEvent()也返回false,则表示View不接受该Touch事件,事件不会继续传递,ACTION_DOWN后面的ACTION_UP等将不会触发。如果onTouchEvent方法返回true则将true返回给dispatchTouchEvent方法,dispatchTouchEvent方法也返回true,则表示View接受了该Touch事件,事件会继续传递,ACTION_DOWN后面的ACTION_UP等会触发。

用一句话总结就是dispatchTouchEvent在进行事件传递的时候,只有当前一个ACTIONdispatchTouchEvent()返回true,才会触发后面的ACTION

聪明的你肯定发现前面的例子中,在onTouch事件里面返回了falseACTION_DOWNACTION_UP也都得到执行,这岂不是和结论相矛盾,仔细分析之前的源码不难发现,首先在onTouch事件里返回了false,就一定会进入到onTouchEvent方法中,然后在onTouchEvent方法中,由于我们点击了按钮,就会进入到if判断,判断是否是CLICKABLE或者是LONG_CLICKABLE,然后你会发现,只要能进入该if代码块,不管当前的action是什么,最终都返回一个true

为了验证发现ACTION_DOWN后面一系列的action都没有再执行了,接下来我们添加一个ImageView控件,并只给它注册Touch事件返回值为false,代码如下:

mImageView.setOnTouchListener(new OnTouchListener() {
	
	@Override
	public boolean onTouch(View v, MotionEvent event) {
		Log.d(TAG, " ImageView onTouch ,action " + event.getAction());
		return false;
	}
});

点击ImageView控件得到log日志如下:

 

发现在ACTION_DOWN执行完后,后面一系列的action都没有执行。原因就是ImageViewButton不同,它默认是不可点击的,因此在onTouchEvent()的无法进入到第三个if的内部,直接跳到该方法最后一行返回了false,使disPatchTouchEvent()ACTION_DOWN返回false,导致后面其它的action都无法执行了。

聪明的你又会发现为什么没有给ImageView设置Click事件呢?

public void setOnClickListener(OnClickListener l) {
	if (!isClickable()) {
		setClickable(true);
	}
	getListenerInfo().mOnClickListener = l;
}

当然代码中可能会出现给该控件注册了一个onClick事件,那么就可以通过设置控件的setClickable(false)将控件的Clickable值置为false,如下:从上面代码可知,View的源码中,只要给View设置了ClickListener,就会将View设置成Clickable,那么就一定能进入到onTouchEvent()方法中的第三个if代码块内,导致onTouchEvent()恒返回true,就会使disPatchTouchEvent()ACTION_DOWN返回true,那么ACTION_DOWN后面一些列的ACTION将会得到执行。

mImageView.setOnClickListener(new OnClickListener() {

			@Override
			public void onClick(View v) {
				Log.d(TAG, "Button onClick execute");
			}
		});
		mImageView.setClickable(false);

综上:dispatchTouchEvent则执行ACTION传递时,只有当前一个ACTIONdispatchTouchEvent()返回true,才会触发后面的ACTION这样你会发现在点击该控件的时候,该控件没有反应。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值