您的位置:首页 > 移动开发 > Android开发

Android事件分发机制(上)

2016-05-05 15:34 513 查看

1、Android事件分发简介

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

1.1、两大基础控件类型

View和ViewGroup。View即普通的控件,没有子布局的,如Button、TextView.
ViewGroup继承自View,表示可以有子控件,如Linearlayout、Listview等。

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。

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

2)onTouchEvent():用于处理触摸事件,如果事件被处理了返回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源码分析

1)dispatchTouchEvent()

事件传递的入口是View的dispatchTouchEvent()函数,所以当点击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()的源码:

2)onTouchEvent()

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返回true,onClick就不会再执行了。而如果onTouch()返回false,就会执行view的onTouchEvent()方法,而onClick()会在该方法里执行。

2.3、View事件分发总结

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



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

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

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

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

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

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

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

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

2.4、事件的层级传递

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

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

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

默认处理方式下(即按照View源码处理方式),当View被点击时,ACTION_DOWN事件就会通过Activity传递给View的dispatchTouchEvent方法。首先会调用View的dispatchTouchEvent()对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在进行事件传递的时候,只有当前一个ACTION的dispatchTouchEvent()返回true,才会触发后面的ACTION。

聪明的你肯定发现前面的例子中,在onTouch事件里面返回了false,ACTION_DOWN和ACTION_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都没有执行。原因就是ImageView和Button不同,它默认是不可点击的,因此在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传递时,只有当前一个ACTION的dispatchTouchEvent()返回true,才会触发后面的ACTION。这样你会发现在点击该控件的时候,该控件没有反应。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: