android-----事件分发机制测试系列
Posted 她说巷尾的樱花开了
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了android-----事件分发机制测试系列相关的知识,希望对你有一定的参考价值。
上一篇我们主要主要是从ViewGroup分发的角度测试了下事件分发机制,但没有涉足多少View的事件分发,也就是说我们没有为MyRelativeLayout、MyLinearLayout、以及MyButton设置Touch和Click监听事件,这一篇将来测试下View的事件分发过程,为了比较简洁的显示打印信息,我简化了布局文件,具体的布局文件代码如下:
<com.hzw.eventtest.MyRelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/myRelativeLayout"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<com.hzw.eventtest.MyButton
android:id="@+id/myButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="我的按钮" />
</com.hzw.eventtest.MyRelativeLayout>
也即布局文件图是酱紫的:
具体的测试代码就是在MainActivity和MyButton中的dispatchTouchEvent以及onTouchEvent方法中打印Log,以及在MyRelativeLayout的dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent中打印Log,并且为MyRelativeLayout、MyButton设置了onTouchListener、onLongClickListener、以及onClickListener事件监听器;
我们点击MyButton按钮,查看Logcat输出结果如下:
06-30 10:27:37.127: I/System.out(2705): MainActivity--->dispatchTouchEvent--->ACTION_DOWN
06-30 10:27:37.127: I/System.out(2705): MyRelativeLayout--->dispatchTouchEvent--->ACTION_DOWN
06-30 10:27:37.132: I/System.out(2705): MyRelativeLayout--->onInterceptTouchEvent--->ACTION_DOWN
06-30 10:27:37.132: I/System.out(2705): MyRelativeLayout--->onInterceptTouchEvent--->ACTION_DOWN--->false
06-30 10:27:37.132: I/System.out(2705): MyButton--->dispatchTouchEvent--->ACTION_DOWN
06-30 10:27:37.132: I/System.out(2705): MyButton--->onTouch--->DOWN
06-30 10:27:37.132: I/System.out(2705): MyButton--->onTouchEvent--->ACTION_DOWN
06-30 10:27:37.144: I/System.out(2705): MyButton--->onTouchEvent--->ACTION_DOWN--->true
06-30 10:27:37.144: I/System.out(2705): MyButton--->dispatchTouchEvent--->ACTION_DOWN--->true
06-30 10:27:37.144: I/System.out(2705): MyRelativeLayout--->dispatchTouchEvent--->ACTION_DOWN--->true
06-30 10:27:37.144: I/System.out(2705): MainActivity--->dispatchTouchEvent--->ACTION_DOWN--->true
06-30 10:27:37.689: I/System.out(2705): MyButton--->onLongClick
06-30 10:27:37.832: I/System.out(2705): MainActivity--->dispatchTouchEvent--->ACTION_UP
06-30 10:27:37.832: I/System.out(2705): MyRelativeLayout--->dispatchTouchEvent--->ACTION_UP
06-30 10:27:37.832: I/System.out(2705): MyRelativeLayout--->onInterceptTouchEvent--->ACTION_UP
06-30 10:27:37.832: I/System.out(2705): MyRelativeLayout--->onInterceptTouchEvent--->ACTION_UP--->false
06-30 10:27:37.832: I/System.out(2705): MyButton--->dispatchTouchEvent--->ACTION_UP
06-30 10:27:37.832: I/System.out(2705): MyButton--->onTouch--->UP
06-30 10:27:37.832: I/System.out(2705): MyButton--->onTouchEvent--->ACTION_UP
06-30 10:27:37.842: I/System.out(2705): MyButton--->onTouchEvent--->ACTION_UP--->true
06-30 10:27:37.842: I/System.out(2705): MyButton--->dispatchTouchEvent--->ACTION_UP--->true
06-30 10:27:37.842: I/System.out(2705): MyRelativeLayout--->dispatchTouchEvent--->ACTION_UP--->true
06-30 10:27:37.842: I/System.out(2705): MainActivity--->dispatchTouchEvent--->ACTION_UP--->true
06-30 10:27:37.852: I/System.out(2705): MyButton--->OnClick
如果你仔细查看输出的话,有一部分会让你觉得很奇怪的,就是输出的第12行的onLongClick方法和第24行的onClick方法是在dispatchTouchEvent方法执行结束之后才开始执行的,这一点让我感到很诧异,所以专门写了这篇博客来试着从代码层面解释下这种现象的原因,因为网上看别人的分析过程均没有涉足到我想要的部分,所以打算自己分析一次View分发过程的源码,有什么错误还请指正,源码分析结束之后我们再来看看Logcat输出或许你会明白点了;
一个事件传递到View上面首先执行的就是他的dispatchTouchEvent方法,那么很自然首先应该从View的dispatchTouchEvent开始分析:
public boolean dispatchTouchEvent(MotionEvent event) {
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
if (onFilterTouchEventForSecurity(event)) {
//noinspection SimplifiableIfStatement
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;
}
第6行判断是否过滤掉当前事件系列,什么情况下会被过滤呢?View在被遮盖的时候,onFilterTouchEventForSecurity方法会返回true,进而直接在第22行dispatchTouchEvent返回了false;如果当前View没有被遮盖的话,执行7--16行的if语句块,首先获取到ListenerInfo对象,他是View的静态内部类,这个对象主要存储的就是一些我们所设置的事件监听器了,稍微看看里面的几个属性字段:
static class ListenerInfo {
public OnClickListener mOnClickListener;
protected OnLongClickListener mOnLongClickListener;
private OnTouchListener mOnTouchListener;
}
接着走到第9行的if判断语句处,这个地方有四个判断条件,第1个li指的就是
ListenerInfo对象,第2个
li.mOnTouchListener其实是在判断是否设置Touch事件监听器,具体
li.mOnTouchListener的值等于什么呢?从下面代码中可以看出来:
public void setOnTouchListener(OnTouchListener l) {
getListenerInfo().mOnTouchListener = l;
}
这是一个public类型的方法,我们通常在程序中为某个控件设置Touch监听器就是调用的这个方法,那么其实第2个判断条件是在查看我们是否有设置Touch事件监听器,第三个条件是在查看我们当前的View是否是enable的,也就是说当前View本身是否能够接受触摸事件,第4个就是onTouch方法的返回值了,这个方法可以被重写,默认情况下是返回false的;如果这个if判断的四个条件都满足的话,执行11行,直接返回,也就是当前事件已经分发结束了,从这里可以看出View事件分发首先执行的是onTouch(当然你必须设置Touch事件监听器);如果if的四个条件中有一个是false,就会执行第14行的if语句,调用onTouchEvent来处理事件,这里我们有必要来看看
onTouchEvent方法了;
该方法是public修饰的,所以你可以在子类中重写它,方法比较长,我们截段分析:
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));
}
首先判断当前View如果本身就不支持触摸的话,进入if语句块,第2行判断当前事件是UP并且设置了PFLAG_PRESSED标志的话,则调用setPressed将标志置位,因为整个事件的最后一步就是UP了,所以我们必须在事件结束之前将设置的标志还原;
public void setPressed(boolean pressed) {
final boolean needsRefresh = pressed != ((mPrivateFlags & PFLAG_PRESSED) == PFLAG_PRESSED);
if (pressed) {
mPrivateFlags |= PFLAG_PRESSED;
} else {
mPrivateFlags &= ~PFLAG_PRESSED;
}
if (needsRefresh) {
refreshDrawableState();
}
dispatchSetPressed(pressed);
}
这里传给setPressed的参数是false,所以执行7行代码取反还原;回到onTouchEvent方法中,第7行查看View是否有设置点击和长点击,有的话返回true,没有返回false,从这里可以看出onTouchEvent的返回值是跟你View是enable还是disable没有多大关系,只要你设置了clickable或者longClickable,那么他就会返回true;
接着分析onTouchEvent下面代码:
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
查看是否有设置事件代理,有的话,则将事件交给代理处理,根据代理事件onTouchEvent方法来判断是否返回true;
接下来的onTouchEvent代码比较长,我们先来整理一个大体框架:
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
...............
...............
...............
return true;
}
return false;
可以看到只要clickable和longClickable有一个被设置就会返回true,只有在两者都没设置的情况下才会返回false,这也更加印证了onTouchEvent方法的返回值只和你有没有设置clickable和longClickable有关,和View的enable和disable没什么关系;
如果clickable和longClickable有一个被设置,那么进入if语句块中,该语句块是一个switch语句,我们按照事件的触发顺序来进行分析,即DOWN--->MOVE--->UP:
先来看DOWN部分:
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();
}
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
setPressed(true);
checkForLongClick(0);
}
break;
刚进来首先设置
mHasPerformedLongPress的值为false,这个比较关键了,用来表示是否有执行长点击事件,如果有设置长点击事件并且onLongClick方法返回true的话,这个值是会被改变成true的,等会你就看到什么原因啦,接着第9行判断当前View是否在正在滚动的控件中,在的话就满足第13行的if条件语句调用postDelay方法来延期press的反馈,为什么要这么做呢?从第11行的注释看出来是为了防止当前事件是一个滚动事件,进入if语句块之后执行第14行,设置PREPRESSED标志,这个标志表示的是prepressed状态,这个状态存在于ACTION_DOWN和真正意识到是press之间,用于识别是不是tap事件,接着第18行执行了postDelayed方法,参数ViewConfiguration.getTapTimeout()的值是150ms,这个方法的代码如下:
public boolean postDelayed(Runnable action, long delayMillis) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.postDelayed(action, delayMillis);
}
// Assume that post will succeed later
ViewRootImpl.getRunQueue().postDelayed(action, delayMillis);
return true;
}
可以看到这个方法其实是调用了handler的postDelayed方法,将action加入到了MessageQueue消息队列中,熟悉handler机制的应该知道随后调用的将是action的run方法了,也就是
mPendingCheckForTap的run方法了,
mPendingCheckForTap是CheckForTap类型的对象,具体定义如下:
private final class CheckForTap implements Runnable {
public void run() {
mPrivateFlags &= ~PFLAG_PREPRESSED;
setPressed(true);
checkForLongClick(ViewConfiguration.getTapTimeout());
}
}
run方法首先是将PREPRESSED标志置位,接着执行setPressed方法,设置PRESSED标志,这个方法在前面又出现过,只不过前面调用的是setPressed(false)而已;接着便调用
checkForLongClick来查看是否有长点击事件了,传入的参数是150ms;如果当前View不在滚动的控件中的话,则直接执行第19行的else语句,接着调用setPressed以及
checkForLongClick方法,这里执行的内容就和CheckForTap的run方法一致了,只不过传入的
checkForLongClick参数值不同而已,那么我们就该看看checkForLongClick方法了:
private void checkForLongClick(int delayOffset) {
if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
mHasPerformedLongPress = false;
if (mPendingCheckForLongPress == null) {
mPendingCheckForLongPress = new CheckForLongPress();
}
mPendingCheckForLongPress.rememberWindowAttachCount();
postDelayed(mPendingCheckForLongPress,
ViewConfiguration.getLongPressTimeout() - delayOffset);
}
}
该方法第2行判断你有没有设置longClick事件,有的话进入if语句块,首先还是将mHasPerformedLongPress设置为false,接着第9行同样调用了postDelayed方法,传入的第二个参数是
ViewConfiguration.getLongPressTimeout() - delayOffset,
ViewConfiguration.getLongPressTimeout()的默认值是500ms,从这句话我们可以看出来不管你是通过
checkForLongClick(0)还是
checkForLongClick(
delayOffset)其中
delayOffset大于0,调用
checkForLongClick方法,其实检测你是不是长点击的时间是一致的,都是500ms,你点击的时间超过500ms的话,会认为是长点击,很自然调用的是mPendingCheckForLongPress的run方法:
public void run() {
if (isPressed() && (mParent != null)
&& mOriginalWindowAttachCount == mWindowAttachCount) {
if (performLongClick()) {
mHasPerformedLongPress = true;
}
}
}
注意到这个方法第2行会判断isPressed(),什么意思呢?就是说如果你500ms之后还是处于点击状态,那么你就是长点击了,执行if语句块中的内容,第4行执行的是
performLongClick方法,而这个方法就主要是执行的我们的OnLongClickListener监听方法了,来看看里面的代码:
public boolean performLongClick() {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
boolean handled = false;
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLongClickListener != null) {
handled = li.mOnLongClickListener.onLongClick(View.this);
}
if (!handled) {
handled = showContextMenu();
}
if (handled) {
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
}
return handled;
}
同样OnLongClickListener存储在ListenerInfo对象里面,第7行执行了onLongClick方法,并且获取到返回值,回到前面的run方法第4行会判断这个返回值是true的话,执行
mHasPerformedLongPress = true语句,我们有必要说明下mHasPerformedLongPress的作用,他是用于表示你的onLongClick是否返回true的,如果没有设置LongClick监听事件或者设置了LongClick监听事件但是onLongClick方法返回false,那么mHasPerformedLongPress的值将是false,随后在UP事件判断中才会执行接下来的click点击事件,如果设置了LongClick监听事件并且onLongClick方法返回true,那么在随后的UP事件判断中将不再会执行click点击事件,从这里我们可以看出其实onLongClick方法是优先于onClick执行的,这也就解释了我们平常使用onLongClick方法有返回值而onClick方法没有返回值的问题了;这样的话DOWN事件处理结束了;
从DOWN事件的处理中,我们可以知道longclick是在它里面进行检测的,并且如果500ms之后还处于press状态的话会调用它的performLongClick,而这个方法是在子线程中调用的,所以就出现了我们上面Log输出第12行在dispatchTouchEvent返回之后才执行的结果;
接下来分析的是MOVE事件:
case MotionEvent.ACTION_MOVE:
final int x = (int) event.getX();
final int y = (int) event.getY();
// 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;
MOVE相对来说比较简单,首先是获取你当前触摸处的位置,接着第6行判断你触摸的地方是否处于当前View的边界内,不处于的话会执行7--15行代码,处于的话不做任何事,我们来看看不处于情况下做了些什么,首先执行第8行的
removeTapCallback方法,这个方法:
private void removeTapCallback() {
if (mPendingCheckForTap != null) {
mPrivateFlags &= ~PFLAG_PREPRESSED;
removeCallbacks(mPendingCheckForTap);
}
}
主要是置位PREPRESSED标志,并且从当前的MessageQueue消息队列中移出封装有mPendingCheckForTap这个线程的消息,为什么要这么做呢?因为你都已经不在我当前View的控制范围内了,我也没必要看你接下来的一些操作了;第9行如果我们设置了
PRESSED标志的话,说明在DOWN事件中也在MessageQueue里面添加了封装有监听长点击事件的Message,那么就需要调用第11行的
removeLongPressCallback方法,将该Message从MessageQueue中移出,并且13行调用setPressed方法将
PRESSED标志置位;
接下来就是UP事件了:
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);
}
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;
UP事件代码相对来说比较多,第3行的判断条件说明只要你在MOVE的过程中没有移出边界都会满足,接着第19行会判断
mHasPerformedLongPress的值,这个值只有在你设置了longclick监听事件,并且在onLongClick方法中返回true的情况下才会是true,否则均是false,这个在上面已经说过了,我们假定这里
mHasPerformedLongPress的值是false,进入if语句块,首先调用
removeLongPressCallback,从MessageQueue中移出长点击监听Message,接着第28--33行的代码比较关键,这里将是解释我们上面Log输出的重要部分,如果没有
PerformClick对象则创建,并在第31行通过post方法将
PerformClick对象添加到MessageQueue消息队列中,接下来将是执行
PerformClick的run方法了:
private final class PerformClick implements Runnable {
public void run() {
performClick();
}
}
很明显PerformClick是一个线程,在他的run方法里面也会执行performClick,也就是说不管第31行post方法有没有执行成功都会执行performClick方法的,那么这里为什么要用到post通过子线程来执行performClick而不是直接执行performClick呢?根据官方的注释看到这样做的目的是为了在click执行之前让view上面的其他visual 状态能够更新,来看看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;
}
可以看到如果我们设置了OnClickListener监听器的话,会执行第7行代码,也就执行了我们的onclick方法了;因为如果我们调用post的话,performClick是执行在
PerformClick类型的子线程中的,所以我们上面的Log
输出会出现onClick方法在dispatchTouchEvent事件返回之后才执行的情况了;UP事件后面的一些操作是用于状态置位的,我们再次不做过多牵涉;
这样的话,View的事件分发源码分析完毕了,我们做个小结以此来解释上面的Log输出:
(1)View中如果我们设置了onTouchListener、onLongClickListener以及onClickListener的话,三者的执行顺序是onTouch--->onLongClick--->onClick;
(2)如果我们在onLongClick方法中返回true的话,那么随后的onClick方法将不再会执行;
(3)我们的onLongClick方法以及onClick方法可能会在dispatchTouchEvent方法返回之后才去执行,原因在于onLongClick方法是在CheckForLongPress类型的子线程中执行的,onClick是在PerformClick类型的子线程中执行的,也即解释了上面Log输出第12行出现在第9行之后,以及第24行出现在第21行之后的问题;
好了,这篇先到这里了,下篇从实例测试的角度进行不同情况下的分析;
以上是关于android-----事件分发机制测试系列的主要内容,如果未能解决你的问题,请参考以下文章
“framework必会”系列:Android Input系统事件分发机制