Android事件分发和消费机制理解

来源:互联网 发布:怎么下载发票软件 编辑:程序博客网 时间:2024/05/16 15:29

首先要明白事件指的是触摸事件(Android里封装到了MotionEvent中进行传递),即用户手指触到屏幕到最终手指离开的全过程,在此过程中会发生一系列的事件,手指按下(ACTION_DOWN)标志事件的开始,其中会有一系列的ACTION_MOVE事件(不是必须的),最后以手指抬起(ACTION_UP)作为事件结束的标志。

先来看一看与事件分发和消费有关的回调函数:

1.事件分发。事件的传递从该方法开始。

public boolean dispatchTouchEvent(MotionEvent ev)

2.事件拦截。会在dispatchTouchEvent方法中进行调用。在该方法中如果返回true,则该事件会被此View拦截。

public boolean onInterceptTouchEvent(MotionEvent ev)

3.事件消耗。会在dispatchTouchEvent方法中进行调用。该方法是View消耗事件的地方,返回true表示事件已经消耗掉,那么后续的相关事件都会传递到该View。

public boolean onTouchEvent(MotionEvent event)

再来看一看哪些类中有以上三个方法。

表1 方法分布表


有了以上的初步了解,让我们从整体的流程来看一下事件是怎么传递到某个View的。以两个嵌套的RelativeLayout+TextView为例,进行讲解,如下图。


图1 视图举例

当手指触摸绿色部分时,事件的传递过程如下:

05-03 09:00:17.969 12565-12565/com.cqupt.mycustomview D/demo: dispatchTouchEvent in red05-03 09:00:17.969 12565-12565/com.cqupt.mycustomview D/demo: onInterceptTouchEvent in red05-03 09:00:17.969 12565-12565/com.cqupt.mycustomview D/demo: dispatchTouchEvent in blue05-03 09:00:17.969 12565-12565/com.cqupt.mycustomview D/demo: onInterceptTouchEvent in blue05-03 09:00:17.969 12565-12565/com.cqupt.mycustomview D/demo: dispatchTouchEvent in green05-03 09:00:17.969 12565-12565/com.cqupt.mycustomview D/demo: onTouchEvent in green05-03 09:00:17.969 12565-12565/com.cqupt.mycustomview D/demo: onTouchEvent in blue05-03 09:00:17.970 12565-12565/com.cqupt.mycustomview D/demo: onTouchEvent in red

  从Log日志分析得到,虽然绿色部分是直接的事件接触者,但是事件不是一下就直接传递到绿色部分进行处理的,而是从最外层逐级传递下来的。对于ViewGroup来说,都是首先调用dispatchTouchEvent,随后调用onIntercepTouchEvent,最后把事件分发给他的子View。然后事件就这样一级一级的传递到最终能够处理该事件的View(判断的依据是手指触摸的范围是否在当前View当中)。当事件传递到绿色部分时,就会调用该View的onTouchEvent,如果在此中返回了false,即表示不消耗本次事件,那么事件又会交给它的父视图去处理。

当手指触摸蓝色部分时,很显然,现在的事件最终接收者不再是绿色部分了,蓝色部分将作为最终事件处理的View,下边来看一看Log,验证一下:

05-03 09:12:32.643 12565-12565/com.cqupt.mycustomview D/demo: dispatchTouchEvent in red05-03 09:12:32.643 12565-12565/com.cqupt.mycustomview D/demo: onInterceptTouchEvent in red05-03 09:12:32.643 12565-12565/com.cqupt.mycustomview D/demo: dispatchTouchEvent in blue05-03 09:12:32.643 12565-12565/com.cqupt.mycustomview D/demo: onInterceptTouchEvent in blue05-03 09:12:32.643 12565-12565/com.cqupt.mycustomview D/demo: onTouchEvent in blue05-03 09:12:32.643 12565-12565/com.cqupt.mycustomview D/demo: onTouchEvent in red

  实际结果和我们预想的一样,在蓝色部分调用onTouchEvent后直接返回给了红色部分去处理,没有再出现绿色部分的调用。

有了整体的认识,我们再从源码的角度分析一下事件分发到底做了什么。首先看ViewGroup中dispatchTouchEvent()的处理流程

// Check for interception.final boolean intercepted;if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;if (!disallowIntercept) {intercepted = onInterceptTouchEvent(ev);ev.setAction(action); // restore action in case it was changed} else {intercepted = false;}} else {// There are no touch targets and this action is not an initial down// so this view group continues to intercept touches.intercepted = true;}

  这段代码逻辑就是去判断是否需要由此ViewGroup拦截当前事件。disallowIntercept默认情况下是false的,也就是说,默认情况下ViewGroup会调用onInterceptTouchEvent(),而此方法默认返回false,即默认情况不会拦截事件。当子View通过getParent().requestDisallowInterceptTouchEvent(true)可以让父View不去拦截自己的事件,此时只是让disallowIntercept为true,也就是说此时父View不会再去调用onIntercepTouchEvent()了。

由代码的注释可知,当mFirstTouchTarget为空时,即没有子View能处理该事件,那么直接让intercept为true,后续的事件直接由该ViewGroup来处理了。

如果事件没有被拦截,则需要遍历所有的children,找到能够处理该事件的child。看下面的代码逻辑(主要代码):

1    if (newTouchTarget == null && childrenCount != 0) {2    final float x = ev.getX(actionIndex);3      final float y = ev.getY(actionIndex);4    // Find a child that can receive the event.5    // Scan children from front to back.67    final View[] children = mChildren;8    for (int i = childrenCount - 1; i >= 0; i--) {9    final int childIndex = getAndVerifyPreorderedIndex(10    childrenCount, i, customOrder);11    final View child = getAndVerifyPreorderedView(12    preorderedList, children, childIndex);1314    if (!canViewReceivePointerEvents(child)15    || !isTransformedTouchPointInView(x, y, child, null)) {16    ev.setTargetAccessibilityFocus(false);17    continue;18    }1920    newTouchTarget = getTouchTarget(child);21    if (newTouchTarget != null) {22    // Child is already receiving touch within its bounds.23    // Give it the new pointer in addition to the ones it is handling.24    newTouchTarget.pointerIdBits |= idBitsToAssign;25    break;26    }2728    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {29    // Child wants to receive touch within its bounds.30    newTouchTarget = addTouchTarget(child, idBitsToAssign);31    alreadyDispatchedToNewTouchTarget = true;32    break;33    }34    }35    }

  首先2-3行获取到本次touch事件的x,y坐标,然后第8行的for循环从前到后的遍历所有的child。14-18行的调用了两个方法做判断,第一个方法canViewReceivePointerEvents()判断该child是否可见且是否在执行动画,isTransformedTouchPointInView()判断此次的x,y坐标是否落在child的面积范围之内。如果坐标并没有在child的面积范围之内或者child不可见或者child正在执行动画,则直接开始遍历下一个child。否则,接着执行后续的代码。

第20行获取child所关联的TouchTarget,如果不为空,则表示child已经在处理事件了,只需要更新touchTarget的坐标即可,随即退出循环,表示已经找到需要处理此事件的view了。

如果newTouchTarget为空,那么进入28行调用dispatchTransformedTouchEvent()方法,把事件的处理交给了child。在该方法中会调用child.dispatchTouchEvent(),如果child也是一个ViewGroup,就会递归调用ViewGroup中的dispatchTouchEvent(),就这样遍历完整个ViewTree。

如果在dispatchTransformedTouchEvent()方法中传递的child为空,就会调用super.dispatchTouchEvent(),即View类的dispatchTouchEvent(),后边会分析该方法的逻辑。
如果dispatchTransformedTouchEvent()方法返回了true,就表示次事件已经找到相应的View进行处理,就用newTouchTarget记录当前处理次事件的View,并且把标志位alreadyDispatchedToNewTouchTarget置为true,最后跳出循环,因为已经找到处理次事件的view了。

至此,ViewGroup就遍历完children,且把能处理当前事件的view赋值给了newTouchTarget。接下来继续分析下面的代码逻辑。

1    if (!canceled && !intercepted) {2    //代码省略3    }4    // Dispatch to touch targets.5    if (mFirstTouchTarget == null) {6    // No touch targets so treat this as an ordinary view.7    handled = dispatchTransformedTouchEvent(ev, canceled, null,8    TouchTarget.ALL_POINTER_IDS);9    } else {10    // Dispatch to touch targets, excluding the new touch target if we already11    // dispatched to it.  Cancel touch targets if necessary.12    TouchTarget predecessor = null;13    TouchTarget target = mFirstTouchTarget;14    while (target != null) {15     final TouchTarget next = target.next;16    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {17    handled = true;18    } else {19    final boolean cancelChild = resetCancelNextUpFlag(target.child)20    || intercepted;21    if (dispatchTransformedTouchEvent(ev, cancelChild,22    target.child, target.pointerIdBits)) {23    handled = true;24    }25    }26    predecessor = target;27    target = next;    }   }
  1-3行是父组件没有拦截事件的处理逻辑,随后第5行的if判断mFirstTouchTarget是否为空,mFirstTouchTarget相当于头指针,指向触摸事件队列的开始(考虑多点触控的情况),如果为空,则表示没有child可以处理该事件或者没有child消耗了此事件,就执行dispatchTransformedTouchEvent()函数,注意此时第三个参数传递是null,进入函数内部可知,当child为空时,就会调用super.dispatchTouchEvent(),即调用View的dispatchTouchEvent(),有自己来处理该事件。如果mFirstTouchTarget不为空,则表示已经找到处理该事件的child,16行alreadyDispatchedToNewTouchTarget为true就置handled为true,表示此次事件被消耗了。alreadyDispatchedToNewTouchTarget就是在1-3行语句块里边,找到能够处理此事件的child的时候设置为true的。

至此ViewGroup的dispatchTouchEvent()方法就大致分析完毕。最后总结一下,该方法主要先判断是否需要拦截此次事件,即调用了onIntercepTouchEvent()方法,如果返回true,那么事件被拦截,导致mFirstTouchTarget为空,那么就会调用自己(View)的dispatchTouchEvent()来进行事件处理;如果返回false,就去遍历自己的所有children,找到能够处理该事件的child(child要可见且触摸事件的坐标落在了child的内部),如果找到就递归的调用child的dispatchTouchEvent(),如果没有找child能够处理此次事件,则还是交由自己来处理。那么最终还是落脚到View的dispatchTouchEvent()方法,接下来分析一下View的dispatchTouchEvent()方法的内部逻辑。

1    public boolean dispatchTouchEvent(MotionEvent event) {2    // If the event should be handled by accessibility focus first.3    if (event.isTargetAccessibilityFocus()) {4    // We don't have focus or no virtual descendant has it, do not handle the event.5    if (!isAccessibilityFocusedViewOrHost()) {6    return false;7    }8    // We have focus and got the event, then use normal event dispatch.9    event.setTargetAccessibilityFocus(false);10    }1112    boolean result = false;1314    if (mInputEventConsistencyVerifier != null) {15    mInputEventConsistencyVerifier.onTouchEvent(event, 0);16    }1718    final int actionMasked = event.getActionMasked();19    if (actionMasked == MotionEvent.ACTION_DOWN) {20    // Defensive cleanup for new gesture21    stopNestedScroll();22    }2324    if (onFilterTouchEventForSecurity(event)) {25    if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {26    result = true;27    }28    //noinspection SimplifiableIfStatement29    ListenerInfo li = mListenerInfo;30    if (li != null && li.mOnTouchListener != null31    && (mViewFlags & ENABLED_MASK) == ENABLED32    && li.mOnTouchListener.onTouch(this, event)) {33    result = true;34    }3536    if (!result && onTouchEvent(event)) {37    result = true;38    }39    }4041    if (!result && mInputEventConsistencyVerifier != null) {42    mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);43    }4445    // Clean up after nested scrolls if this is the end of a gesture;46    // also cancel it if we tried an ACTION_DOWN but we didn't want the rest47    // of the gesture.48    if (actionMasked == MotionEvent.ACTION_UP ||49    actionMasked == MotionEvent.ACTION_CANCEL ||50    (actionMasked == MotionEvent.ACTION_DOWN && !result)) {51    stopNestedScroll();52    }53    return result;54    }
  View的dispatchTouchEvent()方法就相对要简单一些,12行定义的result表示事件的消耗情况,默认为false(不消耗)。19-22行判断如果事件为ACTION_DOWN,则需要停止当前View的滚动。重点看一下30-34行li.mOnTouchListener就是通过View.setOnTouchListener(),设置的监听器,li.mOnTouchListener.onTouch(this, event)会调用我们设置的监听器的onTouch()方法,如果监听器不为空且onTouch方法也返回了true,那么此次事件就被消耗点了result = true。

再看36-38的关键代码,如果事件没有被消耗掉,即30行if里有一个条件没有满足,那么就会调用onTouchEvent(event)了去处理事件了,很明显,此时事件的最终消耗情况由ontouchEvent()来决定了。那么下边再来看看onTouchEvent()方法的代码逻辑。

1    final float x = event.getX();2    final float y = event.getY();3    final int viewFlags = mViewFlags;4    final int action = event.getAction();56    if ((viewFlags & ENABLED_MASK) == DISABLED) {7    if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {8    setPressed(false);9    }10    // A disabled view that is clickable still consumes the touch11    // events, it just doesn't respond to them.12     return (((viewFlags & CLICKABLE) == CLICKABLE13    || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)14    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);15    }

  1-2行获得当前触摸事件的x,y坐标。6行判断如果当前View是Disabled,那么进入到12-14行,当View是CLICKABLE或者LONG_CLICKABLE或者CONTEXT_CLICKABLE就直接返回true,否则返回false。也就是说即便View是disabled的,只要当前View可以点击或者可以长按那么默认都会消耗事件。

1   if (((viewFlags & CLICKABLE) == CLICKABLE ||2(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||3(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {4  switch (action) {5case MotionEvent.ACTION_UP:6if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {78if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {9// This is a tap, so remove the longpress check10removeLongPressCallback();1112// Only perform take click actions if we were in the pressed state13if (!focusTaken) {14// Use a Runnable and post this rather than calling15// performClick directly. This lets other visual state16// of the view update before click actions start.17if (mPerformClick == null) {18mPerformClick = new PerformClick();19}20if (!post(mPerformClick)) {21performClick();22}23}24}25}26break;2728case MotionEvent.ACTION_DOWN:29mHasPerformedLongPress = false;30if (isInScrollingContainer) {31mPrivateFlags |= PFLAG_PREPRESSED;32if (mPendingCheckForTap == null) {33mPendingCheckForTap = new CheckForTap();34}35mPendingCheckForTap.x = event.getX();36mPendingCheckForTap.y = event.getY();37postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());38} else {39// Not inside a scrolling container, so show the feedback right away40setPressed(true, x, y);41checkForLongClick(0, x, y);42}43break;4445case MotionEvent.ACTION_CANCEL:46break;4748case MotionEvent.ACTION_MOVE:49break;50}5152return true;53}

  同样只要View为可点击或者可长按,那么都会进行4-53的逻辑判断,这里边的语句,整体来看,就是通过switch-case语句来分别处理ACTION_DOWN,ACTION_MOVE,ACTION_CANCEL,ACTION_UP事件。但是最终52行都会返回true,即只要view的enabled属性为true,且可点击或者长按,那么默认一定会消耗掉此次事件。

先来看ACTION_DOWN事件的处理,关键一行就是41,调用了一个函数checkForLongClick(0, x, y),这个方法就是为了回调我们通常设置的OnLongClickListener。先来看看checkForLongClick(0, x, y)这个方法的内部实现:

1    private void checkForLongClick(int delayOffset, float x, float y) {2        if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {3            mHasPerformedLongPress = false;45            if (mPendingCheckForLongPress == null) {6                mPendingCheckForLongPress = new CheckForLongPress();7            }8            mPendingCheckForLongPress.setAnchor(x, y);9            mPendingCheckForLongPress.rememberWindowAttachCount();10           postDelayed(mPendingCheckForLongPress,11                    ViewConfiguration.getLongPressTimeout() - delayOffset);12        }13    }
  第3行首先把mHasPerformedLongPress标志位设为false,表示长按操作还未进行。5-7行为在为mPendingCheckForLongPress赋初值,mPendingCheckForLongPress其实就是一个实现Runnable接口的对象,在它的run方法中会最终会回到我们的onLongClick方法去执行用户自定义的逻辑代码,如果onLongClick方法返回true,会把mHasPerformedLongPress设为true(这关系我们的onClick方法是否能回调)。

10-11行通过View的postDelayed方法,内部实现当然还是通过handler发起了一个延时(ViewConfiguration.getLongPressTimeout() - delayOffset,系统默认发送长按事件的触发条件)的消息(封装了我们的mPendingCheckForLongPress长按事件)。。//后续在另写一篇文章结束handler机制

这样当手指触摸屏幕开始,就通过handler发起了长按事件的message,在一定的时延之后就会回调onLongClick()方法了。但是如果用户很快就把手指离开了屏幕,我们的长按事件是不会触发的,那么这又是怎么实现的呢?继续分析代码,来看看ACTION_UP中的处理:

回到上段代码的5-26行,看关键代码第8行!mHasPerformedLongPress,标志位表示长按事件还未发生,什么意思?还记得在事件的开始即ACTION_DOWN事件触发的时候,就把mHasPerformedLongPress设为了false。那么mHasPerformedLongPress到底什么时候被舍为true的呢?那就是我们的mPendingCheckForLongPress在一定时延后,以message的信息,调用了run()方法:(也就是关键的10-12行)

1    private final class CheckForLongPress implements Runnable {2        private int mOriginalWindowAttachCount;3        private float mX;4        private float mY;56        @Override7        public void run() {8            if (isPressed() && (mParent != null)9                    && mOriginalWindowAttachCount == mWindowAttachCount) {10                if (performLongClick(mX, mY)) {11                    mHasPerformedLongPress = true;12                }13            }14        }1516        public void setAnchor(float x, float y) {17            mX = x;18            mY = y;19        }2021        public void rememberWindowAttachCount() {22            mOriginalWindowAttachCount = mWindowAttachCount;23        }24    }
  很明显,如果在ACTION_UP事件触发后,我们的runnable对象还未被执行的话,就需要取消此次的长按事件了,也就是通过removeLongPressCallback()来实现的,该方法最终会通过handler内部的messageQueue来remove掉最开始ACTION_DOWN事件触发时所发出的长按事件。

至此我们就大致了解了系统是怎么触发onLongClick方法的了。接下来分析一下onClick()方法是怎么在onTouchEvent()方法中回调的。onClick的回调时刻是在ACTION_UP事件里,onTouchEvent()方法的20-22行,

mPerformClick也是一个实现runnable的对象,在它的run方法中最终会回调我们的onclick方法,来实现我们用户自己的逻辑(这也是我们最常用的在view上setOnClickListener())。同样,也是通过我们的handler来发起一个消息。如果大家对handler的机制不太了解,可以去网上看一看,一搜一大片。

至此我们把onTouchEvent()的大致逻辑也分析了一下,主要分析系统是怎么为我们回调onLongClick和onClick方法的,关键的点就是通过了handler机制。

由于个人能力有限,Android事件传递和消费机制还有很多内容没有分析到,当真还有很多逻辑也没有能完全弄懂,尽最多的努力把基本的逻辑清理了一遍,完成了这篇博客的撰写,在记录学习的过程同时希望能够读到这篇文章的读者解决哪怕那么一丢丢的疑惑,我也就心满意足了。。。

0 0