[关闭]
@universal 2017-11-06T14:03:33.000000Z 字数 7935 阅读 286

View的事件分发机制

android 源码分析


view的事件分发主要是指点击事件,也就是系统对MotionEvent的传递

准备&&拓展

  1. 首先,拓展了解下Android界面的架构图



    UI界面架构

    Activity -->PhoneWindow -->DectorView -->TitleView/ContentView

    • PhoneWindow:每个activity都包含一个window对象,具体由PhoneWindow实现
    • DectorView:作为界面的根View,继承FrameLayout,是整个窗口界面的顶层视图。
    • TitleView:继承FrameLayout,actionbar就是设置在这里
    • ContentView:ID为content的FrameLayout,activity_main.xml就是设置在这里面



主界面详细结构


View的事件分发(or传递)

1. 点击事件的产生

正常情况下,一次手指触摸屏幕的行为会产生一系列点击事件。事件序列即:
DOWN -> UP 或者 DOWN -> MOVE-> ... ->MOVE -> UP ;
一个事件序列由DOWN开始,中间含有n个MOVE事件,最终以UP结束。

2. 事件的传递规则


先上张图,下面都会围绕这张图展开


传递规则

注:在处理事件时,如果一个View设置了OnTouchListener,那么listener中的OnTouch方法的返回值将影响事件的下一步传递,如果为true,则OnTouchEvent不会被调用,如果为false,则调用当前View的OnTouchEvent。在OnTouchEvent中,如果设置了OnClickListener,则OnClick方法会被调用。

3. 源码分析

从故事开始的地方说起,

  1. 当一个MotionEvent产生,事件最先传给activity,由activity进行分发,

    1. public boolean dispatchTouchEvent(MotionEvent ev) {
    2. if (ev.getAction() == MotionEvent.ACTION_DOWN) {
    3. onUserInteraction();
    4. }
    5. if (getWindow().superDispatchTouchEvent(ev)) {
    6. return true;
    7. }
    8. return onTouchEvent(ev);
    9. }

    activity会将事件交给window的dispatchTouchEvent去处理,如果下级View们都不处理,则调用自身的onTouchEvent处理
    注:可以在activity中重写此方法,以便在事件发送到窗口之前拦截所有触摸屏事件

  2. 事件传给Window

    1. //Window
    2. public abstract boolean superDispatchTouchEvent(MotionEvent event);
    3. //PhoneWindow(实现类)
    4. @Override
    5. public boolean superDispatchTouchEvent(MotionEvent event) {
    6. return mDecor.superDispatchTouchEvent(event);
    7. }

    抽象类window会将事件交由它的实现类 PhoneWindow去处理
    PhoneWindow直接将事件传递给了DecorView

  3. 事件再到DecorView

    1. public boolean superDispatchTouchEvent(MotionEvent event) {
    2. return super.dispatchTouchEvent(event);
    3. }

    在上面曾提到,DecorView是继承自FrameLayout,所以直接调用父类的dispatchTouchEvent去处理事件,就这样将事件从activity传到了根View

  4. 事件进入正轨,从根ViewGroup开始
    (一)

    1. @Override
    2. public boolean dispatchTouchEvent(MotionEvent ev) {
    3. ...
    4. ...
    5. // Handle an initial down.
    6. if (actionMasked == MotionEvent.ACTION_DOWN) {
    7. cancelAndClearTouchTargets(ev);
    8. resetTouchState();
    9. }
    10. //mFirstTouchTarget:First touch target in the linked list of touch targets.
    11. // Check for interception.
    12. final boolean intercepted;
    13. if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {
    14. final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    15. if (!disallowIntercept) {
    16. intercepted = onInterceptTouchEvent(ev);
    17. ev.setAction(action);
    18. } else {
    19. intercepted = false;
    20. }
    21. } else {
    22. intercepted = true;
    23. }
    24. }

    先看其中的两个操作check for interception 、 handle an initial down;
      (1)check for interception:
      从字面意思来看:判断是否拦截事件。在当前事件为ACTION_DOWN时或者mFirstTouchTarget != null时,ViewGroup会判断是否拦截事件。这里的mFirstTouchTarget != null是指有子view要处理此事件。所以一旦ViewGroup决定拦截事件,那么mFirstTouchTarget就为null,而且当事件序列中后续的move和up事件到来时,判断条件不满足,导致ViewGroup中的OnInterceptTouchEvent不会被重复调用。
      满足条件后,ViewGroup会判断标记位FLAG_DISALLOW_INTERCEPT的值(一般在子View中设置),如果禁止拦截,Viewgroup将无法拦截除了ACTION_DOWN之外的事件(因为在处理ACTION_DOWN事件的时候,Viewgroup会在下面的"initial down"中resetTouchState重置标志位);反之则调用onInterceptTouchEvent去判断是否拦截事件。
      (2)handle an initial down:
      在ACTION_DOWN事件到来的时候,清除上一个触摸手势所保存的状态,包括FLAG_DISALLOW_INTERCEPT标志位的重置。所以在面对ACTION_DOWN事件时,ViewGroup总会调用OnInterceptTouchEvent判断是否拦截事件。
    (二)

    1. @Override
    2. public boolean dispatchTouchEvent(MotionEvent ev) {
    3. ....
    4. if (!canceled && !intercepted) {
    5. // If the event is targeting accessiiblity focus we give it to the
    6. // view that has accessibility focus and if it does not handle it
    7. ....
    8. final View[] children = mChildren;
    9. for (int i = childrenCount - 1; i >= 0; i--) {
    10. ....
    11. if (!canViewReceivePointerEvents(child)||
    12. !isTransformedTouchPointInView(x, y, child, null))
    13. {
    14. ev.setTargetAccessibilityFocus(false);
    15. continue;
    16. }
    17. ....
    18. if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)){
    19. ....
    20. newTouchTarget = addTouchTarget(child, idBitsToAssign);
    21. alreadyDispatchedToNewTouchTarget = true;
    22. break;
    23. }
    24. }
    25. }
    26. // Dispatch to touch targets.
    27. if (mFirstTouchTarget == null) {
    28. // No touch targets so treat this as an ordinary view.
    29. handled = dispatchTransformedTouchEvent(ev,canceled,null,TouchTarget.ALL_POINTER_IDS;
    30. }
    31. ...
    32. }
    33. private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
    34. View child, int desiredPointerIdBits) {
    35. .....
    36. if (child == null) {
    37. handled = super.dispatchTouchEvent(event);
    38. } else {
    39. handled = child.dispatchTouchEvent(event);
    40. }
    41. return handled;
    42. .....
    43. }

      如果ViewGroup不拦截事件,则交由分发给子项。具体实现:
      先遍历ViewGroup所有子元素,判断子元素是否能收到点击事件,如果能,则事件会传递给它来处理。可以看到dispatchTransformedTouchEvent其实就是调用子view的dispatchTouchEvent。如果子View选择处理事件,则dispatchTouchEvent就会返回true,同时mFirstTouchTarget就会被赋值并结束循环。如果当前子元素不处理,则进入下一个子元素的循环。
      如果ViewGroup没有子元素,或者子元素的dispatchTouchEvent返回了false,导致mFirstTouchTarget未被赋值,这时ViewGroup就会自己处理事件。

  5. 事件交给了View
    (1)View中的dispatchTouchEvent

    1. public boolean dispatchTouchEvent(MotionEvent event) {
    2. ....
    3. if (onFilterTouchEventForSecurity(event)) {
    4. //noinspection SimplifiableIfStatement
    5. ListenerInfo li = mListenerInfo;
    6. if (li != null && li.mOnTouchListener != null&& (mViewFlags & ENABLED_MASK) ==ENABLED
    7. && li.mOnTouchListener.onTouch(this, event)) {
    8. result = true;
    9. }
    10. if (!result && onTouchEvent(event)) {
    11. result = true;
    12. }
    13. }
    14. ....
    15. return result;
    16. }

      从代码中可以看出,View会先判断有没有设置OnTouchListener,如果有且onTouch方法返回true,则不再调用下面的onTouchEvent,这样就方便在外部处理MotionEvent。
    (2)View中的onTouchEvent

    1. public boolean onTouchEvent(MotionEvent event) {
    2. if ((viewFlags & ENABLED_MASK) == DISABLED) {
    3. if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
    4. setPressed(false);
    5. }
    6. // A disabled view that is clickable still consumes the touch
    7. // events, it just doesn't respond to them.
    8. return (((viewFlags & CLICKABLE) == CLICKABLE
    9. || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
    10. || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
    11. }
    12. }

      这个地方就应证了上面所说的,view的enable属性不影响view消耗事件,只和CLICKABLE、LONG_CLICKABLE、CONTEXT_CLICKABLE属性有关。

    1. public boolean onTouchEvent(MotionEvent event) {
    2. if (((viewFlags & CLICKABLE) == CLICKABLE ||
    3. (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
    4. (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
    5. switch (action) {
    6. case MotionEvent.ACTION_UP:
    7. ....
    8. if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
    9. // This is a tap, so remove the longpress check
    10. removeLongPressCallback();
    11. if (!focusTaken) {
    12. // Use a Runnable and post this rather than calling
    13. // performClick directly. This lets other visual state
    14. // of the view update before click actions start.
    15. if (mPerformClick == null) {
    16. mPerformClick = new PerformClick();
    17. }
    18. if (!post(mPerformClick)) {
    19. performClick();
    20. }
    21. }
    22. }
    23. break;
    24. case MotionEvent.ACTION_DOWN:
    25. // For views inside a scrolling container, delay the pressed feedback for
    26. // a short period in case this is a scroll.
    27. if (isInScrollingContainer) {
    28. ....
    29. } else {
    30. // Not inside a scrolling container, so show the feedback right away
    31. setPressed(true, x, y);
    32. checkForLongClick(0, x, y);
    33. }
    34. break;
    35. case MotionEvent.ACTION_CANCEL:
    36. ....
    37. break;
    38. case MotionEvent.ACTION_MOVE:
    39. ....
    40. break;
    41. }
    42. return true;
    43. }
    44. }
    45. private final class CheckForLongPress implements Runnable {
    46. @Override
    47. public void run() {
    48. if (isPressed() && (mParent != null)
    49. && mOriginalWindowAttachCount == mWindowAttachCount) {
    50. if (performLongClick(mX, mY)) {
    51. mHasPerformedLongPress = true;
    52. }
    53. }
    54. }
    55. }

      可以看到,当ACTION_UP事件发生时,如果不是longPress会调用performClick,在这个方法里,如果view设置了OnClickListener,则会调用它的OnClick方法,而且performClick是通过Runnable发布调用的,方便在处理事件之前更新View状态。
      当ACTION_DOWN事件发生时,View会检查是否是LongClick事件,如果长按事件设置了回调,则执行回调方法。而且如果回调返回true的话,mHasPerformedLongPress就会设置为true,所以在长按时,当上面UP事件发生,performClick就无法调用,即短按事件就会被屏蔽。

总结

  View事件分发的流程大概就是这样,后面还会继续研究滑动冲突的问题。最后,说说源码分析的那点事儿,过程很蓝瘦,但是得学会取舍,一时看不懂先略过,注重大体逻辑,跟着翻译走,没准在后面就能明白前面变量或者方法的意义,水到渠成。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注