Android事件处理机制:事件分发、传递、拦截、处理机制的原理分析(中)

àì夳堔傛蜴生んèń 2023-06-24 06:26 101阅读 0赞

文章目录

  • 前言
  • onTouch、onClick、onLongClick的调用顺序
  • onTouch、onClick、onLongClick的源码藏身之处
  • 后续

前言


  在上一章节中,我们谈到了Android中的事件处理机制,展示了事件传递过程的基本性质。

onTouch、onClick、onLongClick的调用顺序


  在本章节中,我们重点谈论一下onTouch、onClick、onLongClick三个方法被回调的过程。

  在上一篇文章中,我们谈到关于为View添加一个点击事件SetOnClickListener后,就可以通过回调onClick方法来实现事件的响应。而另外还有一个setOnTouchListener方法,通过设置监听后可以在触摸的时候回调onTouch方法。而我们又说到onTouchEvent方法是处理事件的。那么这三个方法究竟有什么区别呢?

  我们在上篇文章的源码中,在Actvity里来分别设置OnClick和onTouch的监听,通过log来打印出他们的调用:

  1. MyView view = (MyView) findViewById(R.id.Button);
  2. view.setOnClickListener(new View.OnClickListener() {
  3. @Override
  4. public void onClick(View v) {
  5. Log.i("lc_miao","MyView : onClick");
  6. }
  7. });
  8. view.setOnTouchListener(new View.OnTouchListener() {
  9. @Override
  10. public boolean onTouch(View v, MotionEvent event) {
  11. Log.i("lc_miao","MyView : onTouch");
  12. return false;
  13. }
  14. });
  15. 1
  16. 2
  17. 3
  18. 4
  19. 5
  20. 6
  21. 7
  22. 8
  23. 9
  24. 10
  25. 11
  26. 12
  27. 13
  28. 14

  我们来看一下关于onClick、onTouch、onTouchEvent三者的调用顺序,运行后点击按钮,log如下:

log

  从log上看,当事件被View接收后,在ACTION_DOWN的时候View会分别触发onTouch和onTouchEvent方法,而在ACTION_UP的时候会分别触发onTouch和onTouchEvent、onClick方法。

  这说明了,在日常中我们给View设置点击事件其实响应优先级是最低的,因为他需要同时接收到ACTION_DOWN和ACTION_UP事件后才会触发,而onTouch方法则是在设置监听后,只要有事件到来,则会触发一次,它比onTouchEvent优先被响应。

  事实上:
  1、onTouch比onClick方法多了一个返回值,其返回值也表示了是否消耗事件,如果返回了true则不会再调用onTouchEvent方法
  2、onClick方法是在onTouchEvent里面被回调的,如果onTouch返回了true,onTouchEvent不会被调用,那么onClick也就不会被调用。
我们来查看一下View的源码。从View接收到事件开始,也就是dispatchTouchEvent方法的源码。

onTouch、onClick、onLongClick的源码藏身之处


  由于Android系统版本的更新,我查看的是android-23的源码,可能与较低版本的源码会有差异,但是整体的核心流程并不会有改变。

  在源码中,我们关注方法中这部分重要的源码:

  1. //如果设置了mOnTouchListener且onTouch返回true,则不走onTouchEvent
  2. if (li != null && li.mOnTouchListener != null
  3. && (mViewFlags & ENABLED_MASK) == ENABLED
  4. && li.mOnTouchListener.onTouch(this, event)) {
  5. result = true;
  6. }
  7. if (!result && onTouchEvent(event)) {
  8. result = true;
  9. }
  10. 1
  11. 2
  12. 3
  13. 4
  14. 5
  15. 6
  16. 7
  17. 8
  18. 9
  19. 10

  源码中先判断li != null && li.mOnTouchListener != null的条件,我们不用看源码也清楚mOnTouchListener 就是我们调用setOnTouchLister的时候赋值进去的。所以如果设置了OnTouchListener的话这里条件就成立,其次(mViewFlags & ENABLED_MASK) == ENABLED,要View是enable状态条件才成立,事实上默认就已经是enable状态,除非调用setEnable(false)让控件变为不可选取状态。满足了上面两个条件后,则onTouch便会触发,同时会把onTouch返回值作为条件。到这里,我们也就清楚了onTouch的为什么会被优先响应。

  然后,假如onTouch消费了事件,也就是返回了true,则result变量则为true,导致下面的:

  1. if (!result && onTouchEvent(event)) {
  2. result = true;
  3. }
  4. 1
  5. 2
  6. 3

  从!result就已经条件不成立了,所以就不会调用onTouchEvent方法,除非onTouch返回false,这验证了前面我们说的:
  onTouch比onClick方法多了一个返回值,其返回值也表示了是否消耗事件,如果返回了true则不会再调用onTouchEvent方法

  接下来我们再来查看onTouchEvent的方法源码,同样方法源码很多,我们挑重点的来看:

  1. // Use a Runnable and post this rather than calling
  2. // performClick directly. This lets other visual state
  3. // of the view update before click actions start.
  4. if (mPerformClick == null) {
  5. mPerformClick = new PerformClick();
  6. }
  7. //performClick()里面会走onClick
  8. if (!post(mPerformClick)) {
  9. performClick();
  10. }
  11. 1
  12. 2
  13. 3
  14. 4
  15. 5
  16. 6
  17. 7
  18. 8
  19. 9
  20. 10

  从这个代码中mPerformClick 确认是不为空的,然后调用post(mPerformClick),我们直到View里面的post方法其实也就是相当于把runnable任务放进主线程的消息队列来处理,如果post进去失败,则会直接处理performClick();我们看一下这个mPerformClick的实现:

  1. private final class PerformClick implements Runnable {
  2. @Override
  3. public void run() {
  4. performClick();
  5. }
  6. }
  7. 1
  8. 2
  9. 3
  10. 4
  11. 5
  12. 6
  13. public boolean performClick() {
  14. final boolean result;
  15. final ListenerInfo li = mListenerInfo;
  16. if (li != null && li.mOnClickListener != null) {
  17. playSoundEffect(SoundEffectConstants.CLICK);
  18. //调用onClick
  19. li.mOnClickListener.onClick(this);
  20. result = true;
  21. } else {
  22. result = false;
  23. }
  24. sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
  25. return result;
  26. }
  27. 1
  28. 2
  29. 3
  30. 4
  31. 5
  32. 6
  33. 7
  34. 8
  35. 9
  36. 10
  37. 11
  38. 12
  39. 13
  40. 14
  41. 15

  发现PerformClick中其实主要的是执行一个performClick方法,而我们在performClick方法中可以看出,首先代码判断li != null && li.mOnClickListener != null,这里我们也不难看出mOnClickListener 就是我们调用setOnClickListener的时候设置进去的对象,当设置了点击监听事件后此处条件便成立,然后会调用一个播放点击音效,然后调用li.mOnClickListener.onClick(this);这正是我们设置点击监听事件的时候,回调的onClick方法,由此可以验证我们的第二条说法:
  onClick方法是在onTouchEvent里面被回调的,如果onTouch返回了true,onTouchEvent不会被调用,那么onClick也就不会被调用。

  另外一个我们还可能会用到一个长按的监听,我们也给view增加一个长按事件并在onLongClick打印出来,,发现log如下:

log2

  从log上看,当View接收到ACTION_DOWN的时候,并且不松开大概0.5s的时候(log从onTouchEvent到onLongClick的执行时间差大概就是0.5s)会执行onLongClick,当接收到ACTION_UP的时候再执行onClick,而如果onLongClick方法中返回了true,则onClick就不会再执行。

  我们再来看一下关于这个说法的源码依据,并且分析源码中是如何确定长按事件并且回调onLongClick的。

  当手指开始按下的时候,执行了onTouchEvent方法。我们从onTouchEvent关于对ACTION_DOWN的执行逻辑代码如下:

  1. case MotionEvent.ACTION_DOWN:
  2. mHasPerformedLongPress = false;
  3. if (performButtonActionOnTouchDown(event)) {
  4. break;
  5. }
  6. // Walk up the hierarchy to determine if we're inside a scrolling container.
  7. //判断是不是一个滚动视图
  8. boolean isInScrollingContainer = isInScrollingContainer();
  9. // For views inside a scrolling container, delay the pressed feedback for
  10. // a short period in case this is a scroll.
  11. //如果是滚动视图的话
  12. if (isInScrollingContainer) {
  13. mPrivateFlags |= PFLAG_PREPRESSED;
  14. if (mPendingCheckForTap == null) {
  15. mPendingCheckForTap = new CheckForTap();
  16. }
  17. mPendingCheckForTap.x = event.getX();
  18. mPendingCheckForTap.y = event.getY();
  19. //则添加一个延迟消息,大概是getTapTimeout()的时间,实际上就是100ms,如果100ms里面没有滚动则判断为是长按的过程
  20. postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
  21. } else {
  22. // Not inside a scrolling container, so show the feedback right away
  23. //如果不是可滚动布局的话,则直接就是代表长按了
  24. setPressed(true, x, y);
  25. //注意传入的是0,代表一个延迟偏移值,如果值越大则等待形成长按的事件会更短
  26. checkForLongClick(0);
  27. }
  28. break;
  29. 1
  30. 2
  31. 3
  32. 4
  33. 5
  34. 6
  35. 7
  36. 8
  37. 9
  38. 10
  39. 11
  40. 12
  41. 13
  42. 14
  43. 15
  44. 16
  45. 17
  46. 18
  47. 19
  48. 20
  49. 21
  50. 22
  51. 23
  52. 24
  53. 25
  54. 26
  55. 27
  56. 28
  57. 29
  58. 30
  59. 31

  从代码上看首先执行performButtonActionOnTouchDown(event),为true则直接跳出,源码如下:

  1. /**
  2. * Performs button-related actions during a touch down event.
  3. *
  4. * @param event The event.
  5. * @return True if the down was consumed.
  6. *
  7. * @hide
  8. */
  9. protected boolean performButtonActionOnTouchDown(MotionEvent event) {
  10. if ((event.getButtonState() & MotionEvent.BUTTON_SECONDARY) != 0) {
  11. //如果是鼠标右键的话会弹出菜单之类的
  12. if (showContextMenu(event.getX(), event.getY(), event.getMetaState())) {
  13. return true;
  14. }
  15. }
  16. return false;
  17. }
  18. 1
  19. 2
  20. 3
  21. 4
  22. 5
  23. 6
  24. 7
  25. 8
  26. 9
  27. 10
  28. 11
  29. 12
  30. 13
  31. 14
  32. 15
  33. 16
  34. 17

  MotionEvent的BUTTON_SECONDARY其实对应的就是鼠标中的右键,事实上,在手机设备中只要我们用手指触摸的都是返回false,而比如是接入了鼠标,那么鼠标点击右键了就会有展开菜单之类的功能,则会消费掉这个事件,所以在这里我们条件不成立会接着走下面的代码。接着执行:

  1. // Walk up the hierarchy to determine if we're inside a scrolling container.
  2. boolean isInScrollingContainer = isInScrollingContainer();
  3. 1
  4. 2

isInScrollingContainer方法的源码如下:

  1. /**
  2. * @hide
  3. */
  4. public boolean isInScrollingContainer() {
  5. ViewParent p = getParent();
  6. while (p != null && p instanceof ViewGroup) {
  7. if (((ViewGroup) p).shouldDelayChildPressedState()) {
  8. return true;
  9. }
  10. p = p.getParent();
  11. }
  12. return false;
  13. }
  14. 1
  15. 2
  16. 3
  17. 4
  18. 5
  19. 6
  20. 7
  21. 8
  22. 9
  23. 10
  24. 11
  25. 12
  26. 13

  说明该方法是用来遍历View树判断当前按下的View是不是在一个滚动的视图容器中,
  如果是在一个可以滚动的容器中,比如(ListView,ScorllView)那么先设置PFLAG_PREPRESSED标记位,表示用户准备点击,
  随后发出一个延迟的消息来确定用户到底是要滚动还是点击.,通过记录用户按下的坐标和ViewConfiguration.getTapTimeout()指定的时间(源码被设置为100ms),来延迟100ms后判断出用户是滚动了还是点击的

  而这个消息的执行任务是mPendingCheckForTap,点击查看:

  1. private final class CheckForTap implements Runnable {
  2. public float x;
  3. public float y;
  4. @Override
  5. public void run() {
  6. mPrivateFlags &= ~PFLAG_PREPRESSED;
  7. setPressed(true, x, y);
  8. //注意这里区别与非滚动容器,因为滚动容器要花getTapTimeout()这个事件去判断是不是滚动,所以形成长按的话要带上这个偏移量来减去这个时间 checkForLongClick(ViewConfiguration.getTapTimeout());
  9. }
  10. }
  11. 1
  12. 2
  13. 3
  14. 4
  15. 5
  16. 6
  17. 7
  18. 8
  19. 9
  20. 10
  21. 11

  不难看出其实在延迟消息后再调用checkForLongClick,并且参数是ViewConfiguration.getTapTimeout()。
,而如果不是在一个滚动的容器中,则执行:setPressed(true, x, y);标记PFLAG_PRESSED为true表示标记一个按下的状态,再调用
checkForLongClick,不同的是参数是0.

  实际上这两种情况,都是确定了是保持按下状态,不是滚动屏幕。然后再调用checkForLongClick,只不过给出的参数不同罢了。

  到这里也不难看出,checkForLongClick会在检查确定是满足长按条件后执行长按监听的回调。我们看checkForLongClick的源码:

  1. private void checkForLongClick(int delayOffset) {
  2. if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
  3. mHasPerformedLongPress = false;
  4. if (mPendingCheckForLongPress == null) {
  5. mPendingCheckForLongPress = new CheckForLongPress();
  6. }
  7. mPendingCheckForLongPress.rememberWindowAttachCount();
  8. //形成长按的过程,如果这个消息到执行的时候依旧满足条件,则代表长按事件成立,注意延迟时间会减去一个delayOffset偏移量
  9. postDelayed(mPendingCheckForLongPress,
  10. ViewConfiguration.getLongPressTimeout() - delayOffset);
  11. }
  12. }
  13. 1
  14. 2
  15. 3
  16. 4
  17. 5
  18. 6
  19. 7
  20. 8
  21. 9
  22. 10
  23. 11
  24. 12
  25. 13

  从代码上看,是把一个mPendingCheckForLongPress 的任务延迟执行,而执行的时间正是ViewConfiguration.getLongPressTimeout()-delayOffset,这里也就直到了如果不是滚动视图则会马上进入这个方法,传了参数0,所以长按延迟是整个ViewConfiguration.getLongPressTimeout()的时间,如果是在滚动容器的话则因为消耗了100ms的时间去判断是否是滚动,所以在这里就会减掉那个时间。

  我们重点看下mPendingCheckForLongPress是个什么任务,查看下他的源码:

  1. private final class CheckForLongPress implements Runnable {
  2. private int mOriginalWindowAttachCount;
  3. @Override
  4. public void run() {
  5. if (isPressed() && (mParent != null)
  6. && mOriginalWindowAttachCount == mWindowAttachCount) {
  7. //performLongClick里面调用onLongClick,并且返回true则记录mHasPerformedLongPress,而不会再让onClick被调用
  8. if (performLongClick()) {
  9. mHasPerformedLongPress = true;
  10. }
  11. }
  12. }
  13. public void rememberWindowAttachCount() {
  14. mOriginalWindowAttachCount = mWindowAttachCount;
  15. }
  16. }
  17. 1
  18. 2
  19. 3
  20. 4
  21. 5
  22. 6
  23. 7
  24. 8
  25. 9
  26. 10
  27. 11
  28. 12
  29. 13
  30. 14
  31. 15
  32. 16
  33. 17
  34. 18

  因为手指按下后不松开到形成长按的这段等待过程之中,界面是可能发生一些变化的,比如Activity被暂停了或者被重启了,或者这个时候,长按的事件就不应该被响应了

  在View中有一个mWindowAttachCount记录了View的attach次数.他的作用是:当检查长按时的attach次数与长按到形成时的attach一样则处理,否则就不应该形成长按事件. 所以在将检查长按的消息添加时队伍的时候,要记录下当前的windowAttachCount.

  而当满足上面说的条件后,则performLongClick()会被调用,它的源码如下:

  1. /**
  2. * Call this view's OnLongClickListener, if it is defined. Invokes the context menu if the
  3. * OnLongClickListener did not consume the event.
  4. *
  5. * @return True if one of the above receivers consumed the event, false otherwise.
  6. */
  7. public boolean performLongClick() {
  8. sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
  9. boolean handled = false;
  10. ListenerInfo li = mListenerInfo;
  11. if (li != null && li.mOnLongClickListener != null) {
  12. handled = li.mOnLongClickListener.onLongClick(View.this);
  13. }
  14. if (!handled) {
  15. handled = showContextMenu();
  16. }
  17. if (handled) {
  18. performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
  19. }
  20. return handled;
  21. }
  22. 1
  23. 2
  24. 3
  25. 4
  26. 5
  27. 6
  28. 7
  29. 8
  30. 9
  31. 10
  32. 11
  33. 12
  34. 13
  35. 14
  36. 15
  37. 16
  38. 17
  39. 18
  40. 19
  41. 20
  42. 21
  43. 22
  44. 23

  可以看出当我们设置了长按监听事件了之后会回调li.mOnLongClickListener.onLongClick(View.this);,
  而如果方法返回了了true,则mHasPerformedLongPress = true;

  我们在回过头来看onTouchEvent的ACTION_UP的处理逻辑部分代码:

  1. if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
  2. // This is a tap, so remove the longpress check
  3. removeLongPressCallback();
  4. // Only perform take click actions if we were in the pressed state
  5. if (!focusTaken) {
  6. // Use a Runnable and post this rather than calling
  7. // performClick directly. This lets other visual state
  8. // of the view update before click actions start.
  9. if (mPerformClick == null) {
  10. mPerformClick = new PerformClick();
  11. }
  12. if (!post(mPerformClick)) {
  13. performClick();
  14. }
  15. }
  16. }
  17. 1
  18. 2
  19. 3
  20. 4
  21. 5
  22. 6
  23. 7
  24. 8
  25. 9
  26. 10
  27. 11
  28. 12
  29. 13
  30. 14
  31. 15
  32. 16
  33. 17

  可以看到如果mHasPerformedLongPress 为true了,则不会再走performClick()方法回调onClick了。这里验证了我们在上文的说法:如果onLongClick方法中返回了true,则onClick就不会再执行。

  到了这里,我们就已经明白了当一个事件序列被View接收后,onTouch、onClick、onLongClick被回调的原理过程。

后续


  我将在下一章节中,我们继续谈论Android事件处理机制。主要是剖析出从顶级ViewGroup到最低级的View的事件分发处理的原理过程。要理解这个事件处理机制的过程还是有一些难度的,毕竟源码的流程也比较多。不过全部贴出代码来一句句深究其含义并不是我们从源码上理解Android系统机制的办法,源码太多,我们挑出重要的代码部分就已经足够我们来理解这个过程了。

发表评论

表情:
评论列表 (有 0 条评论,101人围观)

还没有评论,来说两句吧...

相关阅读

    相关 Android事件分发机制

    在触摸屏幕的过程中,要涉及到和控件的交互,如何处理多个控件之间的事件处理,保证正常的交互效果。我们今天来看事件分发机制。 零、事件分发的一些基础知识 什么是事件?

    相关 Android事件分发机制

    在触摸屏幕的过程中,要涉及到和控件的交互,如何处理多个控件之间的事件处理,保证正常的交互效果。我们今天来看事件分发机制。 零、事件分发的一些基础知识 什么是事件?

    相关 Android事件分发机制

    Android事件分发机制    就是一个触摸事件发生了,从一个窗口传递到下一个视图,在传递到另一个视图,最后被消费的过程,在android中还是比较复杂的传递流程:   

    相关 Android 事件分发机制

    前言 说到这个事件分发机制呢,我觉得一直以来都是我的弱项,可能它太抽象了,也与我在实际项目中没怎么使用到过,也没自定义过view有着很大的关系。虽然在面试过程中事件分发是必不