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

Android自定义view之事件传递机制

2017-08-08 23:27 417 查看

Android自定义view之事件传递机制

在上一篇文章《Android自定义view之measure、layout、draw三大流程》中,我们探讨了一下view的显示过程。不太熟悉的同学可以看下上篇文章巩固一下。本篇我们将一起探讨一下Android的事件分发机制,也就是触摸事件的流程。对于一个view来说,对动作的控制和显示一样重要。

本文一些知识点来自于《Android开发艺术探索》,在此感谢作者。文章中如有纰漏,欢迎留言讨论。

本文将会由浅入深讲解,事件分发机制不过是几个函数而已,只是其中的细节比较繁杂。控件分为两种:View和ViewGroup,事件分发流程有略微不同。

0. View的事件:MotionEvent类

开始之前,我们首先需要了解下包装事件的类:MotionEvent。Android的触摸事件是包装在这个类的对象之中,通过这个类,我们可以获取事件的各种信息,比如坐标值、事件发生时间、事件类型等。下面列举一些常用的方法:

(1) public final float getRawX() / getRawX()

这两个方法返回的是触摸点在屏幕上的绝对坐标,坐标值相对于屏幕而言。

(2) public final float getY() / getY(int index) / getX() / getX(int index)

返回触摸点基于该View的坐标值,有参数的方法则会返回某个点的坐标值,无参数的方法返回index为0的点的坐标值。这是针对多点触控。index值范围从0到getPointerCount() - 1。

(3) public final float getAction() / getActionMasked()

返回事件类型。
getAction
返回4种常用类型:ACTION_DOWN、ACTION_MOVE、ACTION_UP、ACTION_CANCEL。
getActionMasked
则可以多返回两种:ACTION_POINTER_DOWN、ACTION_POINTER_UP,它们代表是多点触控时有其他手指落下或抬起。某些时候,比如滚动,为了防止抬起落下多根手指时出现跳动,我们是需要检测并计算多点触控的。因此推荐直接用
getActionMasked


(4) public final void offsetLocation(float deltaX, float deltaY)

将一个事件中的坐标值进行位移变换。这个通常是在自定义滚动控件的时候会用到。由于滚动有两种方式,一种是改变子控件的位置,另一种就是利用方法
setScrollY(int value) / setScrollX(int value)
,这两个方法会影响View类中的
mScrollX / mScrollY
两个属性,而这两个属性会影响View在分发事件以及绘制时的行为。简而言之,我们可以把View看做一个很大的画布,而我们能看到的部分其实就是一个屏幕大小的窗口,
mScrollX / mScrollY
则决定这个窗口在画布上的位置。这都是后话。

以上就是MotionEvent类中的主要内容。另外需要注意的是事件流,事件序列就是从触摸屏幕开始,到所有手指离开屏幕,其中会包含移动、另外的手指落下、抬起,这就是一个事件流。所以事件序列总是以ACTION_DOWN开始,以ACTION_UP结束。另外需要注意的是,CPU的处理速度很快,那些你以为很快的点击只是点击而已,其实基本绝大多数的点击都会有
ACTION_MOVE
的,在处理事件的时候尤其注意。

1. View的事件分发流程

首先了解一下View类的事件分发流程,毕竟View类是所有控件的父类。由于View类的源码比较繁杂,我们就直接列出和事件分发有关的函数。

(1)
public boolean dispatchTouchEvent(MotionEvent event)

最关键的就是
public boolean dispatchTouchEvent(MotionEvent event)
这个函数,它是负责分发事件的,当一个事件到达一个view,首先调用的就是这个函数。在View类中它的注释是

/**
* Pass the touch screen motion event down to the target view, or this
* view if it is the target.
*
* @param event The motion event to be dispatched.
* @return True if the event was handled by the view, false otherwise.
*/


很简单的功能,将一个事件分发下去,如果它自己就是目标view,那么就它自己消化这个事件。参数是要分发的事件,返回true时代表它或者它的子view消化了这个事件,返回false代表它以及它的子view都不消化这个事件。由于这里是View,因此不会有子view存在,因此它只负责检查自己是否能消化这个事件。所以将它简化后我们能得到下面的流程伪代码:

public boolean dispatchTouchEvent(MotionEvent event)
{
boolean result = false;
...
if(onTouchListener != null)
{
result = onTouchListener.onTouchEvent(event);
}
if(!result && onTouchEvent(event))
{
result = true;
}
...
return result;
}


这便是一个简化的流程,其他的部分都省略掉了,毕竟现在的关注点不是那些。我们可以看到,首先这个函数会检查View的onTouchListener,如果它不为空,那么就将事件传递给它处理。如果它返回了true,在下面的步骤中就不会调用onTouchEvent,最后返回result。如果它返回了false,那么还会调用onTouchEvent,如果onTouchEvent返回了false,那么最后result就是false,否则result为true。

然后看一下OnTouchListener这个接口,它其实只有
onTouch
一个函数:

/**
* Interface definition for a callback to be invoked when a touch event is
* dispatched to this view. The callback will be invoked before the touch
* event is given to the view.
*/
public interface OnTouchListener {
/**
* Called when a touch event is dispatched to a view. This allows listeners to
* get a chance to respond before the target view.
*
* @param v The view the touch event has been dispatched to.
* @param event The MotionEvent object containing full information about
*        the event.
* @return True if the listener has consumed the event, false otherwise.
*/
boolean onTouch(View v, MotionEvent event);
}


注释写得很明白。这个接口对象如果不为空,那么它就会在调用
onTouchEvent
之前被调用。其实这也就是给了我们一个在view对事件进行反应之前来处理事件的机会,如果我们在这个接口中返回true,即消化这个事件,那么view就不会对事件作出反应了,同样的,我们也可以在此之前对事件进行加工来达到各种效果。

接下来看
onTouchEvent
,毕竟
OnTouchListener
这么针对它了,那它的地位肯定非常重要。

(2)
public boolean onTouchEvent(MotionEvent event)

这个函数相比于第一个就不是很容易看明白了,不过就算源码看不明白,咱们也不能放过注释。

/**
* Implement this method to handle touch screen motion events.
* <p>
* If this method is used to detect click actions, it is recommended that
* the actions be performed by implementing and calling
* {@link #performClick()}. This will ensure consistent system behavior,
* including:
* <ul>
* <li>obeying click sound preferences
* <li>dispatching OnClickListener calls
* <li>handling {@link AccessibilityNodeInfo#ACTION_CLICK ACTION_CLICK} when
* accessibility features are enabled
* </ul>
*
* @param event The motion event.
* @return True if the event was handled, false otherwise.
*/


翻译:实现这个函数来处理触摸事件。如果要在这个函数中判断点击动作,推荐使用
performClick()
函数来进行点击操作,因为它可以确保一些系统响应,包括点击音效、调用
OnClickListener
等。

很简单明了,它就是view真正消化触摸事件并作出响应的地方。其实一般来讲,一个普通的
onTouchEvent
函数内部可能会是如下的结构:

public boolean onTouchEvent(MotionEvent event) {

boolean result = true; //or false
switch (event.getAction())
{
case MotionEvent.ACTION_DOWN
...
break;
case MotionEvent.ACTION_MOVE:
...
break;
case MotionEvent.ACTION_UP:
...
break;
...
}
return result;
}


其实就是对触摸的不同动作来响应,比如我们会在View类中看到
setPress
函数,它一般就是在ACTION_DOWN时调用,来反馈控件被按下的状态。刚才提到的
performClick()
函数则是在ACTION_UP时调用。

接下来我们需要看一下
performClick()
函数:

/**
* Call this view's OnClickListener, if it is defined.  Performs all normal
* actions associated with clicking: reporting accessibility event, playing
* a sound, etc.
*
* @return True there was an assigned OnClickListener that was called, false
*         otherwise is returned.
*/
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}

sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
return result;
}


可以清楚看到它调用了
OnClickListener.onClick
,同时也有
playSoundEffect


现在我们就基本清楚了View类的事件分发流程,也知道了我们平时用的OnClickListener等监听器是在何处被调用的。但是对于自定义View来说,我们通常是会重写
onTouchEvent
函数的,所以其中的细节往往比较麻烦,包括何时判断为点击、长按和双击等操作,以及相应的操作造成的View的音效、视觉反馈等。而这些都只能靠我们自己。

以上基本就是View类的事件分发过程。接下来探索ViewGroup类的事件分发流程。相比于View类,ViewGroup会比较复杂些,因为它不但自己可以消耗事件,也要负责将事件传递给自己的子View。

2. ViewGroup的事件分发流程

由于是View类的子类,因此方法上肯定大同小异。我们还是先从
dispatchTouchEvent(MotionEvent event)
开始看起。

(1)
public boolean dispatchTouchEvent(MotionEvent event)

相比于View类,ViewGroup中这个方法就复杂得多。像之前一样,我们就直接抽取其中的主要逻辑来看。

@Override
public boolean dispatchTouchEvent(MotionEvent ev)
{
boolean consumed = false;
if(onInterceptTouchEvent(ev))
{
consumed = super.dispatchTouchEvent(ev);
}else
{
consumed = dispatchTouchEventToChild(ev);
}
return consumed;
}


这就是其中的主要逻辑。我们可以看到,事件分发有两条路,一条是ViewGroup自己消化,也就是
super.dispatchTouchEvent(ev)
,另一条则是分发给子view,
dispatchTouchEventToChild(ev)
(需要注意的是源码里并没这个方法,这里只是伪代码)。而决定事件去向的,明显就是
onInterceptTouchEvent(ev)
这个方法了。这个方法在View类中并没有,下面看一下它的源码和注释。

(2)
public boolean dispatchTouchEvent(MotionEvent event)

/**
* Implement this method to intercept all touch screen motion events.  This
* allows you to watch events as they are dispatched to your children, and
* take ownership of the current gesture at any point.
*
* <p>Using this function takes some care, as it has a fairly complicated
* interaction with {@link View#onTouchEvent(MotionEvent)
* View.onTouchEvent(MotionEvent)}, and using it requires implementing
* that method as well as this one in the correct way.  Events will be
* received in the following order:
*
* <ol>
* <li> You will receive the down event here.
* <li> The down event will be handled either by a child of this view
* group, or given to your own onTouchEvent() method to handle; this means
* you should implement onTouchEvent() to return true, so you will
* continue to see the rest of the gesture (instead of looking for
* a parent view to handle it).  Also, by returning true from
* onTouchEvent(), you will not receive any following
* events in onInterceptTouchEvent() and all touch processing must
* happen in onTouchEvent() like normal.
* <li> For as long as you return false from this function, each following
* event (up to and including the final up) will be delivered first here
* and then to the target's onTouchEvent().
* <li> If you return true from here, you will not receive any
* following events: the target view will receive the same event but
* with the action {@link MotionEvent#ACTION_CANCEL}, and all further
* events will be delivered to your onTouchEvent() method and no longer
* appear here.
* </ol>
*
* @param ev The motion event being dispatched down the hierarchy.
* @return Return true to steal motion events from the children and have
* them dispatched to this ViewGroup through onTouchEvent().
* The current target will receive an ACTION_CANCEL event, and no further
* messages will be delivered here.
*/
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}


好家伙,代码没多少全是注释。我给大家把注释翻译在下面:

翻译:实现这个方法拦截所有从屏幕上传来的触摸事件。这允许你监测所有传递到子view的事件,并且可以在任何节点获取对事件的掌控。

使用这个方法时需要注意,因为它和
OnTouchEvent
方法具有相互的作用,并且需要和这个方法一样正确地实现它。事件将会按照如下的顺序被接收:

1. 你将会在本方法中接收到落下事件(ACTION_DOWN)

2. 落下事件将会被该ViewGroup的子view处理。或者由你自己的
onTouchEvent()
方法处理;这意味着你应该实现
onTouchEvent()
并且使之返回true,这样你就可以收到这个手势剩下的事件(而不是指望父view来处理它)。同时,如果你在
onTouchEvent()
中返回true,那么你就不会在
onInterceptTouchEvent()
中收到任何接下来的事件,所有的事件都会在你的
onTouchEvent()
中像往常一样处理。

3. 一旦你从这里返回false,接下来的所有事件都会被首先交到这里,然后才交给目标View的
onTouchEvent()
方法。

4. 如果你从这里返回true,你就不会在这个方法里收到接下来的任何事件:目标子view也会收到这个事件但是动作是
ACTION_CANCEL
,并且接下来所有事件都会被直接提交到你的
onTouchEvent()
方法中而不会出现在这里。

返回值:true意味着你将会拦截从此开始所有的事件并将事件发送到该ViewGroup的
onTouchEvent()
中,当前的目标子view将会收到
ACTION_CANCEL
,并且之后也不会有事件被发送到这里。

说了那么多,第2条到第4条有点绕(本人英语力不强啊)。其实简单说,返回true代表你要拦截这个事件自己处理,返回false代表你不拦截这个事件,可以把它分发到子view中。你可以在任何时候从这个方法中拦截事件,一旦你拦截了,那么该事件和之后的事件都会被直接交给该ViewGroup的
onTouchEvent()
处理并且不会再出现在这里,意味着接下来事件分发就不会再调用
onInterceptTouchEvent()
了;并且之前已经收到事件的子view会收到一个ACTION_CANCEL以做出响应。如果你没有拦截,事件就会被分发到子view中,并且在整个事件流过程中,分发事件时这个函数都会被调用,意味着你仍然有机会在任何时候拦截事件。

以上说的事件以及过程是指一个事件流中,每当新的事件流发生时(以ACTION_DOWN开始),所有过程是重新来过的,之前是否拦截不会对后面的过程产生影响,这也很好理解。

然而还是有一种特殊情况是需要我们考虑的,就是ViewGroup的事件发生的区域中没有子view时会怎么办。此时即使ViewGroup在
onInterceptTouchEvent()
中返回了false,那么事件仍然还是会交给ViewGroup自己处理。ViewGroup的
dispatchTouchEvent()
的流程中,会先找到事件的坐标对应的子View,然后调用
dispatchTransformedTouchEvent()
来执行真正的事件分发逻辑,该函数声明如下:

/**
* Transforms a motion event into the coordinate space of a particular child view,
* filters out irrelevant pointer ids, and overrides its action if necessary.
* If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.
*/
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits)


从注释很清楚可以看到,它会对事件执行变换操作,然后分发下去。如果没有对应的子view,那么事件就会分发给ViewGroup本身来处理。

以上基本就是ViewGroup的事件分发流程。按照顺序我们接下来要看
onTouchEvent()
方法,但在ViewGroup类中并没有重写这个方法,而是沿用了View类的(super.dispatchTouchEvent(ev)),这也很好理解,毕竟View类已经把消耗事件写好了,ViewGroup就没必要自己再写一遍。

onInterceptTouchEvent()
onTouchEvent()
方法联系得非常紧密,大家在重写这两个方法的时候一定要控制好。

3. 特殊情况及注意事项

这一部分的内容说实话在正常情况下比较少发生(如果你写代码的时候考虑得够周全的话)。不过肯定有车到山前的时候,所以我列出了供大家遇到的时候有路可循。

事件的传递总是由外向内的,即从父元素传递给子元素。但是子元素可以通过调用
requestDisallowInterceptTouchEvent(boolean)
方法来干预父元素的分发流程。顾名思义,传入true代表我们要求父控件不要拦截这个事件。详细用法大家可以自行再查。

好吧,很特殊的情况其实我也没想到有别的,基本很多的情况在上面我们探索源码的过程中就讲得很明白了。大家灵活运用一定可以应对各种情况。

4. 总结

事件分发流程对于View类和ViewGroup类是不同的。

对于View

事件到达一个View时,首先会调用
dispatchTouchEvent(MotionEvent event)
。然后这个方法会先调用
OnTouchListener.onTouch()
方法(如果注册过OnTouchListener的话),如果OnTouchListener不消耗事件,那么会接着调用View的
onTouchEvent()
方法。

对于ViewGroup

事件到达一个ViewGroup时,同样先调用
dispatchTouchEvent(MotionEvent event)
方法,不过这个方法和View类中的有不同。该方法会首先调用
onInterceptTouchEvent()
方法是否拦截这个事件。如果拦截,则交由该ViewGroup自己的
onTouchEvent()
方法(其实就是走了super.dispatchTouchEvent(ev)流程,和上面View类处理事件的流程一样了)。如果不拦截,则会将这个事件分发给子view。所以对于ViewGroup来说,我们可以在两个地方拦截事件:一是
onInterceptTouchEvent()
,二是
OnTouchListener.onTouch()
,只要在这两个方法中的任何一个返回true,ViewGroup的
onTouchEvent()
都不会被调用。(ViewGroup:我好可怜(:з」∠))

以上就是Android的基本的事件分发流程了。看起来其实比较容易,也比上一章绘制显然篇幅小得多,不过用起来就会知道坑其实还是挺多的。后续我会写几篇自定义view的小例子,一步一步走过这些坑。

如果有错误或者疑问,欢迎大家留言讨论。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  android android开发