【Android】事件分发详解

1. 简介

在Android开发中,事件分发是比较重要的基础知识,了解并熟悉整套机制有助于更好的分析各种点击滑动失效问题,也更有利于去扩展控件的事件功能开发自定义控件

事件分发中事件指的是一次完整的点击中所包含的事件(如 手指按下屏幕、手指在屏幕中移动、手指抬起等)分发指的是事件从Activity到Window再到ViewGroup最后到View的捕获阶段以及逆方向消费事件的冒泡阶段

首先我给出一张事件分发的流程图,看过之后能对事件分发有一个大概的了解。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-etv8E6SS-1672149055402)(/Users/hxf/Documents/事件分发_1.png)]

在此,先对整个流程图进行大体上的描述:

  1. 点击事件首先被Activity捕获,接着一路向下分发,直到被ViewGroup或者View的onTouchEvent消费。
  2. 事件向下传递的过程中会经过0或者多个ViewGroup。
  3. 当onTouchEvent返回true时,代表事件已经被处理了,流程结束了。
  4. 当onTouchEvent返回false时,事件就会向上冒泡给父视图处理。
  5. 本质上View没有参与事件分发,只是参与了事件处理。
  6. 上图是针对ACTION_DOWN事件的,ACTION_MOVE以及ACTION_UP事件会在后面分析

2. 源码分析

上图只是对事件分发的流程进行了简单的描述,想要分析各种点击滑动失效问题或者开发自定义控件,我们必须对深入了解事件分发的细节。

2.1 Activity

我们上文讲到点击事件首先被Activity捕获,我们先看Activity对点击事件的处理。

/**
 * Activity
 * 可以通过重写此方法截获所有Touch事件
 */
public boolean dispatchTouchEvent(MotionEvent ev) {
  if (ev.getAction() == MotionEvent.ACTION_DOWN) {
    // 默认为空实现,用于管理状态栏通知,非重点。
    onUserInteraction();
  }
  if (getWindow().superDispatchTouchEvent(ev) /* 分析1 */) {
    return true;
  }
  return onTouchEvent(ev)/* 分析2 */;
}

/**
 * 分析1
 * Activity
 * 通过此方法获得Window对象,由于Window是一个抽象类,仅有一个PhoneWindow实现,
 * 因此此处是拿到了一个PhoneWindow对象,后续就会走到PhoneWindow的superDispatchTouchEvent方法
 */
public Window getWindow() {
  return mWindow;
}

/**
 * 分析2
 * Activity
 * 当没有视图处理触摸屏事件时调用,若判断成立则关闭Activity
 */
public boolean onTouchEvent(MotionEvent event) {
  if (mWindow.shouldCloseOnTouch(this, event) /* 分析3 */) {
    finish();
    return true;
  }

  return false;
}

/**
 * 分析3
 * Window
 * 通过源码可以看到,如果手指抬起时在边界之外 或者 事件本身就是出界事件则认为应当关闭Activity
 */
public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
  final boolean isOutside =
    event.getAction() == MotionEvent.ACTION_UP && isOutOfBounds(context, event)
    || event.getAction() == MotionEvent.ACTION_OUTSIDE;
  if (mCloseOnTouchOutside && peekDecorView() != null && isOutside) {
    return true;
  }
  return false;
}

2.2 PhoneWindow

/**
 * Window
 * PhoneWindow仅仅是调用了DecorView的superDispatchTouchEvent方法
 */
public boolean superDispatchTouchEvent(MotionEvent event) {
  return mDecor.superDispatchTouchEvent(event);
}

2.3 DecorView

/**
 * DecorView
 * DecorView又调用了父类的dispatchTouchEvent方法
 * 由于DecorView继承自FrameLayout属于ViewGroup,因此此处调用了ViewGroup的dispatchTouchEvent
 */
public boolean superDispatchTouchEvent(MotionEvent event) {
  return super.dispatchTouchEvent(event);
}

2.4 ViewGroup

由于ViewGroup以及View中方法的源码一般非常长,还涉及到一些在此处不太重要的细节,比如多指手势或者鼠标点击事件等,因此我们在下文讨论的源码都是经过简化的代码,用…代表省略的代码。

public boolean dispatchTouchEvent(MotionEvent ev) {
  	...
    // 处理初始Down事件
    if (actionMasked == MotionEvent.ACTION_DOWN) {
      // 当开始一个新的触摸手势时,重置状态。
      cancelAndClearTouchTargets(ev);
      //分析1
      resetTouchState();
    }
  
  	// 拦截
    final boolean intercepted;
    if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
      // 由分析1可以知道在没有别的设置项影响的情况下默认是允许拦截事件的 分析2
      final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
      if (!disallowIntercept) {
        // 此处为判断是否拦截事件关键函数 分析3
        intercepted = onInterceptTouchEvent(ev);
        ev.setAction(action);
      } else {
        intercepted = false;
      }
    } else {
      // 没有touch事件的目标并且这个动作不是一个初始的down事件,继续拦截。
      intercepted = true;
    }
  
    ...
      
    // 递归获取可获得焦点的子视图
    View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
    				? findChildWithAccessibilityFocus() : null;
    
    ...
      
    final View[] children = mChildren;
    for (int i = childrenCount - 1; i >= 0; i--) {
      // 获取子视图(可以自定义获取顺序)
      final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
      final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);

      // 如果有一个视图具有可访问焦点,我们希望它先获取事件,如果未处理,将执行正常的调度。
      if (childWithAccessibilityFocus != null) {
        if (childWithAccessibilityFocus != child) {
          continue;
        }
        // 此处令 i = childrenCount - 1 可以进行双重循环
        // 第一重循环为 优先处理具有可访问焦点的视图
        // 第二重循环为 正常调度所有子视图
        childWithAccessibilityFocus = null;
        i = childrenCount - 1;
      }

      // 如果视图不能获得点击事件(可见且无动画)或者点击位置不在视图内 舍弃此View
      if (!child.canReceivePointerEvents() /* 分析4 */
          || !isTransformedTouchPointInView(x, y, child, null)) {
        ev.setTargetAccessibilityFocus(false);
        continue;
      }

      // 此时 mFirstTouchTarget 仍为null,getTouchTarget 的结果仍为null ,下面的分支不会走入
      newTouchTarget = getTouchTarget(child);
      if (newTouchTarget != null) {
        newTouchTarget.pointerIdBits |= idBitsToAssign;
        break;
      }

      // 重置标志
      resetCancelNextUpFlag(child);
      if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)/* 分析5 */) {
        // Child wants to receive touch within its bounds.
        mLastTouchDownTime = ev.getDownTime();
        if (preorderedList != null) {
          // 通过预排序列表查找原始索引
          for (int j = 0; j < childrenCount; j++) {
            if (children[childIndex] == mChildren[j]) {
              mLastTouchDownIndex = j;
              break;
            }
          }
        } else {
          mLastTouchDownIndex = childIndex;
        }
        mLastTouchDownX = ev.getX();
        mLastTouchDownY = ev.getY();
        // 通过addTouchTarget设置后mFirstTouchTarget不再为null 分析6
        newTouchTarget = addTouchTarget(child, idBitsToAssign);
        alreadyDispatchedToNewTouchTarget = true;
        break;
      }
    }
  
    ...
}

/**
 * 分析1
 * ViewGroup
 */
private void resetTouchState() {
  clearTouchTargets();
  resetCancelNextUpFlag(this);
  // 此处位运算导致 mGroupFlags & FLAG_DISALLOW_INTERCEPT = 0
  mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
  mNestedScrollAxes = SCROLL_AXIS_NONE;
}

/**
 * 分析2
 * ViewGroup
 * 见名知意,请求不允许拦截触摸事件,true则不允许拦截,false则可以拦截。
 */
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
  if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
    // We're already in this state, assume our ancestors are too
    return;
  }

  if (disallowIntercept) {
    mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
  } else {
    mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
  }

  // Pass it up to our parent
  if (mParent != null) {
    mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
  }
}

/**
 * 分析3
 * ViewGroup
 */
public boolean onInterceptTouchEvent(MotionEvent ev) {
  ...
  // 默认为不拦截事件,可以在自定义组件时重写。
  return false;
}

/**
 * 分析4
 * ViewGroup
 */
protected boolean canReceivePointerEvents() {
  // 可见或者存在动画
  return (mViewFlags & VISIBILITY_MASK) == VISIBLE || getAnimation() != null;
}

/**
 * 分析5
 * ViewGroup
 * 此方法本质上就是调用了子视图的dispatchTouchEvent方法
 * 若子视图为空,则调用父类(View)的dispatchTouchEvent方法
 * @Return 事件是否被消费
 */
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
                                              View child, int desiredPointerIdBits) {
  ...
  
  if (child == null) {
    handled = super.dispatchTouchEvent(event);
  } else {
    ...
    handled = child.dispatchTouchEvent(event);
    ...
  }
  return handled;
  
  ...
}

/**
 * 分析6
 * ViewGroup
 * 使用头插法将touch目标放在链表的开头
 */
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
  final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
  target.next = mFirstTouchTarget;
  mFirstTouchTarget = target;
  return target;
}

ViewGroup的dispatchTouchEvent过程比较复杂,但可以被划分为一下几个步骤:

  1. 接收到Down事件后,重置标志
  2. 判断是否可以拦截事件,如果可以尝试拦截 (若无重写拦截函数,最终结果仍是未拦截)
  3. 双重循环查找可处理事件的子视图,并调用其dispatchTouchEvent分配事件

2.5 View

View是以事件处理者的身份存在的,因此它没有ViewGroup拥有的事件分发以及拦截的能力,View更关心事件的消费。由于View没有拦截事件的能力,自然也就没有onInterceptTouchEvent方法。

public boolean dispatchTouchEvent(MotionEvent event) {
    ...
    // 优先出发onTouch回调
    ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnTouchListener != null
        && (mViewFlags & ENABLED_MASK) == ENABLED
        && li.mOnTouchListener.onTouch(this, event)) {
      result = true;
    }

    // onTouch没有消费事件,调用onTouchEvent
    if (!result && onTouchEvent(event)/* 分析1 */) {
      result = true;
    }
    ...
    return result;
}

/**
 * 分析1
 * View
 */
public boolean onTouchEvent(MotionEvent event) {
  final float x = event.getX();
  final float y = event.getY();
  final int viewFlags = mViewFlags;
  final int action = event.getAction();

  final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                             || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

  if ((viewFlags & ENABLED_MASK) == DISABLED
      && (mPrivateFlags4 & PFLAG4_ALLOW_CLICK_WHEN_DISABLED) == 0) {
    if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
      // 设置视图的按下状态,由于此处视图被禁用,因此设置为非按下状态
      setPressed(false);
    }
    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
    // 可以点击但被禁用的视图仍然会消费事件,但不会响应事件。
    return clickable;
  }
  // 委托处理触点落在此视图中但应由另一个视图处理的触摸事件。
  if (mTouchDelegate != null) {
    if (mTouchDelegate.onTouchEvent(event)) {
      return true;
    }
  }

  // 可以点击 或者 长按、悬停有提示 时
  if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
    // 对不同事件类型进行区别处理
    switch (action) {
      case MotionEvent.ACTION_UP:
        ...
        if (!clickable) {
          ...
          // 如果长按的回调仍未触发,则移除
          removeLongPressCallback();
          ...
          break;
        }
        ...
        if (prepressed) {
          // 在我们实际显示按钮为按下之前,该按钮正在被释放。
          // 为了确保用户看到它,使其变成按下的状态。
          setPressed(true, x, y);
        }

        ...
        // 使用一个 Runnable 而不是直接调用 performClick。
        // 这使得视图的其他视觉状态在单击操作开始之前可以继续更新。
        if (mPerformClick == null) {
          // PerformClick通过实现了Runnabl接口,使得可以直接post给主线程Handler处理
          // 可以运行时,会通过UI线程来执行点击事件 分析1
          mPerformClick = new PerformClick();
        }
        if (!post(mPerformClick)) {
          // 若没有将 mPerformClick 添加到消息队列中则立即执行
          // 执行点击事件的地方 分析2
          performClickInternal();
        }
        ...
        break;

      case MotionEvent.ACTION_DOWN:
        ...
        mHasPerformedLongPress = false;

        // 如果不能点击,尝试触发长按
        if (!clickable) {
          checkForLongClick(
            ViewConfiguration.getLongPressTimeout(),
            x,
            y,
            TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
          break;
        }
        ...
        // 设置成按下状态,尝试触发长按
        setPressed(true, x, y);
        checkForLongClick(
          ViewConfiguration.getLongPressTimeout(),
          x,
          y,
          TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
        }
        break;

      case MotionEvent.ACTION_CANCEL:
        // 如果是可点击的,可能之前已经进入按下状态,因此需要取消状态
        if (clickable) {
          setPressed(false);
        }
        ...
        // 取消长按回调
        removeLongPressCallback();
        break;

      case MotionEvent.ACTION_MOVE:
				...
        // 获取手势分类
        //     CLASSIFICATION_NONE : 没有明显意图
        //     CLASSIFICATION_AMBIGUOUS_GESTURE :用户对当前事件流的意图尚未确定。
        //             在分类解析为另一个值或事件流结束之前,应禁止手势操作(如滚动)。 
        //     CLASSIFICATION_DEEP_PRESS :用户有意用力按屏幕。应使用此分类类型来加速长按行为
        final int motionClassification = event.getClassification();
        final boolean ambiguousGesture =
          motionClassification == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE;
				...
        if (ambiguousGesture && hasPendingLongPressCallback()) {
          if (!pointInView(x, y, touchSlop)/* 事件移出了视图 */) {
            // 此处的默认操作是取消长按。但在这里仅仅是延长超时,以防分类不明确。
            removeLongPressCallback();
            long delay = (long) (ViewConfiguration.getLongPressTimeout()
                                 * mAmbiguousGestureMultiplier);
            // 减去已经花费的时间
            delay -= event.getEventTime() - event.getDownTime();
            checkForLongClick(
              delay,
              x,
              y,
              TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
          }
          ...
        }

        // 事件移出了视图
        if (!pointInView(x, y, touchSlop)) {
          ...
          // 移除长按回调
          removeLongPressCallback();
          if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
            setPressed(false);
          }
          ...
        }

        final boolean deepPress =
          motionClassification == MotionEvent.CLASSIFICATION_DEEP_PRESS;
        if (deepPress && hasPendingLongPressCallback()) {
          // 由于意图是CLASSIFICATION_DEEP_PRESS,立即执行长按相关的行为
          removeLongPressCallback();
          checkForLongClick(
            0 /* 立即执行 */,
            x,
            y,
            TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DEEP_PRESS);
        }
        break;
    }

    return true;
  }

  return false;
}

/**
 * 分析1
 * View
 */
private final class PerformClick implements Runnable {
  @Override
  public void run() {
    ...
    // 执行点击事件的地方 分析2
    performClickInternal();
  }
}

/**
 * 分析2
 * View
 */
private boolean performClickInternal() {
  ...
  // 真正执行点击事件的地方 分析3
  return performClick();
}

/**
 * 分析3
 * View
 */
public boolean performClick() {
  ...
  final boolean result;
  final ListenerInfo li = mListenerInfo;
  if (li != null && li.mOnClickListener != null) {
    // 播放点击音效
    playSoundEffect(SoundEffectConstants.CLICK);
    // 调用 onClick 事件
    li.mOnClickListener.onClick(this);
    result = true;
  } else {
    result = false;
  }
  ...
  return result;
}

View的onTouchEvent由于要处理不同类型的事件因此显得复杂一些,我们对View的事件消费做一个简单的总结:

  1. dispatchTouchEvent 会优先触发 onTouch 回调,若没有设置onTouch监听则会触发onTouchEvent。
  2. ACTION_UP 会触发 onClick 回调。
  3. ACTION_DOWN 会尝试触发长按,在ACTION_MOVE移出视图或者其他事件中会取消这个回调。如果到时间还没取消则会执行长按的回调。
  4. 如果view的onTouchEvent返回false即事件没有被消费,则dispatchTouchEvent也会返回false。这将会导致父视图再次触发dispatchTransformedTouchEvent,但此时child是null,将执行super.dispatchTouchEvent,即将ViewGroup自身当成一个View来消费事件。

相关文章

  1. 【Android】自定义View / ViewGroup
  2. 【Android】Handler机制详解
  3. 【Android】动画简介
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小黄才不管那么多

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值