第三章 View 的事件体系.md

何言 2021年08月11日 76次浏览

第三章 View 的事件体系

3.1 View 基础知识

3.1.1 什么是 View

View 是 Android 中界面层的控件的一种抽象,它代表了一个控件。它派生出的各种 子类 如 TextView 等,共同组成一个界面。同时,View 有一个很特殊的抽象派生类 ViewGroup,它可以存放多个 子 View。最终 整个 View 体系会形成一棵树,这和前端的 DOM 树类似。而 ViewGroup 又派生出了各种布局,如 LinearLayout 等。

3.1.2 View 的位置参数

在 Android 的 View 中,使用的是相对定位,即每个 View 都会储存自己在 父 ViewGroup 中的位置,而最终它要显示在屏幕什么位置,则需要从父节点不断向下绘制,最终完成整体的绘制。

View 是矩形模型,由四个顶点的坐标决定,而他们坐标分量分别对应 View 的四个属性:top, left, right, bottom. 具体对应关系可以看下图:

image20210503212802663.png

可以得出 View 的宽高公式:

width = right-left
height = bottom - top

此外,从 Android3.0 开始,View 额外增加了四个参数:x, y, translationX, translationY. 其中 x, y 为 View 左上角的坐标,而 translationX 与 translationY 表示在原有 四个属性的基础上整体的便宜,因此可以得到以下公式:

x = left + translationX
y = top + translationY

可以参考下图:

image20210503212910647.png

3.1.3 MotionEvent 和 TouchSlop

  1. MotionEvent

    MotionEvent 是一个实体类,表示一个触摸事件,它有一些属性:

    • Action:它表明触摸事件的类型,取值与常量对应,其中 ACTION_DOWN 表示手指刚按下,ACTION_MOVE 表示手指在屏幕移动,ACTION_UP 表示手指在屏幕上松开。除此之外还有第二根手指按下等 ACTION,这里不做详解。
    • x,y:通过调用 getX()/getY() 方法获取,获取点击的位置相对于点击的位置最顶层的 View 的坐标
    • rawX, rawY:通过 getRawX()/getRawY() 方法获取,获取点击位置相对于手机屏幕的坐标

    当我们手指按下后抬起时,会产生两个事件,分别是 ACTION_DOWN 与 ACTION_UP

    当我们手指按下滑动一段距离后抬起时,会产生若干个事件,首先是 ACTION_DOWN,然后再滑动时不断产生 ACTION_MOVE ,当然坐标会不断更新,最后抬起时产生一个 ACTION_UP。

  2. TouchSlop

    这个是系统能识别的最小滑动像素。当手指再屏幕上滑动时,如果滑动距离少于这个值,则系统会认为它不在滑动。这个和硬件有关,在不同设备上是不同的,可以通过以下方式获取:

    • ViewConfiguration.get(context).getScledTouchSlop()

    这个常量还是比较常用的,例如当你需要判断用户是再横向滑动、纵向滑动还是同时滑动时,可以通过两个 MOVE 事件的坐标差与该值比较(因为人手滑动的时候一般会有轻微抖动,直接判断坐标是否相等会破坏用户体验)。

3.1.4 VelocityTracker、GestureDetector 和 Scroller

  1. VelocityTracker

    速度追踪器,用于追踪手指在滑动过程中的速度,包括水平方向的速度和竖直方向。使用也很简单,先获取一个对象:

    VelocityTracker vt = VelocityTracker.obtain();
    

    然后我们在 View 的 OnTouchEvent 方法中将事件传递给追踪器:

    vt.addMovement(event);
    

    当我们需要获取速度时,先调用 computeCurrentVelocity(Long) 方法计算速度,然后就可以获取速度了

    vt.computeCurrentVelocity(1000);
    float vX = vt.getXVelocity();
    float vY = vt.getYVelocity();
    

    其中,computeCurrentVelocity 方法的参数为时间段,实际上,我们获取的速度计算公式如下:

    velocity = ( 终点坐标 - 起点坐标 )/ 时间段
    

    其中时间段就是我们传入的数值,以毫秒为单位。这里坐标的方向是向右为 X 轴方向,向下为 Y 轴方向,因此如果在该时间段内手指往左或往上滑动,则速度为负值。

    最后,在不需要使用的时候,记得释放内存,避免内存泄漏:

    vt.clear();
    vt.recycle();
    
  2. GestureDetector

    手势检测器,用于帮助检测用户单击、滑动、长按、双击等行为,要使用它也很简单,先实例化一个:

    GestureDetector gd = new GestureDetector(Context, OnGestureListener)
    

    当然它还有许多构造方法,这里就不一一展开了。

    此外,如果有长按的需求,需要先调用一下允许长按:

    gd.setIsLongpressEnabled(true);
    

    然后再 View 的 onTouchEvent 方法中将事件传给检测器并返回消费结果:

    boolean con = gd.onTouchEvent(event);
    return con;
    

    至此,手势检测器就可以工作了,不过我们需要设置一下监听器:

    还记得刚刚实例化传入的 OnGestureListener,这就是手势的回调,它有以下方法:

    方法名描述
    onDown按下一瞬间,由一个 ACTION_DOWN 触发
    onShowPress手指轻触屏幕,未松开或拖动,ACTION_DOWN 之后一段时间(通常很短)无其他事件时触发。
    onSingleTapUp手指按下后松开,单击行为,由 ACTION_DOWN 与 一个 ACTION_UP 触发
    onScroll按下后滑动,由一个 ACTION_DOWN 与 多个 ACTION_MOVE 触发
    onLongPress长按
    onFling快速滑动,按下,快速滑动后松开,注意要快速滑动

    再对应的事件发生时候监听器的对应回调会启动,除此之外,还有一个方法可以指定双击监听器:

    gd.setOnDoubleTapListener(OnDoubleTapListener)
    

    这里 OnDoubleTapListener 与之前的 OnGestureListener 是可以共同存在的,而 OnDoubleTapListener 中有如下方法:

    方法名描述
    onDoubleTap双击,两次连续单击组成,调用之前 onSimgleTapUp 会调用两次
    onSingleTapConfirmed严格单击,当单击之后一段时间没有再次单击(双击)后才会触发
    onDoubleTapEvent表示发生了双击行为,双击期间,ACTION_DOWN 、ACTION_MOVE 和 ACTION_UP 都会触发
  3. Scroller

    弹性滑动对象,将在 3.3 做介绍,这里就不详解了。

3.2 View 的滑动

再 Android 设备上,滑动时标配,基本上大部分场景都有滑动的身影,因此,掌握滑动的方法时实现绚丽的自定义 View 的基础。有三种方式可以实现 View 的滑动,现在来一一介绍:

3.2.1 使用 scrollTo/scrollBy

View 中有两个属性 scrollX 与 scrollY,这两个属性不会改变 View 的当前位置,但是却会改变 View 中内容的位置。也就是说,在 View 定位之后,其位置参数已经确定,此时 View 的区域已经确定。然后就会开始绘制内容,而 scrollX 与 scrollY 就是在绘制内容时候产生的偏移。这里的偏移方向与坐标轴方向相反,当scrollX 为 100 时,代表此时 View 的左边缘在 View 的内容左边缘右边 100 个像素。

这两个方法都可以改变 View 的 scrollX 与 scrollY 的值,只不过 scrollTo(int x, int y) 传入的 x 和 y 会直接赋值给 scrollX 与 scrollY,而 scrollBy(int x, int y) 传入的值会在原先 scrollX 的基础上加上 x(y 也一样)。

例如以下场景:

  • 当我们调用 scrollTo(0, 0) 的时候,表示将内容拉回起始点。
  • 当我们调用 scrollBy(100, 100) 的时候,表示将内容往左上滑动,两个方向各滑动 100 个像素点。

注意:scrollTo 和 scrollBy 不会改变 View 的位置,当内容滑动到 View 边缘的时候,超出部分将不会绘制,直接消失。

3.2.2 使用动画

使用动画也可以使 View 滑动,这里介绍两种动画,View 动画(补间动画)和属性动画:

关于动画会在第七章进行详解,这里简单提一下。

  • View 动画是 Android 很早就提供的比较原始的动画,它支持 平移 缩放 旋转 和 渐变 四种基本效果及其组合。不过需要注意的是,这种动画只是针对 View 的虚影,也就是 并不会真正改变 View 的位置,当我们将一个按钮使用 View 的动画平移后,新的位置无法响应点击事件,而原来的位置依然可以相应。
  • 属性动画严格来说并不能算动画,它的主要思路是在一段时间内,将 View 的某个属性以某种顺序不断地改变并刷新。因此,如果想要用属性动画实现平移效果,我们可以改变 View 的 translationX 和 translationY 属性来达到目的(之前的位置参数中有讲到)。这种动画的好处是其是确确实实地改变了 View 的属性,也就是 View 的位置确实会发生改变,在新的位置也可以工作。同时,属性动画也抽象出了插值器估值器等功能,可以实现各种各样的效果。不过属性动画是在 安卓3.0 版本之后才加入的,如果需要支持更低版本,需要用到兼容库。(实际上, translationX 和 translationY 属性 也是 安卓3.0 加入的)

3.2.3 改变布局参数

严格来说之前的属性动画也是属于这个这分支,但那个是通过 属性动画来改变 View 自己的属性,而这里是改变 View 在其父布局中的参数。也就是直接改变 LayoutParams 的相关参数来达到滑动的目的,比如左外边距等。当然这里也可以使用 属性动画 工具来实现平滑移动。

3.3 弹性滑动

这一小节将介绍如何实现平滑滑动,其实本质上都是在一次大滑动直接加入一点小滑动,打个时间差,让视觉上看起来像是慢慢滑过去。

这里书上讲的比较乱,这里我做了一下整理:

3.3.1 使用 Scroller 和 scrollTo

首先我们先来看看第一种,以下就是常规用法了:

// DiyView.java
public class SmoothScrollView extends View{
    // 省略构造方法
    
    private Scroller mScroller = new Scroller(mContext);
    
    public void smoothScrollTo(int x, int y){
        int dx = x-getScrollX();
        int dy = y-getScrollY();
        // 最后一个参数为滑动需要的时间,以毫秒为单位
        mScroller.startScroll(getScrollX(), getScrollY(), dx, dy, 1000);
        invalidate();
    }
    
    @Override
    public void computeScroll(){
        if(mScroller.computeScrollOffset()){
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            postInvalidate();
        }
            
    }
}

可以看到还是很简单的,这里主要讲解一下 computeScroll 方法,这个方法会在 View 重绘时在 onDraw 中调用,而我们在这里判断一下 mScroller 是否标记需要滑动,然后调用以下 scrollTo 方法,在重回一次。因此当我们调用 mScroller.startScroll 时,mScroller 就会标记为需要滑动的状态,并且之后我们会重绘一次,然后 View 就会不断重绘,不断调用 ScrollTo 方法,直到滑动完成,而滑动标记,滑动的值的计算则通过 Scroller 完成。

3.3.2 使用 ObjectAnimator 改变 translationX 或 translationY

来看看第二种,第二种就非常简单了,ObjectAnimation 就是属性动画,其内部实现了一个 ValueAnimator,并且在值更新的时候自动设置 View 的相关属性,因此我们可以直接时候它来实现弹性滑动:

ObjectAnimator.ofFloat(targetView, "translationX", 0, 100).setDuration(1000).start();

当调用 start 之后,ObjectAnimator 会在 1000 毫秒内将 targetView 的 translationX 属性从 0 逐步变为 100,如果需要多个 ObjectAnimator 同时,则可以使用 AnimatorSet,这里不做详解,在之后将会提到。

3.3.3 使用 ValueAnimator 和 scrollTo

来看看最后一种,使用 ValueAnimator,我们刚刚讲到 ObjectAnimation 内部实现了一个 ValueAnimator,其实 ValueAnimator 是一个在特定时间内更新一些值的工具,这么说有点抽象,来看代码:

ValueAnimator animator = ValueAnimator.ofFloat(0F, 100F).setDuration(1000);
animator.addUpdateListener(new AnimatorUpdateListener(){
    @Override
    public void onAnimationUpdate(ValueAnimator animator){
        float value = (float) animation.getAnimatedValue();
        targetView.scrollTo(value, targetView.getScrollY());
    }
});
animator.start();

调用 start 之后, ValueAnimator 会在 1000 ms 内生成一组 从 0 开始 到 100 的数字,并在更新数字的时候调用 onAnimationUpdate 。而我们将该数字 传入 scrollTo 即可实现弹性滑动。这里可以看出 ValueAnimator 和 ObjectAnimation 的关系,因为 ObjectAnimation 就是内部实现了 ValueAnimator,并在数据更新时调用我们指定的 View 的设置指定属性的方法而已。

3.3.4 手动实现

其实抛开一切动画的库,我们可以自己实现弹性滑动,例如我们可以新建一个 Handler 然后不断调用 PostDelay 来实现,直到滑动到想要的位置,这里比较灵活也比较好理解,就不做详解。

3.4 View 的事件分发机制

这部分是重点,也是难点,许多开发者都会觉得困难,因此我们更要好好理解,掌握。这里也会花较大篇幅去讲解:

3.4.1 点击事件传递规则

这里书中讲解得比较繁琐,这里重新梳理了一下:

首先,事件是有序列的,当手指按下屏幕,此时会产生一个 ACTION_DOWN 的事件,而手指在若干操作后松开,则会产生一个 ACTION_UP 事件。这个过程中 ACTION_DOWN 为序列的第一个事件,ACTION_UP 为序列的最后一个事件。

当一个 ACTION_DOWN 事件产生,系统会在 各种 View 中传递该事件,当某个 View 决定处理该事件时,则同个序列之后的所有事件都会直接交给该 View 处理(指的是 View 的默认实现,如果重写了相关方法可以改变该规则)。

首先要先明白点击事件产生后的入口,首先 Android 中的布局是有以下层次构成的:

Activity -> Windows -> RootLayout(ViewGroup)

然后 RootLayout 就是最初的 View 架构,最终形成一课树。

点击事件的传递也是按照这个顺序,首先是 Activity,然后是 Windows,然后 WIndows 在传递给 RootLayout,然后就进入 View 的层级了。

其中 事件 的传递与三个方法息息相关:

  • public boolean dispatchTouchEvent(MotionEvent ev)

    这是 点击事件 在 View 中的入口,只要一个事件传递到该 View ,则该方法会被首先调用,同时其他方法也由该方法 间接调用,返回值为是否消费了该事件。

  • public boolean onInterceptTouchEvent(MotionEvent ev)

    在 ViewGroup 中才有,用来判断是否拦截该事件,返回值为是否拦截。该方法由 dispatchTouchEvent 调用,返回值为是否拦截。

  • public boolean onTouchEvent(MotionEvent ev)

    在 dispatchTouchEvent 中调用,用来处理该事件,返回值为是否消费了该事件。

以下是,对于 ACTION_DOWN 事件的,ViewGroup 中 dispatchTouchEvent 的 默认实现 的伪代码:

public boolean dispatchTouchEvent(MotionEvent ev){
    if(onInterceptTouchEvent(ev) || !child.dispatchTouchEvent(ev)){
        if(mOnTouchListener != null && mOnTouchListener.onTouch(ev)){
            return true;
        }
        if(onTouchEvent(ev)){ 
            return true;
        }
        return false;
    }
    return true;
}

可以看到,会先调用 onInterceptTouchEvent(ev) 判断是否拦截,如果拦截则直接由自己处理(if 分支),如果没拦截则短路或会将事件传递给 child,这里省去了根据点击位置判断下一级 View 的过程。如果 child 消费了该事件,则直接返回 true,如果 child 没消费,则还是交给自己处理。

而事件的处理有三种方式,优先级由高到低分别为 onTouchListener,onTouchEvent 和 onClickListener。三种方式都没处理则返回 false。

注意,这里只分析 ACTION_DOWN 的事件传递,并且是该方法的默认实现。如果是其他事件则会按照刚刚的规则直接传递到目标 View 中,在下一节源码分析中会详细讲到。

规则其实大概讲完了,这里简单整理一下(有一些额外的规则整理):

  1. 一个事件序列以 ACTION_DOWN 开始,以 ACTION_UP 结束,中间由若干个 ACTION_MOVE
  2. 正常情况下(View 的默认实现),一个序列的事件只能由同一个 View 拦截且消耗
  3. 如果某个 ViewGroup 的 onInterceptTouchEvent(ev) 返回 true,但是它自身无法消费该事件,那么该事件还是会交给它 父布局处理(从伪代码中可以看出,如果拦截了但是没消费依然返回 false)
  4. 如果某个 View 消费了 ACTION_DOWN ,那么之后的事件都会交给它处理,在之后的事件中,如果这个 View 没有消费该事件,则不会影响该序列之后的事件,同时没消费的事件会传递给 Activity 的 onTouchEvent 处理。
  5. ViewGroup 的 onInterceptTouchEvent(ev) 方法默认 返回 false,即不拦截
  6. 只要 View 的 clickable 、longClickable、contextClickable 三者其中一个为 true,则 该 View 的 onTouchEvent 会返回 true (默认实现)
  7. View 的 enable 属性不会影响 onTouchEvent 的结果(默认实现)
  8. onClick 会发生的前提是 可点击标记为 true,同时收到了 DOWN 和 UP 事件。
  9. 除了 ACTION_DOWN 事件之外的事件在传递到目标 View 时也会经过 父布局,此时父布局的 onInterceptTouchEvent 依然可以强制拦截,子 View 可以调用 requestDisallowInterceptTouchEvent 来禁止父布局拦截本应由自己消费的 View(此时父布局的 onInterceptTouchEvent 将不会调用)。但 ACTION_DOWN 除外,因为 ACTION_DOWN 事件传递时还没有决定由谁消费,因此 requestDisallowInterceptTouchEvent 无法禁止父布局拦截。

3.4.2 事件分发源码分析

以上规则都是默认实现下的规则,如果你重写了相关方法,那么规则可能会被改变。因此为了更好的理解点击事件的分发,我们需要进行源码追踪。开始把:我分析的源码 sdk 版本为 30,而书中的是 20 ,虽然大体类似但细节可能会有冲突。

首先,一个 MotionEvent 最初会来到 Activity 并调用它的 dispatchTouchEvent 方法,先来看看:

// Activity#dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction(); // 默认为空实现,可以重写
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        // 将事件传递给 window,并且如果被消费了则直接返回
        return true;
    }
    // 如果没被消费则调用 onTouchEvent 方法
    return onTouchEvent(ev);
}

可以看到事件会被传递到 window,window 是个抽象类:

**
 * Abstract base class for a top-level window look and behavior policy.  An
 * instance of this class should be used as the top-level view added to the
 * window manager. It provides standard UI policies such as a background, title
 * area, default key processing, etc.
 *
 * <p>The only existing implementation of this abstract class is
 * android.view.PhoneWindow, which you should instantiate when needing a
 * Window.
 */
public abstract class Window {

同个注释可以看出它可以控制顶层 View 的外观和行为。同时它是一个抽象类,它的一个唯一实现类在 android.view.PhoneWindow 中 (不过最终我在 com.android.internal.policy 找到这个类,与注解的包名不同)

// PhoneWindow#superDispatchTouchEvent
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}

可以看到最终是调用了 mDecor 的方法

// PhoenWindow.mDecor

// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;

可以看出至此事件就来到 View 架构中了。这个 DecorView 是用于 承接 我们在 Activity 中调用 setContentView 设置的 View 的。它是 FrameLayout 的子类。

// DecorView#superDispatcTouchEvent
public boolean superDispatchTouchEvent(MotionEvent event) {
    return super.dispatchTouchEvent(event);
}

可以看到我们最终调用了熟悉的 dispatchTouchEvent。至此事件就传递到 View 层了。而因为事件传递时从根出发往下传递的,因此一般都是先传递到 ViewGroup 在 到 View,因此我们来到 ViewGroup ,ViewGroup 的 disaptchTouchEvent 代码比较长,这里来分一下段:

// ViewGroup#disaptchTouchEvent tip1
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
    }// 目标决定器
    
    if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
        ev.setTargetAccessibilityFocus(false);
    }// 焦点的相关操作
    
    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)){
        // tip 2
    }
    
    if (!handled && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
        }
        return handled;
    
}

这里开头和结尾的一些操作暂时不在我们考虑范围,先暂时隐藏,因此逻辑简化为:

注意,在之后的源码中,有关于 mInputEventConsistencyVerifier 的操作都会省去,因为它是一个 debug 时候的事件管理器,不在我们考虑的范围。

// ViewGroup#disaptchTouchEvent simple tip1
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)){
        // tip 2
    }
    return handled;
}

先来看看 onFilterTouchEventForSecurity:

// View#onFilterTouchEventForSecurity
/**
     * Filter the touch event to apply security policies.
     *
     * @param event The motion event to be filtered.
     * @return True if the event should be dispatched, false if the event should be dropped.
     *
     * @see #getFilterTouchesWhenObscured
     */
public boolean onFilterTouchEventForSecurity(MotionEvent event) {
    //noinspection RedundantIfStatement
    if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0
        && (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
        // Window is obscured, drop this touch.
        return false;
    }
    return true;
}

这是用于过滤事件的方法,某些事件因为不安全会被过滤掉,这里和事件传递无关也暂时不讲解。因此实际逻辑就放到了 tip2 部分,这部分也比较长,这里分段讲解:

final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;

if (actionMasked == MotionEvent.ACTION_DOWN) { // 如果是按下
	cancelAndClearTouchTargets(ev);// 清空当前处理的 View
	resetTouchState();//重置点击状态
}

这里可以看到针对 ACTION_DOWN 的事件做了一些初始化,表面新的事件序列开始。cancelAndClearTouchTargets 方法最终会调用 clearTouchTargets 方法,会将一个成员变量 mFirstTouchTarget 设置为 null,这个变量是 TouchTarget 类型,这个变量中含有当前正在处理当前事件队列的 View 的相关信息。因此这里就是将正在处理的 View 置空,表面新的序列开始。

// Check for interception.
final boolean intercepted; // 标记是否拦截
if (actionMasked == MotionEvent.ACTION_DOWN
    || mFirstTouchTarget != null) { 
    // 当事件为 ACTION_DOWN 或者其他 View 正在处理的事件
    
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; 
    // 这个标记可由子 View 调用 requestDisallowInterceptTouchEvent 设置
    
    if (!disallowIntercept) { // 如果子 View 没有禁止调用
        
        intercepted = onInterceptTouchEvent(ev); 
        // 调用 onInterceptTouchEvent 看是否拦截
        
        ev.setAction(action); // 重置一下 action (防止 在 onInterceptTouchEvent 中修改 )
    } else { // 子 View 禁止拦截
        intercepted = false;
    }
} else { 
    // 会执行到这里当且仅当事件不是 MOTION_DOWN 且 mFirstTouchTarget 为空,表示这是由这个 ViewGroup 本身处理的事件
    intercepted = true;
}

这里主要是是否拦截的判断,不过要注意:

如果当前事件是 ACTION_DOWN,则 FLAG_DISALLOW_INTERCEPT 的标记会在 上面 resetTouchState() 中重置,因此 子 View 无法通过 requestDisallowInterceptTouchEvent 来阻止父布局拦截 ACTION_DOWN 的事件

在 onInterceptTouchEvent 中改变的 MotionEvent 的 ACTION,不会产生效果,因为在之后会复原。

if (intercepted || mFirstTouchTarget != null) {
    ev.setTargetAccessibilityFocus(false);
}// 焦点相关设置

// 判断事件是否被关闭
final boolean canceled = resetCancelNextUpFlag(this)
    || actionMasked == MotionEvent.ACTION_CANCEL;

// 一些标记判断
// Update list of touch targets for pointer down, if needed.
final boolean isMouseEvent = ev.getSource() == InputDevice.SOURCE_MOUSE;
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0
    && !isMouseEvent;

这段代主要是用于设置焦点和判断事件是否被关闭,当我们的 View 决定要处理该事件后,如果事件被父布局强行拦截并处理了,则子 View 会收到一个 ACTION_CANCEL 事件。同时,在一个 事件队列结束后,负责处理该事件的 View 也会收到一个 CANCEL 事件。

继续看:

TouchTarget newTouchTarget = null; // 新的处理事件的 View
boolean alreadyDispatchedToNewTouchTarget = false; // 是否已经分发给新的 View 处理
if (!canceled && !intercepted) {
    // If the event is targeting accessibility focus we give it to the
    // view that has accessibility focus and if it does not handle it
    // we clear the flag and dispatch the event to all children as usual.
    // We are looking up the accessibility focused host to avoid keeping
    // state since these events are very rare.
    View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
        ? findChildWithAccessibilityFocus() : null; // 这个变量后面不会再用到,这里先暂时不看
    
    // tip 3
}

可以看到,如果不拦截,不是 CANCEL 事件,则要进行分发了,分发的具体逻辑在上述代码中 tip 3 部分。

接下来看 tip 3 部分:

// tip 3
if (actionMasked == MotionEvent.ACTION_DOWN
    || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { // 新事件序列
    final int actionIndex = ev.getActionIndex();
    final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
        : TouchTarget.ALL_POINTER_IDS; // 多指支持
    
    removePointersFromTouchTargets(idBitsToAssign);

    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);
        
        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 (!child.canReceivePointerEvents()
                || !isTransformedTouchPointInView(x, y, child, null)) {
                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;
            }

            // The accessibility focus didn't handle the event, so clear
            // the flag and do a normal dispatch to all children.
            ev.setTargetAccessibilityFocus(false);
        }
        if (preorderedList != null) preorderedList.clear();
    }

    if (newTouchTarget == null && mFirstTouchTarget != null) {
        // Did not find a child to receive the event.
        // Assign the pointer to the least recently added target.
        newTouchTarget = mFirstTouchTarget;
        while (newTouchTarget.next != null) {
            newTouchTarget = newTouchTarget.next;
        }
        newTouchTarget.pointerIdBits |= idBitsToAssign;
    }
}

逻辑还是比较清晰的,遍历一下 chirldren,然后重点看这里:

if (!child.canReceivePointerEvents() // 是否能接收事件
    || !isTransformedTouchPointInView(x, y, child, null)) { // 点击位置是否在该孩子处
    continue;
}

newTouchTarget = getTouchTarget(child); // 先看看该 View 是否已经作为处理事件的 View
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; // 该 View 已经在处理事件,这里加入一个指头标记(第二根手指按下)
    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;
}

其中 dispatchTransformedTouchEvent 方法会调用子 View 的 DispatchTouchEvent 方法,并且如果该方法返回 true,即孩子消费了这个 DOWN 事件,则讲其记录为这一个事件序列的处理 View,并跳出循环。否则会继续循环。

至此,tip3 部分就完了。让我们继续看

这里的部分已经跳出了 tip3,因此所有的事件都会走到这里,之前做了判断 只有事件序列的第一个事件才能走:

// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
    // No touch targets so treat this as an ordinary view.
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
                                            TouchTarget.ALL_POINTER_IDS);
} else {
    // Dispatch to touch targets, excluding the new touch target if we already
    // dispatched to it.  Cancel touch targets if necessary.
    TouchTarget predecessor = null;
    TouchTarget target = mFirstTouchTarget;
    while (target != null) {
        final TouchTarget next = target.next;
        if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
            handled = true;
        } else {
            final boolean cancelChild = resetCancelNextUpFlag(target.child)
                || intercepted;
            if (dispatchTransformedTouchEvent(ev, cancelChild,
                                              target.child, target.pointerIdBits)) {
                handled = true;
            }
            if (cancelChild) {
                if (predecessor == null) {
                    mFirstTouchTarget = next;
                } else {
                    predecessor.next = next;
                }
                target.recycle();
                target = next;
                continue;
            }
        }
        predecessor = target;
        target = next;
    }
}

可以自己分析下,这里先判断是否有 目标 View,如果没有则调用 dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS) 该方法逻辑较为复杂,具体为根据各种情况将事件分发。

如果有,还是调用 dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits) ,只不过第三个参数为目标 View,该方法会将事件分发给 child (正常情况下)。

以下是 dispatchTransformedTouchEvent 重点代码 (节选):

// ViewGroup#dispatchTransformedTouchEvent
// ……
if (child == null) {
    handled = super.dispatchTouchEvent(transformedEvent);
} else {
    final float offsetX = mScrollX - child.mLeft;
    final float offsetY = mScrollY - child.mTop;
    transformedEvent.offsetLocation(offsetX, offsetY);
    if (! child.hasIdentityMatrix()) {
        transformedEvent.transform(child.getInverseMatrix());
    }

    handled = child.dispatchTouchEvent(transformedEvent);
}
// ……

可以看到最终调用了 dispatchTouchEvent 方法,根据是否有目标而调用特定的方法。如果没有目标,则 ViewGroup会自己处理该事件,调用 super.dispatchTouchEvent,因为 ViewGroup 是继承于 View,因此我们来到的 View 的 dispatchTouchEvent 方法。

至此,我们分析了 dispatchTouchEvent 的部分源码,相信对事件的分发有了大概的了解。接下来我们来到 View 的 dispatchTouchEvent 方法:

// View#dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
    if (event.isTargetAccessibilityFocus()) {
        if (!isAccessibilityFocusedViewOrHost()) {
            return false;
        }
        event.setTargetAccessibilityFocus(false);
    }
    boolean result = false;

    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onTouchEvent(event, 0);
    }

    final int actionMasked = event.getActionMasked();
    if (actionMasked == MotionEvent.ACTION_DOWN) {
        stopNestedScroll();
    }

    if (onFilterTouchEventForSecurity(event)) {
        // tip
    }

    if (!result && mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
    }
    if (actionMasked == MotionEvent.ACTION_UP ||
        actionMasked == MotionEvent.ACTION_CANCEL ||
        (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
        stopNestedScroll();
    }

    return result;
}

可以看到比较简单,因为 View 不存在子 View,它只能自己处理事件,因此该方法也比较简单,这里前后都不做讲解,重点看 tip 部分:

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;
}

可以看到,它会先调用 mOnTouchListener.onTouch 方法,而这个变量可以通过 setTouchListener 来设置。如果在这里就将时间消费,则不会走到 onTouchEvent,否则则尝试走 onTouchEvent。

关于 View 的 onTouchEvent 的默认实现,因为内容较多这里不详细给出,不过要记得几点:

  • 只要 View 的 Clickable 和 LongClickable 的 flags 中有一个为 true,则 onTouchEvent 会返回 true
  • 当 ACTION_UP 发生时,会触发 performClick 方法,如果有设置 OnClickListener ,则会调用

3.5 View 的滑动冲突

3.5.1 常见滑动冲突情景

简单来说有三种情景:

  • 场景 1 —— 外部滑动方向与内部滑动方向不一致
  • 场景 2 —— 外部滑动方向与内部滑动方向一致
  • 场景 3 —— 上面两种情况的嵌套

对于场景一,一种典型场景是 ViewPager 中 嵌套 RecyclerView,此时外部滑动方向是左右,而内部是上下。不过对于 ViewPager,它内部帮我们解决了滑动冲突。如果我们使用的是横向的 HorizontalScrollView,则需要处理。

对于场景二,一种典型场景是 RecyclerView 的嵌套,而外层内层滑动方向一致,此时会产生滑动冲突

对于场景三就是套娃,比如 ViewPager 套 两层 RecyclerView 等。需要我们分别处理各两层之间的冲突,最终解决整体冲突

3.5.2 滑动冲突处理规则

滑动冲突的规则其实还是比较好理解的,这里根据上面三个场景一个一个来看:

  • 对于场景一,我们需要判断用户是往什么方向滑动,如果是水平滑动,则将事件交给左右滑动的 View 处理,如果是竖直滑动反之。这里问题在于如何判断是左右滑动还是上下滑动,这里有很多方法,我们可以取一段时间然后根据滑动的位移差来判断,也可以用滑动速度来判断,也可以终点起点连线与水平方向夹角来判断。
  • 对于场景二,我们没办法从技术的角度判断了。我们只能从业务角度来判断,比如对于上下滑动嵌套,我们可以根据内部的 View 是否滑动到顶部来判断,当内部的 View 滑动到顶部,则该滑动事件需要交给父布局。
  • 对于场景三,则更复杂了,我们需要综合判断各种业务。下一节将会通过例子来演示解决方案。

3.5.3 滑动冲突解决方式

解决滑动冲突的核心在于,将事件分发给正确的 View 来处理,处理方法很多,只要能达到目的即可,但一般说来,有两种方式,一是外部拦截法和二是内部拦截法。第一种为事件先交给父布局,父布局判断该事件是否是自己想要的,如果是则自己处理如果不是则传递给孩子。第二种是直接交给子布局,子布局判断该事件是否是自己想要的,如果不是则返回自己未能处理。

外部拦截法

主要为重写父容器的 onInterceptTouchEvent 方法,如果该事件为需要的话则直接拦截,主要代码如下,其中 need 方法为返回是否需要该事件:

public boolean onInterceptTouchEvent(MotionEvent envet){
    boolean intercepted = false;
    int x = (int) event.getX();
    int y = (int) event.getY();
    switch(event.getjAction()){
        case MotionEvent.ACTION_DOWN:
            intercepted = false;
            break;
        case MotionEvent.ACTION_MOVE:
            // 如果父容器需要该事件
            if(need(mLastX, mLastY, x, y)){ 
                intercepted = true;
            }else{
                intercepted = false;
            }
            break;
        case MotionEvent.ACTION_UP:
            intercepted = false;
            break;
    }
    mLastX = x;
    mLastY = y;
    return intercepted;
}
内部拦截法

这种方法与安卓原本的分发机制不一致,需要配合 requestDisallowInterceptTouchEvent 方法才能工作,主要为重写子控件的 dispatchTouchEvent 方法,具体代码如下:

public boolean dispatchTouchEvent(MotionEvent event){
    int x = (int) event.getX();
    int y = (int) event.getY();
    
    switch(event.getAction()){
            case MotionEvent.ACTION_DOWN:
            parent.requestDisllowInterceptTouchEvent(true);
            break;
        case MotionEvent.ACTION_MOVE:
            // 如果父容器需要该事件
            if(need(mLastX, mLastY, x, y)){ 
                parent.requestDisllowInterceptTouchEvent(false);
            }
            break;
        case MotionEvent.ACTION_UP:
            intercepted = false;
            break;
    }
    mLastX = x;
    mLastY = y;
    return super.dispatchTouchEvent(event);
}

同时注意,ACTION_DOWN 事件不受 FLAG_DISALLOW_INTERCEPT 标记的控制,因此父容器不能拦截 ACTION_DOWN 事件,否则所有事件都无法传到孩子,则 父布局的 onInterceptTouchEvent 方法中对于 ACTION_DOWN 事件必须返回 false 。