深入了解触摸事件的分发

1. 触摸动作及事件序列

(1)触摸事件的动作

    触摸动作一共有三种:ACTION_DOWN、ACTION_MOVE、ACTION_UP。当用户手指接触屏幕时,便产生一个动作为ACTION_DOWN的触摸事件,此时若用户的手指立即离开屏幕,会产生一个动作为ACTION_UP的触摸事件;若用户手指接触屏幕后继续滑动,每当滑动距离超过了系统中预定义的距离常数,就产生一个动作为ACTION_MOVE的触摸事件,系统中预定义的用来判断用户手指在屏幕上的滑动是否是一个ACTION_MOVE动作的这个距离常量叫做TouchSlop,可通过ViewConfiguration.get(getContext()).getScaledTouchSlop()获取。

(2)事件序列

    当用户的手指接触屏幕,在屏幕上滑动的距离超过预定义长度,又离开屏幕,这个过程会产生一系列触摸事件:ACTION_DOWN-->若干个ACTION_MOVE-->ACTION_UP。这一系列触摸事件即为一个事件序列

 

2. 触摸事件的分发

(1)概述

    当产生了一个触摸事件后,系统要负责把这个触摸事件给一个View(TargetView)来处理,touch事件传递到TargetView的过程即为touch事件的分发。

    触摸事件的分发顺序:Activity-->顶级View-->顶级View的子View-->. . .-->Target View

    触摸事件的响应顺序:TargetView --> TargetView的父容器 --> . . . -->顶级View -->Activity


(2)三个方法

    在touch事件分发的过程中,主要会涉及到对三个方法的调用:

  • dispatchTouchEvent方法负责分发touch事件。该方法返回true表示不再继续往下分发,这次touch事件也就没有被“消耗”;返回false表示继续往下分发本次的touch事件。
  • onTouchEvent方法负责处理touch事件。该方法返回true表示已经消耗了当前事件,意味着touch事件已经被相应的控件成功处理;返回false表示没有消耗本次的touch事件。
  • onInterceptTouchEvent方法负责拦截touch事件。该方法返回true的表示拦截当前事件,不再继续往下分发,而是传递给自身的onTouchEvent方法进行处理;返回false表示不拦截,会继续往下分发。
    注意,onInterceptTouchEvent方法只存在于ViewGroup中,原因很简单,非容器View是没有子View的,所以一旦touch事件到了非容器View中,所以拦截也就无从谈起了。

(3)touch事件分发的具体过程

    在讲解touch事件分发前,我们要明确touch事件分发的目的在于找到一个处理本次事件的 目标控件(Target View),因为实际场景中可能同一个Activity包含的View层级体系中的许多控件都设置了OnTouchListener,所以我们的目的是找到一个应该对本次touch事件负责的控件。

  a. Activity对touch事件的分发

    当用户手指接触屏幕时,便产生了一个touch事件,封装了touch事件的MotionEvent对象最先被传递给当前Activity的dispatchTouchEvent方法,dispatchTouchEvent方法负责touch事件的分发。每个Activity都封装了一个Window对象,WIndow是对Activity包含的窗口的抽象,分发touch事件的实际工作由当前Activity的Window完成,而Window会将touch事件传递给DecorView(当前用户界面顶级View)。Activity的dispatchTouchEvent方法代码如下:

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 }

    首先,本次touch事件是ACTION_DOWN事件,则会调用onUserInteraction方法,该方法在开始对一个touch事件序列进行分发前会被调用,它的返回值为void,不会影响到touch事件的分发。接着往下,我们可以看到touch事件会交由Window的superDispatchTouchEvent进行分发,这个方法内部包含整个分发与响应的逻辑。若这个方法返回true,意味touch事件的分发过程结束;返回false则说明经过层层分发,没有子View对这个事件进行处理,即所有子View的onTouchEvent方法都返回false(也意味着这个touch事件没有被“消耗”)。这时会调用Activity的onTouchEvent方法来处理这个touch事件。

    接下来我们看一看superDispatchTouchEvent中究竟包含了怎样的逻辑。在Window的superDispatchTouchEvent方法中,首先会把touch事件分发给DecorView,因为它是当前用户界面的顶级View。Window的superDispatchTouchEvent方法如下:

1 public abstract boolean superDispatchTouchEvent(MotionEvent ev);

    是个抽象方法,这个方法由Window的实现类PhoneWindow实现,PhoneWindow的superDispatchTouchEvent方法的代码如下:

1 public boolean superDispatchTouchEvent(MotionEvent ev) {
2     return mDecor.superDispatchTouchEvent(event);
3 }

    由以上代码可得,PhoneWindow的superDispatchTouchEvent方法实际上是通过DecorView的superDispatchTouchEvent方法来完成自己的工作,也就是说,当前Activity的Window直接将这个touch事件传递给了DecorView。目前为止,touch事件已经经过了如下的分发:Activity-->Window-->DecorView。


 b. 顶级View对touch事件的分发

    经过Activity与Window的分发,现在touch事件已经被传递到了DecorView的superDispatchTouchEvent方法中。DecorView本质上是一个ViewGroup(更具体的说是FrameLayout),ViewGroup的dispatchTouchEvent方法所做的工作可以分为如下几个阶段,第一个阶段的主要代码如下:

1 //Handle an initial down.
2 if (actionMasked == MotionEvent.ACTION_DOWN) { 3 //Throw away all previous state when starting a new touch gesture. 4 //The framework may have dropped the up or cancel event for the previous gesture due to an app switch, ANR, or some other state change. 5  cancelAndClearTouchTargets(ev); 6  resetTouchState(); 7 }

    第一阶段的主要工作有俩:一是在第6行的resetTouchState方法中完成了对FLAG_DISALLOW_INTERCEPT标记的重置(只有当前事件为ACTION_DOWN时才会重置);二是第5行的cancelAndClearTouchTargets方法会清除当前MotionEvent的touch target。关于FLAG_DISALLOW_INTERCEPT标记和touch target,在下文会有相关说明。

    第二阶段的主要工作是决定当前ViewGroup是否拦截本次的touch事件,主要代码如下:

 1 //Check for interception.
 2 final boolean intercepted;
 3 if (actionMasked == MotionEvent.ACTION_DOWM || mFirstTouchTarget != null) {
 4     final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
 5     if (!disallowIntercept) {
 6         intercepted = onInterceptTouchEvent(ev); 
 7         ev.setAction(action); //restore action in case it was changed
 8     } else {
 9         intercepted = false;
10     }
11 } else {
12     //There are no touch targets and this action is not an initial down so this view group continues to intercept touches.
13     intercept =true;
14 }

    由以上代码我们可以知道,当一个touch事件被传递到ViewGroup时,会先判断这个touch事件的动作是否是ACTION_DOWN,如果这个事件是ACTION_DOWN或者mFirstTouchTarget不为null,就会根据FLAG_DISALLOW_INTERCEPT标记决定是否拦截这个touch事件。那么mFirstTouchTarget是什么呢?当touch事件被ViewGroup的子View成功处理时,mFirstTouchTarget就会被赋值为成功处理touch事件的View,也就是上面提到的touch target。

    那么什么情况自mFirstTouchTarget为null呢?若本次touch事件为ACTION_DOWN,那么说明是一个touch事件序列的第一个事件(我们的手指刚接触屏幕),这意味着还没有touch事件到过当前ViewGroup的子View,因而此时mFirstTouchTarget必然为null。不过因为本次事件为ACTION_DOWN,所以会检测下disallowIntercept,不过之前我们对这个标记进行过重置,因而它现在是false,所以ACTION_DOWN事件来到时,会直接传给onInterceptTouchEvent方法。

    我们再看下什么情况下mFirstTouchTarget不为null呢?我们已经提到过,那就是本次的touch事件被ViewGroup的子View成功处理后,mFirstTouchTarget就会被赋值为那个子View。

    总结一下上述代码的流程:在子View不干预ViewGroup的拦截的情况下(上述代码中的disallowIntercept为false),若当前事件为ACTION_DOWN或者mFirstTouchTarget不为空(本次touch事件不是序列中的第一个,且之前的事件已被当前ViewGroup的某子View成功处理),则会调用ViewGroup的onInterceptTouchEvent方法来决定最终是否拦截此事件(该方法默认返回false,也就是默认情况下不拦截);若mFirstTouchTarget为空并且此事件不是ACTION_DOWN,当前ViewGroup就拦截下此事件。 一旦ViewGroup拦截了某次touch事件,那么mFirstTouchTarget就不会被赋值,因此当再有ACTION_MOVE或是ACTION_UP传递到该ViewGroup时,mTouchTarget就为null,所以上述代码第3行的条件就为false,ViewGroup会拦截下来。由此可得到的结论是:一旦ViewGroup拦截了某次事件,则同一事件序列中的剩余事件也会它默认被拦截而不会再询问是否拦截(即不会再调用onInterceptTouchEvent)。还有一种情况是没拦截ACTION_DOWN,却拦截了同一touch事件序列的ACTION_MOVE和ACTION_UP,那么touch事件只能传递到拦截它的这层,并且会发出ACTION_CANCEL。

    若本阶段拦截了touch事件,则touch事件不会再往下分发,而是直接交由拦截它的控件的onTouchEvent方法处理。

    这里存在一种特殊情形,就是子View通过requestDisallowInterceptTouchEvent方法设置父容器的FLAG_DISALLOW_INTERCEPT为true,这个标记指示是否不允许父容器拦截,为true表示不允许。这样做能够禁止父容器拦截除ACTION_DOWN以外的所有touch事件。之所以不能够拦截ACTION_DOWN事件,是因为每当ACTION_DOWN事件到来时,都会重置FLAG_DISALLOW_INTERCEPT这个标记位为默认值(false),所以每当开始一个新touch事件序列(即到来一个ACTION_DOWN动作),都会通过调用onInterceptTouchEven询问ViewGroup是否拦截此事件。当ACTION_DOWN事件到来时,重置标记位的工作是在上面的第一阶段完成的。   

    接下来,会进入第三阶段的工作

 1 final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL;
 2 final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
 3 TouchTarget newTouchTarget = null;
 4 boolean alreadyDispatchedToNewTouchTarget = false;
 5 if (!canceled && !intercepted) {
 6     // 不是ACTION_CANCEL并且不拦截
 7     if (actionMasked == MotionEvent.ACTION_DOWN) {
 8           // 若当前事件为ACTION_DOWN则去寻找这次事件新出现的touch target
 9           final int actionIndex = ev.getActionIndex();
10 
11           ...
12 
13           final int childrenCount = mChildrenCount;
14           if (newTouchTarget == null && childrenCount != 0) {
15               // 根据触摸的坐标寻找能够接收这个事件的touch target
16               final float x = ev.getX(actionIndex);
17               final float y = ev.getY(actionIndex);
18 
19               final View[] children = mChildren;
20               // 遍历所有子View
21               for (int i = childrenCount - 1; i >= 0; i--) {
22                   final int childIndex = i;
23                   final View child = children[childIndex];
24                   // 寻找可接收这个事件并且touch事件坐标在其区域内的子View
25                   if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) {
26                       continue;
27                   }
28 
29                   newTouchTarget = getTouchTarget(child); // 找到了符合条件的子View,赋值给newTouchTarget
30                   if (newTouchTarget != null) {
31                       //Child is already receiving touch within its bounds.
32                       //Give it the new pointer in addition to ones it is handling.
33                       newTouchTarget.pointerIdBits |= idBitsToAssign;
34                       break;
35                   }
36                   resetCancelNextUpFlag(child);
37                   // 把ACTION_DOWN事件传递给子组件进行处理
38                   if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
39                       //Child wants to receive touch within its bounds.
40                       mLastTouchDownTime = ev.getDownTime();
41                       if (preorderedList != null) {
42                           //childIndex points into presorted list, find original index
43                           for (int j=0;j<childrenCount;j++) {
44                               if (children[childIndex]==mChildren[j]) {
45                                   mLastTouchDownIndex=j;
46                                   break;
47                               }
48                           }
49                       } else {
50                           mLastTouchDownIndex = childIndex;
51                       }
52                       mLastTouchDownX = ev.getX();
53                       mLastTouchDownY = ev.getY();
54                       //把mFirstTouchTarget赋值为newTouchTarget,此子View成为新的touch事件的起点
55                       newTouchTarget = addTouchTarget(child, idBitsToAssign);
56                       alreadyDispatchedToNewTouchTarget = true;
57                       break;
58                  }                      
59              }
60          }
61     }
62 }

    当ViewGroup不拦截本次事件,则touch事件会分发给它的子View进行处理,相关代码从第21行开始:遍历所有ViewGroup的子View,寻找能够处理此touch事件的子View,若一个子View不在播放动画并且touch事件坐标位于其区域内,则该子View能够处理此touch事件,并且会把该子View赋值给newTouchTarget。

    若当前遍历到的子View能够处理此touch事件,就会进入第38行的dispatchTransformedTouchEvent方法,该方法实际上调用了子View的dispatchTouchEvent方法。dispatchTransformedTouchEvent方法中相关的代码如下:

1 if (child == null) {
2     handled = super.dispatchTouchEvent(event);
3 } else {
4     handled = child.dispatchTouchEvent(event);
5 }

    若dispatchTransformedTouchEvent方法传入的child参数不为null,则会调用child(即处理touch事件的子View)的dispatchTouchEvent方法。若该子View的dispatchTouchEvent方法返回true,则dispatchTransformedTouchEvent方法也会返回true,则表示成功找到了一个处理该事件的touch target,会在第55行把newTouchTarget赋值给mFirstTouchTarget(这一赋值过程是在addTouchTarget方法内部完成的),并跳出对子View遍历的循环。若子View的dispatchTouchEvent方法返回false,ViewGroup就会把事件分发给下一个子View。

    若遍历了所有子View后,touch事件都没被处理(该ViewGroup没有子View或是所有子View的dispatchTouchEvent返回false),ViewGroup会自己处理touch事件,相关代码如下:

1 if (mFirstTouchTarget == null) {
2     handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
3 }

    由以上代码可知,ViewGroup自己处理touch事件时,会调用dispatchTransformedTouchEvent方法,传入的child参数为null。根据上文的分析,传入的chid为null时,会调用super.dispatchTouchEvent方法,即调用View类的dispatchTouchEvent方法。

 

c. View对touch事件的处理

    View类的dispatchTouchEvent方法的主要代码如下:

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

    由上述代码可知,View对touch事件的处理过程如下:由于View不包含子元素,所以它只能自己处理事件。它首先会判断是否设置了OnTouchListener,若设置了,会调用onTouch方法,若onTouch方法返回true(表示该touch事件已经被消耗),则不会再调用onTouchEvent方法;若onTouch方法返回false或没有设置OnTouchListener,则会调用onTouchEvent方法,onTouchEvent对touch事件进行具体处理的相关代码如下:

 1 if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
 2     switch (event.getAction()) {
 3         case MotionEvent.ACTION_UP:
 4             boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
 5             if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
 6                 . . .
 7                 if (!mHasPerformedLongPress) {
 8                     //This is a tap, so remove the longpress check 
 9                     removeLongPressCallback();
10                     
11                     //Only perform take click actions if we were in the pressed state
12                     if (!focusTaken) {
13                         //Use a Runnable and post this rather than calling performClick directly.
14                         //This lets other visual state of the view update before click actions start.
15                         if (mPerformClick == null) {
16                             mPerformClck = new PeformClick();
17                         }
18                         if (!post(mPerformClick)) {
19                             performClick();
20                         }
21                     }
22                 }
23                 . . .
24             }
25             break;
26     }
27     . . .
28     return true;
29 }

    由以上代码可知,只要View的CLICKABLE属性和LONG_CLICKABLE属性有一个为true(View的CLICKABLE属性和具体View有关,LONG_CLICKABLE属性默认为false,setOnClikListener和setOnLongClickListener会分别自动将以上俩属性设为true),那么这个View就会消耗这个touch事件,即使这个View处于DISABLED状态。若当前事件是ACTION_UP,还会调用performClick方法,该View若设置了OnClickListener,则performClick方法会在其内部调用onClick方法。performClick方法代码如下:

 1 public boolean performClick() {
 2     final boolean result;
 3     final ListenerInfo li = mListenerInfo;
 4     if (li != null && li.mOnClickListener != null) {
 5         playSoundEffect(SoundEffectConstants.CLICK);
 6         li.mOnClickListener.onClick(this);
 7         result = true;
 8     } else {
 9         result = false;
10     }
11     sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
12     return result;
13 }

 

以上是我学习Android中触摸事件分发后的简单总结,很多地方叙述的还不够清晰准确,如有问题欢迎大家在评论区一起讨论 :)

   


 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Android触摸事件分发是指在屏幕上发生触摸事件时,Android系统将该事件分发给适当的视图处理。触摸事件分发的过程涉及多个层级,包括Activity、ViewGroup和View。 当用户触摸屏幕时,Android系统首先将触摸事件发送给当前活动的Window。然后,Window将事件传递给顶级ViewGroup(通常是根布局),该ViewGroup负责协调子视图的事件处理。 在ViewGroup中,触摸事件会按照一定的规则进行分发。常见的分发方式有以下几种: 1. 捕获阶段(Capture Phase):从根布局向下遍历,让父级ViewGroup有机会拦截事件。可以通过重写`onInterceptTouchEvent()`方法来实现事件的拦截。 2. 目标阶段(Target Phase):如果没有被拦截,触摸事件将传递给目标View,即最终接收事件的视图。目标View将调用`onTouchEvent()`方法处理事件。 3. 冒泡阶段(Bubble Phase):如果目标View没有消耗事件事件将向上传递给父级ViewGroup,直到根布局。在这个阶段,可以通过返回值来控制是否继续向上传递。 除了上述的默认分发方式外,还可以通过重写`dispatchTouchEvent()`方法来自定义事件分发逻辑。通过调用`super.dispatchTouchEvent()`来保持默认行为,或者根据需求进行处理。 总结来说,Android触摸事件分发涉及捕获阶段、目标阶段和冒泡阶段,通过重写相关方法或自定义分发逻辑,可以实现对触摸事件的处理和控制。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值