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

Android:View事件分发机制详解

2016-04-19 16:20 796 查看
Android事件传递机制绝对不是三言两语就能说得清的,在网上查了相关资料,觉得大部分都没有讲的很清楚透彻,写本文的目的就是让更多的开发者进从FrameWork层到Application层一步步深入Android事件传递机制的原理,今天先小小试牛刀,主要是讲View的事件传递机制原理,下一篇会将更复杂的控件ViewGroup事件的传递机制。

Android事件构成

在Android中,事件主要包括点按、长按、拖拽、滑动等,点按又包括单击和双击,另外还包括单指操作和多指操作。所有这些都构成了Android中的事件响应。总的来说,所有的事件都由如下三个部分作为基础:

按下(ACTION_DOWN)

移动(ACTION_MOVE)

抬起(ACTION_UP)

所有的操作事件首先必须执行的是按下操作(ACTIONDOWN),之后所有的操作都是以按下操作作为前提,当按下操作完成后,接下来可能是一段移动(ACTIONMOVE)然后抬起(ACTION_UP),或者是按下操作执行完成后没有移动就直接抬起。这一系列的动作在Android中都可以进行控制。

我们知道,所有的事件操作都发生在触摸屏上,而在屏幕上与我们交互的就是各种各样的视图组件(View),在Android中,所有的视图都继承于View,另外通过各种布局组件(ViewGroup)来对View进行布局,ViewGroup也继承于View。所有的UI控件例如Button、TextView都是继承于View,而所有的布局控件例如RelativeLayout、容器控件例如ListView都是继承于ViewGroup。所以,我们的事件操作主要就是发生在View和ViewGroup之间,那么View和ViewGroup中主要有哪些方法来对这些事件进行响应呢?记住如下3个方法,我们通过查看View和ViewGroup的源码可以看到:

View.java

public boolean dispatchTouchEvent(MotionEvent event)

public boolean onTouchEvent(MotionEvent event)

ViewGroup.java

public boolean dispatchTouchEvent(MotionEvent event)

public boolean onTouchEvent(MotionEvent event)

public boolean onInterceptTouchEvent(MotionEvent event)

在View和ViewGroup中都存在dispatchTouchEvent和onTouchEvent方法,但是在ViewGroup中还有一个onInterceptTouchEvent方法,那这些方法都是干嘛的呢?别急,我们先看看他们的返回值。这些方法的返回值全部都是boolean型,为什么是boolean型呢,看看本文的标题,“事件传递”,传递的过程就是一个接一个,那到了某一个点后是否要继续往下传递呢?你发现了吗,“是否”二字就决定了这些方法应该用boolean来作为返回值。没错,这些方法都返回true或者是false。在Android中,所有的事件都是从开始经过传递到完成事件的消费,这些方法的返回值就决定了某一事件是否是继续往下传,还是被拦截了,或是被消费了。

接下来就是这些方法的参数,都接受了一个MotionEvent类型的参数,MotionEvent继承于InputEvent,用于标记各种动作事件。之前提到的ACTIONDOWN、ACTIONMOVE、ACTION_UP都是MotinEvent中定义的常量。我们通过MotionEvent传进来的事件类型来判断接收的是哪一种类型的事件。到现在,这三个方法的返回值和参数你应该都明白了,接下来就解释一下这三个方法分别在什么时候处理事件。

dispatchTouchEvent方法用于事件的分发,Android中所有的事件都必须经过这个方法的分发,然后决定是自身消费当前事件还是继续往下分发给子控件处理。返回true表示不继续分发,事件没有被消费。返回false则继续往下分发,如果是ViewGroup则分发给onInterceptTouchEvent进行判断是否拦截该事件。

onTouchEvent方法用于事件的处理,返回true表示消费处理当前事件,返回false则不处理,交给子控件进行继续分发。

onInterceptTouchEvent是ViewGroup中才有的方法,View中没有,它的作用是负责事件的拦截,返回true的时候表示拦截当前事件,不继续往下分发,交给自身的onTouchEvent进行处理。返回false则不拦截,继续往下传。这是ViewGroup特有的方法,因为ViewGroup中可能还有子View,而在Android中View中是不能再包含子View的(iOS可以)。

Android事件处理

比如一个Activity页面有一个Button 按钮,要想为该按钮设置onClick事件,只需简单的使用下面几句代码即可:

mTestButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Log.d(TAG, "onClick execute");
}
});


有了上面click事件,点击Button时就会执行上述onClick方法中的具体实现,这个我们都知道,但是如果我再为button添加一个OnTouchListener,代码实现也很简单,如下:

mTestButton.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
Log.d(TAG, "onTouch execute, action event " + motionEvent.getAction());
return false;
}
});


此时,我们现在分析一下,是onTouch先执行,还是onClick执行,我想一般人都能立即回答出,肯定是onTouch事件先执行,但是为什么会这样呢?

其中的原理是什么,接下来我从FrameWork 源码去探寻一下整个事件的执行流程和原理:

我们知道Button ,TextView等基础控件的基类都是View,只要你触摸到了任何一个控件,就一定会调用该控件的dispatchTouchEvent方法。那当我们去点击按钮的时候,就会去调用Button类(实际上是基类View)里的dispatchTouchEvent方法,所以接下来看View源码中dispatchTouchEvent()方法的具体实现

public boolean dispatchTouchEvent(MotionEvent event) {
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}


分析上述代码,第2行如果三个条件都为真的话,就返回true,否则执行onTouchEvent:

第一个条件mOnTouchListener!=null,这个条件就是如果设置了OnTouchListener就会为true,否则是false;

第二个条件(mViewFlags & ENABLED_MASK) == ENABLED是判断当前点击的控件是否是enable的,按钮默认都是enable的,因此这个条件恒定为true;

第三个条件就比较复杂了,mOnTouchListener.onTouch(this, event),这个其实就是去回调控件注册touch事件时的onTouch方法。也就是说如果我们在onTouch方法里返回true,就会让这三个条件全部成立,从而整个方法直接返回true。

在 onTouch(View v, MotionEvent event)中会处理一系列的ACTION_DOWN,ACTION_MOVE,ACTION_UP.

该onTouch()方法返回true表示事件已经消耗,返回false表示事件未消耗.就会再去执行onTouchEvent(event)方法。

比如在处理ACTION_DOWN时返回true才会继续分发ACTION_MOVE事件

比如在处理ACTION_MOVE时返回true才会继续分发ACTION_UP事件

比如在处理ACTION_DOWN时返回false,那么后续的ACTION_MOVE,ACTION_UP就不会再继续分发.

我们在代码中也就无法捕捉到ACTION_MOVE,ACTION_UP这两个Action了.

从该dispatchTouchEvent()的源码也可以看出

onTouch(this,event)和 onTouchEvent(event)的区别和关系:

1 先调用onTouch()后调用onTouchEvent()

2 在onTouch()方法中处理了Touch事件,即处理一系列的ACTION_DOWN,ACTION_MOVE,ACTION_UP事件

, 返回false时表示事件(每个单独的ACTION_DOWN,ACTION_MOVE,ACTION_UP都叫一个事件,并不是说这三者联系在一起才是一个事件)

,未被消耗才会调用onTouchEvent(event).

3 在onTouchEvent(event)中的ACTION_UP事件里会调用performClick()处理OnClick点击事件!!!!

4 所以可知:

4.1 Touch事件先于Click事件发生和处理,且注意onTouch()方法默认返回为false.

4.2 只有在onTouch()返回false时(即事件未被消耗)才会调用onTouchEvent()

4.3 在onTouchEvent()中的ACTION_UP事件会调用performClick()处理OnClick点击事件.

5 参见下面的onTouchEvent()源码,请注意第三个if判断,这个if判断很重要!!!!!!!

5.1 在该if条件中判断该控件是否是可点击的(CLICKABLE)或者是否是可以长按的(LONG_CLICKABLE).

5.2 如果满足CLICKABLE和LONG_CLICKABLE中任一条件则始终会返回true给onTouchEvent()方法

5.3 如果CLICKABLE和LONG_CLICKABLE这两个条件都不满足则返回false给onTouchEvent()方

接下来我们结合上面的具体例子,来分析一下这个过程,首先会执行dispatchTouchEvent(MotionEvent event) ,所以onTouch方法肯定是早于onClick方法的,如果在onTouch里返回false,就会出现下面的现象:

04-19 17:33:20.846 7795-7795/? D/MainActivity: onTouch execute, action event 0
04-19 17:33:20.856 7795-7795/? D/MainActivity: onTouch execute, action event 2
04-19 17:33:20.866 7795-7795/? D/MainActivity: onTouch execute, action event 1
04-19 17:33:20.866 7795-7795/? D/MainActivity: onClick execute


public static final int ACTION_DOWN             = 0;单点触摸动作
public static final int ACTION_UP               = 1;单点触摸离开动作
public static final int ACTION_MOVE             = 2;触摸点移动动作
public static final int ACTION_CANCEL           = 3;触摸动作取消
public static final int ACTION_OUTSIDE          = 4;触摸动作超出边界
public static final int ACTION_POINTER_DOWN     = 5;多点触摸动作
public static final int ACTION_POINTER_UP       = 6;多点离开动作


即先执行了onTouch,再执行了onClick事件,而且onTouch执行了三次,一个是action_down,一个是action_up事件;

如果onTouch里返回true,则出现下面的现象:

04-19 17:39:00.127 10574-10574/? D/MainActivity: onTouch execute, action event 0
04-19 17:39:00.207 10574-10574/? D/MainActivity: onTouch execute, action event 2
04-19 17:39:00.207 10574-10574/? D/MainActivity: onTouch execute, action event 1


结果是onClick事件没有执行了,原因是如果onTouch返回true的话,则dispatchEvent(MotionEvent event)方法直接返回true了,相当于不往下传递事件了,所以onClick不会执行,相反如果onTouch返回false的话(此时会执行onClick方法),则会执行 onTouchEvent(MotionEvent event)方法,由此可以得出这样一个结论,onClick事件的具体调用执行肯定是在onTouchEvent(MotionEvent event)方法源码中,接下来分析一下该函数的源码:

public boolean onTouchEvent(MotionEvent event) {
final int viewFlags = mViewFlags;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
// 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;
}
}
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;
if ((mPrivateFlags & 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 (!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) {
mPrivateFlags |= PRESSED;
refreshDrawableState();
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
break;
case MotionEvent.ACTION_DOWN:
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPrivateFlags |= PREPRESSED;
mHasPerformedLongPress = false;
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
break;
case MotionEvent.ACTION_CANCEL:
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
removeTapCallback();
break;
case MotionEvent.ACTION_MOVE:
final int x = (int) event.getX();
final int y = (int) event.getY();
// Be lenient about moving outside of buttons
int slop = mTouchSlop;
if ((x < 0 - slop) || (x >= getWidth() + slop) ||
(y < 0 - slop) || (y >= getHeight() + slop)) {
// Outside button
removeTapCallback();
if ((mPrivateFlags & PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();
// Need to switch from pressed to not pressed
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
}
}
break;
}
return true;
}
return false;
}


虽然源码有点多,但是我们只重点关注关键代码,在38行我们看到了代码:performClick();这个方法从名字表义来看就是OnClick方法的调用,我们进入到该方法中去看一探究竟,是否执行了OnClick方法呢?

public boolean performClick() {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
if (mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
mOnClickListener.onClick(this);
return true;
}
return false;
}


从上述代码可以看到,只要mOnClickListener不是null,就会去调用它的onClick方法,那mOnClickListener又是在哪里赋值的呢?经过分析后找到如下方法:

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


而上述这个方法就是我们在Application层经常使用的方法,即我们给button 设置点击事件的时候就会调用该方法了,分析到这了,我们知道了OnClick方法确实是在OnTouchEvent方法中,那么除了要设置 OnClickListener,调用onClick的条件又是什么呢?我们从38行代码往前推,从第14行可以分析出:

只要该控件是可点击的或者是长按类型的,则会进入到MotionEvent.ACTION_UP这个分支当中 ,然后经过各种条件判断,则会进入到38行的performClick()方法中。

至此,一切都清晰明白了!当我们通过调用setOnClickListener方法来给控件注册一个点击事件时,就会给mOnClickListener赋值。然后每当控件被点击时或者长按时,都会在performClick()方法里回调被点击控件的onClick方法。

经验之谈:

关于OnTouchEvent(MotionEvent事件)事件的层级传递。我们都知道如果给一个控件注册了touch事件,每次点击它的时候都会触发一系列的ACTION_DOWN,ACTION_MOVE,ACTION_UP等事件。这里需要注意,如果你在执行ACTION_DOWN的时候返回了false,后面一系列其它的action就不会再得到执行了。简单的说,就是当dispatchTouchEvent在进行事件分发的时候,只有前一个action返回true,才会触发后一个action。

那我们可以换一个控件,将按钮替换成ImageView,然后给它也注册一个touch事件,并返回false。如下所示:

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


运行一下程序,点击ImageView,你会发现结果如下:



在ACTION_DOWN执行完后,后面的一系列action都不会得到执行了。这又是为什么呢?因为ImageView和按钮不同,它是默认不可点击的,因此在onTouchEvent的第14行判断时无法进入到if的内部,直接跳到第91行返回了false,也就导致后面其它的action都无法执行了。

Button默认情况下就是CLICKABLE和LONG_CLICKABLE的,但是ImageView在 默认情况下CLICKABLE和LONG_CLICKABL均为不可用的.

所以在用Button和ImageView分别实验OnTouchListener和OnClickListener是有区别的.

再次提醒注意:onTouch()方法默认返回为false.

1 Button做实验分析dispatchTouchEvent().mOnTouchListener.onTouch()返回false(默认值),所以dispatchTouchEvent()

如上源码中的if不满足,于是继续调用onTouchEvent(event)时由于Button满足CLICKABLE和LONG_CLICKABLE

所以最后返回给dispatchTouchEvent()的是true,即继续事件的分发.

所以可以捕获到一系列的:ACTION_DOWN,ACTION_MOVE,ACTION_UP.

这里就解释了为什么在Button中虽然onTouch()返回false(默认值)但是事件分发还在继续!!!!!!!!!!!!!

2 用ImageView做实验分析dispatchTouchEvent().

mOnTouchListener.onTouch()返回false(默认值),所以dispatchTouchEvent()

如上源码中的if不满足,在调用onTouchEvent(event)时由于ImageView不满足CLICKABLE和LONG_CLICKABLE

中任何一个所以最后返回给dispatchTouchEvent()的是false,即终止事件的分发.所以对于ImageView只有

ACTION_DOWN没有ACTION_MOVE和ACTION_UP

这里就解释了为什么在ImageView中在onTouch()返回里false(默认值)就终止了事件分发!!!!!!!!!!!!!

如何才可以使ImageView像Button那样”正规的”事件分发,有如下两个方法:

1 为ImageView设置setOnTouchListener,且在其onTouch()方法中返回true而不是默认的false.

2 为ImageView设置android:clickable=”true”或者ImageView设置OnClickListener.就是说让ImageView变得可点击.

好了,关于View的事件分发,我想讲的东西全都在这里了。现在我们再来回顾一下开篇时提到的那三个问题,相信每个人都会有更深一层的理解。

1. onTouch和onTouchEvent有什么区别,又该如何使用?

从源码中可以看出,这两个方法都是在View的dispatchTouchEvent中调用的,onTouch优先于onTouchEvent执行。如果在onTouch方法中通过返回true将事件消费掉,onTouchEvent将不会再执行。

另外需要注意的是,onTouch能够得到执行需要两个前提条件,第一mOnTouchListener的值不能为空,第二当前点击的控件必须是enable的。因此如果你有一个控件是非enable的,那么给它注册onTouch事件将永远得不到执行。对于这一类控件,如果我们想要监听它的touch事件,就必须通过在该控件中重写onTouchEvent方法来实现。

2. 为什么给ListView引入了一个滑动菜单的功能,ListView就不能滚动了?

如果你阅读了Android滑动框架完全解析,教你如何一分钟实现滑动菜单特效 这篇文章,你应该会知道滑动菜单的功能是通过给ListView注册了一个touch事件来实现的。如果你在onTouch方法里处理完了滑动逻辑后返回true,那么ListView本身的滚动事件就被屏蔽了,自然也就无法滑动(原理同前面例子中按钮不能点击),因此解决办法就是在onTouch方法里返回false。

3. 为什么图片轮播器里的图片使用Button而不用ImageView?

提这个问题的朋友是看过了Android实现图片滚动控件,含页签功能,让你的应用像淘宝一样炫起来 这篇文章。当时我在图片轮播器里使用Button,主要就是因为Button是可点击的,而ImageView是不可点击的。如果想要使用ImageView,可以有两种改法。第一,在ImageView的onTouch方法里返回true,这样可以保证ACTION_DOWN之后的其它action都能得到执行,才能实现图片滚动的效果。第二,在布局文件里面给ImageView增加一个android:clickable=”true”的属性,这样ImageView变成可点击的之后,即使在onTouch里返回了false,ACTION_DOWN之后的其它action也是可以得到执行的。

资料

/article/1562152.html

/article/1379267.html

http://www.infoq.com/cn/articles/android-event-delivery-mechanism/

/article/1336234.html
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: