Android View的事件分发机制深入分析讲解

作者:Hdnw 时间:2023-06-18 21:28:49 

1.分发对象-MotionEvent

事件类型有:

1.ACTION_DOWN-----手指刚接触屏幕

2.ACTION_MOVE------手指在屏幕上移动

3.ACTION_UP------手指从屏幕上松开的一瞬间

4.ACTION_CANCEL-----事件被上层拦截时触发

MotionEvent主要的方法:

getX()得到事件发生的x轴坐标(相对于当前视图)
getY()得到事件发生的y轴坐标(相对于当前视图)
getRawX()得到事件发生的x轴坐标(相对于屏幕左顶点)
getRawY()得到事件发生的y轴坐标(相对于屏幕左顶点)

2.如何传递事件

1.传递流程

底层IMS->ViewRootImpl->activity->viewgroup->view

2.事件分发的源码解析

1.Activity对点击事件的分发过程

Activity#dispatchTouchEvent()

public boolean dispatchTouchEvent(MotionEvent ev) {
       if (ev.getAction() == MotionEvent.ACTION_DOWN) {
           onUserInteraction();
       }
//事件交给Activity所附属的Window进行分发,如果返回true,循环结束,返回false,没人处理
       if (getWindow().superDispatchTouchEvent(ev)) {
           return true;
       }
//所有View的onTouchEvent都返回false,那么Activity的onTouchEvent就会被调用
       return onTouchEvent(ev);
}

Window#superDispatchTouchEvent

public abstract boolean superDispatchTouchEvent(MotionEvent event);

PhoneWindow#superDispatchTouchEvent

public boolean superDispatchTouchEvent(MotionEvent event) {
       return mDecor.superDispatchTouchEvent(event);
   }

DecorView#superDispatchTouchEvent()

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

ViewGroup#dispatchTouchEvent()

public boolean dispatchTouchEvent(MotionEvent ev) {<!--{cke_protected}{C}%3C!%2D%2D%20%2D%2D%3E-->

2.顶级View对点击事件的分发过程

把ViewGroup的dispatchTouchEvent()方法中的代码进行分段说明

第一段:

描述的是View是否拦截点击事件这个逻辑

// Check for interception.
           final boolean intercepted;
           if (actionMasked == MotionEvent.ACTION_DOWN
                   || mFirstTouchTarget != null) {//事件类型为down或者mFirstTouchTarget有值
               final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
               if (!disallowIntercept) {
                   intercepted = onInterceptTouchEvent(ev);//询问是否拦截,方法返回true就拦截
                   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;//直接拦截了
           }

当事件类型为down或者mFirstTouchTarget有值时,就不拦截当前事件,否则直接拦截了这个事件。那么mFirstTouchTarget什么时候有值?当ViewGroup不拦截事件并且把事件交给子元素处理时,mFirstTouchTarget就有值并且指向子元素。所以当事件类型为down并且拦截事件,那么mFirstTouchTarget为空,这会让后面的事件move和up无法满足mFirstTouchTarget有值的条件,直接无法调用onInterceptTouchEvent方法。

特殊情况:通过requestDisallowInterceptTouchEvent方法来设置标记位FLAG_DISALLOW_INTERCEPT,ViewGroup就无法拦截除了ACTION_DOWN以外的点击事件,这个标记位无法影响ACTION_DOWN事件,因为当事件为ACTION_DOWN时,就会重置这个标记位,将导致子View设置的这个标记位无效。

总结:

1.当ViewGroup决定拦截事件后,那么后续的点击事件将会默认交给它处理并且不再调用它的onInterceptTouchEvent方法。

2.当ViewGroup不拦截ACTION_DOWN事件,那么标记位FLAG_DISALLOW_INTERCEPT让ViewGroup不再拦截事件。

第二段:

当ViewGroup不拦截事件时,分发事件给子View,看哪个子View处理事件

final int childrenCount = mChildrenCount;
                   if (newTouchTarget == null && childrenCount != 0) {
                       final float x =
                               isMouseEvent ? ev.getXCursorPosition() : ev.getX(actionIndex);
                       final float y =
                               isMouseEvent ? ev.getYCursorPosition() : ev.getY(actionIndex);
                       // Find a child that can receive the event.
                       // Scan children from front to back.
                      //对子元素进行排序
                       final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                       final boolean customOrder = preorderedList == null
                               && isChildrenDrawingOrderEnabled();
                       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 there is a view that has accessibility focus we want it
                           // to get the event first and if not handled we will perform a
                           // normal dispatch. We may do a double iteration but this is
                           // safer given the timeframe.
                           if (childWithAccessibilityFocus != null) {
                               if (childWithAccessibilityFocus != child) {
                                   continue;
                               }
                               childWithAccessibilityFocus = null;
                               i = childrenCount - 1;
                           }
                           if (!child.canReceivePointerEvents()
                                   || !isTransformedTouchPointInView(x, y, child, null)) {
                               ev.setTargetAccessibilityFocus(false);
                               continue;
                           }
                           newTouchTarget = getTouchTarget(child);
                           if (newTouchTarget != null) {
                               // Child is already receiving touch within its bounds.
                               // Give it the new pointer in addition to the ones it is handling.
                               newTouchTarget.pointerIdBits |= idBitsToAssign;
                               break;
                           }
                           resetCancelNextUpFlag(child);
                           if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                               // Child wants to receive touch within its bounds.
//
                               mLastTouchDownTime = ev.getDownTime();
                               if (preorderedList != null) {
                                   // childIndex points into presorted list, find original index
                                   for (int j = 0; j < childrenCount; j++) {
                                       if (children[childIndex] == mChildren[j]) {
                                           mLastTouchDownIndex = j;
                                           break;
                                       }
                                   }
                               } else {
                                   mLastTouchDownIndex = childIndex;
                               }
                               mLastTouchDownX = ev.getX();
                               mLastTouchDownY = ev.getY();
                               newTouchTarget = addTouchTarget(child, idBitsToAssign);
                               alreadyDispatchedToNewTouchTarget = true;
                               break;
                           }

遍历ViewGroup的所有的子元素,判断子元素是否在播动画和点击事件的坐标是否落在子元素的区域内,如果是,就能接收到点击事件,并且事件会传递给它来处理。

我们来看一下dispatchTransformedTouchEvent方法

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
           View child, int desiredPointerIdBits) {
           ...
           if (child == null) {
               handled = super.dispatchTouchEvent(event);
           } else {
               handled = child.dispatchTouchEvent(event);
           }
           ...
}

dispatchTransformedTouchEvent实际上调用的是子元素的dispatchTouchEvent方法。

如果子元素的dispatchTouchEvent方法返回true,那么mFirstTouchTarget就会被赋值同时跳出for循环

newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;

这几行代码完成了mFirstTouchTarget的赋值并终止对子元素的遍历,如果子元素的dispatchTouchEvent方法返回false,那么ViewGroup就会把事件分发给下一个子元素。

其实mFirstTouchTarget真正的赋值是在addTouchTarget方法里面,mFirstTouchTarget是一种单链表结构。

private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
       final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
       target.next = mFirstTouchTarget;
       mFirstTouchTarget = target;
       return target;
   }

第三段:

执行事件

//当前View的事件处理代码
if (mFirstTouchTarget == null) {
         // No touch targets so treat this as an ordinary view.
          handled = dispatchTransformedTouchEvent(ev, canceled, null,
           TouchTarget.ALL_POINTER_IDS);
} else {
//子View的事件处理代码
...

dispatchTransformedTouchEvent方法的第三个参数为null,则会调用super.dispatchTouchEvent方法,也就是View的dispatchTouchEvent方法,所以点击事件给View处理。

View对点击事件的处理过程

View(不包含ViewGroup)是一个单独的元素,没有子元素,只能自己处理事件。

public boolean dispatchTouchEvent(MotionEvent event) {
  ...
  boolean result = false;
  ...
  if (onFilterTouchEventForSecurity(event)) {
           if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
               result = true;
           }
           //noinspection SimplifiableIfStatement
           ListenerInfo li = mListenerInfo;
           if (li != null && li.mOnTouchListener != null
                   && (mViewFlags & ENABLED_MASK) == ENABLED
                   && li.mOnTouchListener.onTouch(this, event)) {
               result = true;
           }

if (!result && onTouchEvent(event)) {
               result = true;
           }
    }
   ...
   return result;
}

首先判断是否设置了OnTouchListener,如果OnTouchListener的onTouch方法返回true,就不会调用onTouchEvent方法,否则就会调用onTouchEvent方法。

public boolean onTouchEvent(MotionEvent event) {
          ...
          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;
               // A disabled view that is clickable still consumes the touch
               // events, it just doesn't respond to them.
                return clickable;
         }
              ...
}

不可用状态下的View照样会消耗点击事件

switch (action) {
   case MotionEvent.ACTION_UP:
       mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
        if ((viewFlags & TOOLTIP) == TOOLTIP) {
                 handleTooltipUp();
        }
        ...
        if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
        ...
             if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                          removeLongPressCallback();

// Only perform take click actions if we were in the pressed state
                           if (!focusTaken) {
                               // Use a Runnable and post this rather than calling
                               // performClick directly. This lets other visual state
                               // of the view update before click actions start.
                               if (mPerformClick == null) {
                                   mPerformClick = new PerformClick();
                               }
                               if (!post(mPerformClick)) {
                                   performClickInternal();
                               }
                           }
               }    
        }
        ...
        mIgnoreNextUpEvent = false;
        break;

当ACTION_UP事件发生时,会触发performClick方法,如果View设置了OnClickListener,那么performClick方法内部会调用它的onClick方法。

private boolean performClickInternal() {
       // Must notify autofill manager before performing the click actions to avoid scenarios where
       // the app has a click listener that changes the state of views the autofill service might
       // be interested on.
       notifyAutofillManagerOnClick();
       return performClick();
   }
public boolean performClick() {
       final boolean result;
       final ListenerInfo li = mListenerInfo;
       //关键代码,判断是否设置了onClickListener
       if (li != null && li.mOnClickListener != null) {
           playSoundEffect(SoundEffectConstants.CLICK);
           li.mOnClickListener.onClick(this);
           result = true;
       } else {
           result = false;
       }
       ...
       return result;//最终返回执行结果
}

点击事件的分发机制的源码实现已经分析完了。

3.主要方法

1.dispatchTouchEvent:用来进行事件的分发,如果事件可以传递给当前View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent方法的影响,表示是否消耗当前事件。

2.onInterceptTouchEvent:用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一事件序列当中,此方法不会被再次调用,返回结果表示是否拦截当前事件。

3.onTouchEvent:用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件。

4.requestDisallowInterceptTouchEvent:一般用于子View中,要求父View不拦截事件。

5.dispatchTransformedTouchEvent:如果child不为空,就发到child的dispatchTouchEvent中,否则发给自己。

4.事件传递中listener

onTouch,performClick和onClick调用的顺序以及onTouch返回值的影响?

当一个View需要处理事件时,View的dispatchTouchEvent方法中,如果设置了OnTouchListener,那么OnTouchListener的onTouch方法会被调用,当onTouch方法返回true时,onTouchEvent就不会被调用,当onTouch方法返回false时,onTouchEvent方法就被调用,在onTouchEvent方法里面进入performClick方法,在performClick方法里面判断是否设置onClickListener,并且如果设置了onClickListener,那么onClick方法就被调用,performClick方法就返回true,如果没有设置了onClickListener,performClick方法就返回false。

总的来说方法调用的顺序为

Android View的事件分发机制深入分析讲解

5.滑动冲突如何用事件分发处理

滑动冲突定义:当有内外两层View都可以响应事件时,事件由谁来决定。

滑动冲突类型:1.当内外两层View滑动方向不一致

2.当内外两层滑动方向一致的时候

3.两种情况叠加

解决思路:

内部拦截:dispatchTouchEvent+dispatchTransformedTouchEvent

重写子元素的dispatchTouchEvent方法

down事件分发给子元素,move事件是看条件的,如果不满足条件,就把事件交给子元素处理,如果满足条件,就会取消子元素的处理事件,然后把事件交给父元

public boolean dispatchTouchEvent(MotionEvent event) {
       int x = (int) event.getX();
       int y = (int) event.getY();
       switch (event.getAction()) {
           case MotionEvent.ACTION_DOWN: {
               //down事件,父容器不要拦截我
               parent.requestDisallowInterceptTouchEvent(true);
               break;
           }
           case MotionEvent.ACTION_MOVE: {
               int deltaX = x - mLastX;
               int deltaY = y - mLastY;
               if (父容器需要此类点击事件) {
                  //父容器拦截我
                   parent.requestDisallowInterceptTouchEvent(false);
               }
               break;
           }
           case MotionEvent.ACTION_UP: {
               break;
           }
           default:
               break;
       }
       mLastX = x;
       mLastY = y;
       return super.dispatchTouchEvent(event);
   }

(当move事件时,进入第一块代码,调用intercepted = onInterceptTouchEvent(ev),我们在onInterceptTouchEvent方法中设置不是down事件就返回true,所以intercepted为true,然后第二块代码不会执行,进入第三块代码,因为intercepted为true,所以cancelChild就为true,取消子元素事件执行,调用dispatchTransformedTouchEvent方法,cancel为true->

event.setAction(MotionEvent.ACTION_CANCEL)->handled = child.dispatchTouchEvent(event)

把mFirstTouchTarget设置为空,所以到下一个move事件来的时候,mFirstTouchTarget是为空的,在第一段代码中intercepted为true,第二段代码不执行,第三块代码走dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS),即由当前View的事件处理代码(父元素))

重写父元素的onInterceptTouchEvent方法

当为down事件时,要return false,因为在ViewGroup的dispatchTouchEvent方法中,当为down事件时,会调用resetTouchState()方法,在resetTouchState()方法里面会重置状态,把mGroupFlags也重置,这样会导致在前面的parent.requestDisallowInterceptTouchEvent(true)没有用,所以我们在onInterceptTouchEvent方法里面要设置为down事件时返回false,因为在down事件时onInterceptTouchEvent一定会执行。

public boolean onInterceptTouchEvent(MotionEvent event) {
       int action = event.getAction();
       if (action == MotionEvent.ACTION_DOWN) {
           super.onInterceptTouchEvent(event);
           return false;
       } else {
           return true;
       }
   }

外部拦截:onInterceptTouchEvent

点击事件先经过父容器的拦截处理,如果父容器需要这件事就拦截,不需要就不拦截。

public boolean onInterceptTouchEvent(MotionEvent event) {
       boolean intercepted = false;
       int x = (int) event.getX();
       int y = (int) event.getY();
       switch (event.getAction()) {
           case MotionEvent.ACTION_DOWN: {
               intercepted = false;
               break;
           }
           case MotionEvent.ACTION_MOVE: {
               if (满足父容器的拦截要求) {
                   intercepted = true;
               } else {
                   intercepted = false;
               }
               break;
           }
           case MotionEvent.ACTION_UP: {
               intercepted = false;
               break;
           }
           default:
               break;
       }
       mLastXIntercept = x;
       mLastYIntercept = y;
       return intercepted;
   }

来源:https://blog.csdn.net/weixin_63357306/article/details/128629042

标签:Android,View,事件分发
0
投稿

猜你喜欢

  • java dump文件怎么生成和分析-JMAP用法详解

    2021-06-03 23:59:43
  • spring boot validation参数校验实例分析

    2023-02-03 02:50:38
  • springSecurity之AuthenticationProvider用法解析

    2022-09-07 20:55:01
  • Java Timer使用讲解

    2023-11-28 20:30:33
  • SrpingDruid数据源加密数据库密码的示例代码

    2021-06-21 03:26:26
  • C#实现数字华容道游戏

    2023-10-26 10:01:07
  • idea 设置鼠标悬停(放上)弹出注释的方法

    2022-03-19 21:10:52
  • Java安全之Tomcat6 Filter内存马问题

    2022-11-20 07:29:23
  • Android token过期刷新处理的方法示例

    2023-11-23 14:11:40
  • MyBatis的 config.xml标签

    2021-07-18 02:01:34
  • Android基于ListView实现类似Market分页加载效果示例

    2021-10-01 10:44:32
  • Java中接收键盘输入的三种方法

    2023-11-13 16:11:29
  • javaweb中Http协议详解

    2022-03-21 05:12:41
  • Android Studio设置绘制布局时的视图

    2021-09-05 08:15:46
  • Java实现二分搜索树的示例代码

    2023-08-05 10:43:40
  • android SDk中常用的java包介绍

    2021-12-18 01:04:45
  • C# InitializeComponent()方法案例详解

    2022-06-12 03:07:06
  • C#微信公众平台开发之access_token的获取存储与更新

    2023-12-16 06:12:04
  • C#多线程之Thread中Thread.Join()函数用法分析

    2022-01-20 14:47:58
  • C#实现集合转换成json格式数据的方法

    2022-03-18 03:28:50
  • asp之家 软件编程 m.aspxhome.com