View 的事件体系

全面了解事件分发

Posted by Song on March 8, 2021

View 基础知识

View 的位置参数

View 的位置由其四个顶点决定,分别对应 View 四个属性:left、right、top、bottom,其中 top 是左上角纵坐标,left 是左上角横坐标,right 是右下角横坐标,bottom 是右下角纵坐标。注意这些坐标都是相对 View 父容器来说的,因此是一种相对坐标。

View 的宽高和坐标关系

1
2
width = right - left
height = bottom -top

在 View 中获取方式分别为

1
2
3
4
5
6
7
8
9
10
11
12
public final int getLeft() {
    return mLeft;
}
public final int getRight() {
    return mRight;
}
public final int getTop() {
    return mTop;
Y
public final int getBottom() {
    return mBottom;
}

由于 Android 3.0 引入了属性动画,故 View 增加了几个额外的参数,x、y、translationX、translationY。其中 x、y 是 View 相对父容器左上角坐标,ranslationX、translationY 是 View 左上角相对父容器的偏移量。translationX、translationY 默认值为 0,x、y 默认值和 left、top 相同,且这四个参数也有 set/get 方法。这是个参数换算关系如下:

1
2
x = left + translationX
y = top + translationY

View 在平移过程中,left、top 值并不会改变,改变的只是 x、y、translationX、translationY 这几个参数

MotionEvent

核心的三种事件

1
2
3
ACTION_DOWN:	手指接触屏幕
ACTION_MOVE:	手指在屏幕前移动
ACTION_UP:	手指从屏幕移开

四个方法

1
2
getX/getY相对父容器当前 View 左上角坐标 xy
getRawX/getRawY相对于屏幕左上角 x  y 坐标

TouchSlop

系统说能够识别出的被认为是滑动的最小距离,通过以下方法获取:ViewConfiguration.get(getContext()).getScaledTouchSlop(); 。在不同的设备上这个值不同,可以根据这个值过滤部分滑动操作。

VelocityTracker

速度追踪,包括水平和垂直方向上的速度。

1
2
3
4
5
6
7
VelocityTracker tracker = VelocityTracker.obtain();
tracker.addMovement(event);
tracker.computeCurrentVelocity(1000); // 计算 1s 时间间隔速度
tracker.getXVelocity(); // 横坐标上平均速度
tracker.getYVelocity(); // 纵坐标上平均速度
tracker.clear();
tracker.recycle();

GestureDetector

手势检测,用于辅助检测用户的单击、滑动、长按、双击等行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public class TestGestureDetector implements GestureDetector.OnGestureListener {

    private GestureDetector detector;

    public TestGestureDetector(Context context) {
        detector = new GestureDetector(context, this);
        detector.setIsLongpressEnabled(false); // 解决长按无法拖动问题
    }

    public boolean onTouchEvent(MotionEvent ev){
        return detector.onTouchEvent(ev);
    }

    @Override
    public boolean onDown(MotionEvent e) {
        return false;
    }

    /**
     * 轻按(按下未松开)
     */
    @Override
    public void onShowPress(MotionEvent e) {
    }

    /**
     * 单击
     */
    @Override
    public boolean onSingleTapUp(MotionEvent e) {
        return false;
    }

    /**
     * 拖动未松开
     */
    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        return false;
    }

    /**
     * 长按
     */
    @Override
    public void onLongPress(MotionEvent e) {
    }

    /**
     * 快速滑动后松开
     */
    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        return false;
    }
}

接管目标 View 的 onTouchListener 方法,在待监听的目标 View 返回 GestureDetector.onTouchEvent(ev)

如果只是监听滑动,建议自己在 onTouchEvent 实现,如果要监听双击,建议使用OnDoubleTapListener

Scroller

实现 View 弹性滑动,我们知道 View 的 scrollTo 和 scrollBy 方法是瞬间完成的,故可以使用 Scroller 和 computeScroll 方法完成弹性滑动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Scroller scroller = new Scroller(getContext());

private void smoothScrollTo(int destX, int destY) {
    int scrollX = getScrollX();
    int delta = destX - scrollX;
    // 1000ms 内划向 destX
    scroller.startScroll(scrollX, 0, delta, 0, 1000);
    invalidate();
}

@Override
public void computeScroll() {
		super.computeScroll();
    if (scroller.computeScrollOffset()) {
        scrollTo(scroller.getCurrX(), scroller.getCurrY());
        postInvalidate();
    }
}

View 的滑动

使用 scrollTo/scrollBy

  • scrollTo 是基于当前位置的绝对滑动,而 scrollBy 是基于当前位置的相对滑动,其中绝对位置是基于当前 View 容器的左上角坐标
  • 核心的 mScrollX 和 mScrollY 参数,mScrollX 为 View 左边缘和 View 内容左边缘在水平方向之间的距离,mScrollY 为 View 上边缘和 View 内容上边缘在水平方向之间的距离
  • scrollTo/scrollBy 只能使 View 的内容再 View 中滑动,并不能改变 View 的位置
  • 当 View 内容相对 View 从左向右滑动时候,mScrollX 为负值,反正为正值
    1
    2
    3
    
    public void scrollBy(int x, int y) {
      scrollTo(mScrollX + x, mScrollY + y);
    }
    

使用动画

  • View 动画本质是对 View 影像作移动,动画完成后会瞬间回到原始位置,若 fillAfter=true 除外,但是在新位置无法操作 View,其真实位置还在原位置
  • 属性动画才是真实操作 View 在父容器的位置,并且改变其属性

改变布局参数

通过 getLayoutParams 获取布局对象,然后 requestLayout 刷新布局,如悬浮窗。

各种滑动方式的对比

  • scrollTo/scrollBy:操作简单,适合对 View 内容滑动
  • 动画:操作简单,主要适用没有交互的 View 和实现复杂的动画效果
  • 改变布局参数:操作稍微麻烦,适用于有交互的 View

弹性滑动

使用 Scroller

本质是通过 onDraw 方法间隔 10ms 被重复调用特点

通过动画

使用延时策略

通过 Handler.postDelayed 操作

View 事件分发机制

点击事件传递规则

事件分发机制示例图

其中 super 表示控件的默认实现

红色的箭头代表ACTION_DOWN 事件的流向 蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向

事件消耗1

我们在ViewGroup 1 的onTouchEvent 返回true消费这次事件

事件消耗2

我们在View的dispatchTouchEvent 返回false并且ViewGroup 1 的onTouchEvent 返回true消费这次事件

事件消耗3

我们在ViewGroup1 的dispatchTouchEvent 方法返回true消费这次事件

伪代码

1
2
3
4
5
6
7
8
9
public boolean dispatchTouchEvent(MotionEvent ev) {
		boolean consume = false;
    if (onInterceptTouchEvent(ev)) {
        consume = onTouchEvent(ev);
    }else {
    		reture child.dispatchTouchEvent(ev);
    }
    return consume;
}
  1. dispatchTouchEvent:事件分发,返回结果受当前 View 的 onTouchEvent 和下级 View 的 dispatchTouchEvent 影响,表示消费当前事件。
  2. onInterceptTouchEvent:事件拦截,如果当前 View 拦截了某个事件,那么同一事件序列中,此方法不会被再次调用,返回结果表示是否拦截当前事件。(针对所有事件)
  3. onTouchEvent:事件处理,dispatchTouchEvent 方法中调用。在返回结果表示是否消耗当前事件,如果不消耗,则在同一事件序列中,当前 View 无法再次接收到事件。(针对 ACTION_DOWN)
  4. 当 View 处理事件时,若设置了 OnTouchListener,那么 OnTouchListener 中的 onTouch 会被调用。若 onTouch 返回 true,onTouchEvent 将不会被调用,否则会被调用。总结就是 OnTouchListener 优先级比 onTouchEvent 高。因为 OnClickListener 监听是在 onTouchEvent 中被调用,所以 OnClickListener 优先级最低,处在事件传递的尾端。
  5. 正常情况下一个事件序列只能被一个 View 拦截且消耗。当一旦决定拦截,那么这个事件序列都只能由它处理,并且 onInterceptTouchEvent 方法不会再被调用。(当不拦截 ACTION_DOWN,而从拦截某次 ACTION_MOVE,则后续事件同理)
  6. 某个 View 一旦开始处理事件,如果它不消耗 ACTION_DOWN 事件,那么同一序列中的其他事件都不会再交给它处理,并且事件将重新交给它的父元素处理,即 onTouchEvent。
  7. 如果 VIew 不消耗除 ACTION_DOWN 以外的其他事件,那么这个点击事件会消失,且父元素的 onTouchEvent 也不会被调用,但是当前 View 可以收到后续其他事件,最终交给 Activity 处理。
  8. ViewGroup 默认不拦截所有事件,View 没有 onInterceptTouchEvent 方法。
  9. View 的 onTouchEvent 默认都会消耗事件,除非其不可点击(clickable 和 longclickable 都为 false),比如 TextView 的 clickable 默认属性为 false。View 的 enable 属性不影响 onTouchEvent 返回值,那么为 disable 状态,只要 clickable 和 longclickable 一个为 true,那么 onTouchEvent 返回 true。
  10. 可以通过 requestDisallowInterceptTouchEvent() 干预父元素事件分发过程,但是 ACTION_DOWN 事件除外,因为 ACTION_DOWN 事件发生 requestDisallowInterceptTouchEvent() 对应的 TAG 每次会被重置。

事件分发源码解析

Activity 的 dispatchTouchEvent 先交给 window(PhoneWindow)处理,PhoneWindow 给绑定的DecorView 处理,DecorView 继承ViewGroup,故 ViewGroup 直接处理,所以处理事件为 Activity、ViewGroup 和 View

1
2
3
4
5
6
7
8
9
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
       return true;
    }
    return onTouchEvent(ev);
}

Activity 的 dispatchTouchEvent 发现,事件都是从 Activity 分发,然后到 Window,到 ViewGroup,最后到 View

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

Window 是一个抽象类,其唯一的一个实现类为 PhoneWindow,这里可以看见,最终事件由 Window 分发给了 mDecor。

1
2
3
4
5
6
7
@Override
public final View getDecorView() {
    if (mDecor == null || mForceDecorInstall) {
        installDecor();
    }
    return mDecor;
}

而 Window 的 getDecorView 方法可知,mDecor 就是 就是 DecorView,即分发给了 Window 最顶层的 View。且 DecorView 继承自 FrameLayout,事件最终会传递给 View。

我们可以通过 ((ViewGroup) getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0) 获得 Activity 的 setContentView() 的 View,由此可见,事件传递到了顶级 View(DecorView)。

DecorView 对事件的分发过程

事件会分发到 ViewGroup,调用 ViewGroup 的 dispatchTouchEvent,如果 ViewGroup 拦截事件,即 onInterceptTouchEvent 返回为 true,则事件由 ViewGroup 处理。若 ViewGroup 设置了 OnTouchListener,则 onTouch 会被调用,否则 onTouchEvent 被调用(注意:OnTouchListener 会屏蔽 onTouchEvent)。若设置了 OnClickListener,则 onClick 会被调用。若 onInterceptTouchEvent 返回 false,则事件分发给 View 处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
        || mFirstTouchTarget != null) {
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    if (!disallowIntercept) {
        intercepted = onInterceptTouchEvent(ev);
        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;
}
  1. 只有当 ACTION_DOWN 或者 mFirstTouchTarget != null 满足条件时才判断走 ViewGroup 的 onInterceptTouchEvent 方法,同理当非 ACTION_DOWN 事件或者 mFirstTouchTarget == null 时候,拦截标记为 true,且不执行 onInterceptTouchEvent。mFirstTouchTarget == null 在事件未被分发给子 View 或者子 View 未消耗事件才成立,故可以得出,只有当 ACTION_DOWN 事件被子 View 消耗,ACTION_MOVE、ACTION_UP 事件才会分发给子 View。
  2. FLAG_DISALLOW_INTERCEPT 标记位可以被子 View 通过 requestDisallowInterceptTouchEvent 方法控制,若被设置,则 ViewGroup 无法拦截。但是 ACTION_DOWN 事件除外,因为 ACTION_DOWN 事件发生 FLAG_DISALLOW_INTERCEPT 标记位会被重置。
  3. 当面对 ACTION_DOWN 事件时,ViewGroup 总是会调用自己的 onInterceptTouchEvent 询问是否拦截事件。正常情况下,若 ViewGroup 拦截消耗 ACTION_DOWN 事件,后续其 onInterceptTouchEvent 不会被调用。若 ViewGroup 子 View消耗 ACTION_DOWN 事件,onInterceptTouchEvent 还是会被调用。
1
2
3
4
5
6
7
8
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
    // Throw away all previous state when starting a new touch gesture.
    // 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.
    cancelAndClearTouchTargets(ev);
    resetTouchState();
}
  1. 发生 ACTION_DOWN 事件 FLAG_DISALLOW_INTERCEPT 标记被重置
  2. 子 View 的 requestDisallowInterceptTouchEvent 方法并不影响 ViewGroup 对 ACTION_DOWN 事件的处理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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 (!canViewReceivePointerEvents(child)
            || !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;
    }

    // The accessibility focus didn't handle the event, so clear
    // the flag and do a normal dispatch to all children.
    ev.setTargetAccessibilityFocus(false);
}
  1. 当 ViewGroup 决定不拦截事件时,会遍历子 View,判断子元素是否接受到事件,动画播放和点击区域是否坐落在子元素区域为判断依据。
  2. 我们从 child.dispatchTouchEvent 看出来,最终事件分发到子 View。
  3. child.dispatchTouchEvent 返回 true,则 mFirstTouchTarget 被赋值,且跳出 for 循环。
  4. mFirstTouchTarget 为单链表结构,其真正赋值是在内 addTouchTarget 完成
1
2
3
4
5
6
// 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);
} 
  1. 当 mFirstTouchTarget 为 null 时候,事件就给 ViewGroup 的 onTouchEvent 处理
  2. 当 ViewGroup 没有子 View,或者 子 View 没有消耗事件 mFirstTouchTarget 才会为 null

总结 一个事件顺序为,activity 的分发,viewgroup 的分发,viewgroup 的拦截,view 的分发,view 的处理,viewgroup 的处理,activity 的处理。当 view 消耗了所有事件,之后的事件直接分发给view消耗,若 view 只消耗 down,那么事件跳过 viewgroup 的处理,交给 activity 的处理,若 viewgroup 拦截了所有事件,那么事件分发不到 view,直接给 viewgroup 消耗,否则给 activity 消耗。

View 对点击事件的处理

View 的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
    }
}
  1. mOnTouchListener 不为 null 且 dispatchTouchEvent 返回 true,则不会调用 onTouchEvent,可见 dispatchTouchEvent 优先级高于 onTouchEvent
1
2
3
4
5
6
7
8
9
if ((viewFlags & ENABLED_MASK) == DISABLED) {
    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 仍然会消耗点击事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
    switch (action) {
        case MotionEvent.ACTION_UP:
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
            if ((viewFlags & TOOLTIP) == TOOLTIP) {
                handleTooltipUp();
            }
            if (!clickable) {
                removeTapCallback();
                removeLongPressCallback();
                mInContextButtonPress = false;
                mHasPerformedLongPress = false;
                mIgnoreNextUpEvent = false;
                break;
            }
            boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
            if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                // take focus if we don't have it already and we should in
                // touch mode.
                boolean focusTaken = false;
                if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                    focusTaken = requestFocus();
                }

                if (prepressed) {
                    // The button is being released before we actually
                    // showed it as pressed.  Make it show the pressed
                    // state now (before scheduling the click) to ensure
                    // the user sees it.
                    setPressed(true, x, y);
                }

                if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                    // This is a tap, so remove the longpress check
                    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();
                        }
                    }
                }

                if (mUnsetPressedState == null) {
                    mUnsetPressedState = new UnsetPressedState();
                }

                if (prepressed) {
                    postDelayed(mUnsetPressedState,
                            ViewConfiguration.getPressedStateDuration());
                } else if (!post(mUnsetPressedState)) {
                    // If the post failed, unpress right now
                    mUnsetPressedState.run();
                }

                removeTapCallback();
            }
            mIgnoreNextUpEvent = false;
            break;
  1. 只要 View clickable 和 longclickable 任意一个为 true,都会消耗这个事件,即 onTouchEvent 返回 true。
  2. 若 View 设置了 OnClickListener 则 会调用 performClickInternal 里面的 click 方法,即 OnClickListener 优先级最低。
  3. View 的 longclickable 属性默认为 false,clickable 区分具体 View,如 TextView 为 false,Button 为 true。
  4. 通过 setClickable 和 setLongClickable 可以分别改变其 clickable 和 longclickable 属性。且 setOnClickListener 和 setOnLongClickListener 可以分别改变其 clickable 和 longclickable 属性为 true。

View 的滑动冲突

外部拦截法

内部拦截法