Android 控件——View的事件体系

https://rainmonth.github.io/posts/A180504.html

摘要

本文内容有点多,主要介绍View的基础知识、View的事件分发机制、从源码分析View的事件分发机制以及滑动冲出的处理这几方面的知识。

View的基础知识

位置参数

以下四个参数是相对父容器而言的:

  • top,左上角纵坐标,通过getTop()获得
  • left,左上角横坐标,通过getLeft()获得
  • right,右下角横坐标,通过getRight()获得
  • bottom,右下角纵坐标,通过getBottom()获得

Android 3.0之后,新增:

  • x,左上角横坐标
  • y,左上角纵坐标
  • transitionX,左上角相对于父容器的横向偏移量,默认值为0
  • transitionY,左上角相对于父容器的纵向偏移量,默认值为0

所以

  • 宽width= right -left
  • 高height=bottom-top
  • x = left + transitionX
  • y = top + transitionY

View平移的时候,top、left表示的原始的左上角的位置信息,不对改变,测试发生改变的时x、y、transitionX、transitionY这四个参数。

MotionEvent

记录手指、鼠标、触控笔等的移动事件的对象。典型的有一下几种:

  • ACTION_DOWN,手指刚接触屏幕
  • ACTION_MOVE,手指在屏幕上移动
  • ACTION_UP,手指离开屏幕瞬间

正常事件流:

  • 点击屏幕后松开,ACTION_DOWN->ACTION_UP
  • 点击屏幕滑动一会松开,ACTION_DOWN->ACTION_MOVE->…->ACTION_UP

获取点击事件发生的位置:

  • getX和getY,获取相对于当前View左上角的x,y值
  • getRawX和getRawY,获取相对于屏幕左上角的x,y值

TouchSlop

TouchSlop是系统所能识别出的被认为是滑动的最小距离,即如果屏幕上的两次滑动距离小于这个值,系统就会判定你不是在进行滑动操作。

可以利用这个值对滑动的距离做过滤。

VelocityTracer

速度追踪,用于追踪手指在滑动过程中的速度,包括水平和竖直方向的速度。具体使用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
public boolean onTouchEvent(MotionEvent event) {
// 获取VelocityTracker
VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);

// 计算速度
velocityTracker.computeCurrentVelocity(1000);
int xVelocity = (int) velocityTracker.getXVelocity();
int yVelocity = (int) velocityTracker.getYVelocity();

// 回收VelocityTracker
velocityTracker.clear();
velocityTracker.recycle();
return super.onTouchEvent(event);
}

针对上面代码做以下说明:

  • 得到VelocityTracker后,要回收

  • 获取速度之前,先要计算速度,计算速度时要传入间隔时间,速度计算公式如下:

    1
    速度 = (终点位置 - 起点位置) / 时间间隔
  • 速度存在正负,从左往右(从上往下)是得到的x方向速度(y方向速度)为正

GestureDetector

手势检测,用于辅助检测用户的单击、双击、长按、滑动等行为,具体使用方法如下:

  • Activity(或View)实现GestureDetector.OnGestureListenerGestureDetector.OnDoubleTapListener这两个接口;

  • 得到GestureDetector

    1
    2
    3
    4
    mGestureDetector = new GestureDetector(this);
    // 设置之后就不能响应长按事件了
    mGestureDetector.setIsLongpressEnabled(false);
    mGestureDetector.setOnDoubleTapListener(this);
  • 重写Activity(或View)的onTouchEvent,我这里直接交由GestureDetector的onTouchEvent来处理

    1
    2
    3
    4
    5
    @Override
    public boolean onTouchEvent(MotionEvent event) {
    // printVelocityTracker(event);
    return mGestureDetector.onTouchEvent(event);
    }

至于GestureDetector的onGestureListener和onDoubleTabListener中每个方法的具体含义,请参考源码的说明,不做详述。(实际上,GestureDetector的所有方法完全可以参考其onTouchEvent在目标的onTouchEvent方法上来实现)

Scroller

弹性滑动对象,用于实现View的弹性滑动。View的scrollTo/scrollBy方法滑动时,是瞬时完成的,无过渡效果,体验不好。采用Scroller可以让滑动过程在一定的时间间隔内完成。

注意,Scroller本身无法让View弹性滑动,需要和View的computeScroll方法配合才能完成这个功能。

一般使用过程如下:

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

private void smoothScrollTo(int destX, int destY) {
int scrollX = getScrollX();
int delta = destX - scrollX;
// 1000毫秒内滑动到destX
mScroller.startScroll(scrollX, 0, delta, 0, 1000);
invalidate();
}

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

View的滑动

View的滑动,是实现绚丽自定义控件的基础。View的滑动注意可以通过以下几种方式:

  • View本身提供的scrollTo/scrollBy方法来实现滑动;
  • 通过平移动画来实现View的滑动;
  • 通过LayoutParams是的View重新布局来实现滑动;

使用scrollTo/scrollBy

先看看这两个方法的源码:

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
/**
* Set the scrolled position of your view. This will cause a call to
* {@link #onScrollChanged(int, int, int, int)} and the view will be
* invalidated.
* @param x the x position to scroll to
* @param y the y position to scroll to
*/
public void scrollTo(int x, int y) {
if (mScrollX != x || mScrollY != y) {
int oldX = mScrollX;
int oldY = mScrollY;
mScrollX = x;
mScrollY = y;
invalidateParentCaches();
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (!awakenScrollBars()) {
postInvalidateOnAnimation();
}
}
}

/**
* Move the scrolled position of your view. This will cause a call to
* {@link #onScrollChanged(int, int, int, int)} and the view will be
* invalidated.
* @param x the amount of pixels to scroll by horizontally
* @param y the amount of pixels to scroll by vertically
*/
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}

针对上面代码作以下说明:

  • scrollBy内部调用了scrollTo,二者区别:前者加上了mScrollX和mScrollY,是相对滑动(要知道mScrollX和mScrollY是可以为负值),后者是绝对滑动
  • mScrollX和mScrollY分别代表View的内容与View的边界的距离,即:
    • mScrollX = ViewEdgeLeft - ViewContentLeft,大于0表示内容边界左边,从右向左滑动
    • mScrollY = ViewEdgeTop - ViewContentTop,大于0表示内容边界上边,从下向上滑动。
  • scrollBy和scrollTo改变的时View内容的位置而不是View在布局中的位置

使用动画

既可以使用传统的View动画,也可以使用Android 3.0之后的属性动画(3.0之前如果要应用属性动画可以采用nineoldanroids这个开源库实现)。

使用View动画

View动画分四种:平移动画,缩放动画,透明度动画,旋转动画,注意使用动画改变的只是View内容,View正在的位置参数没有改变,这就会导致问题的发生。如我将一个Button向下移动100px,移动过后,假如我设置了点击事件,此时你会发现点击现在的位置无法响应点击事件,而点击原来的地可以,这就是View动画局限的地方。

使用属性动画

针对使用View动画的问题,Android 3.0后可以采用属性动画来解决。属性动画可以改变View的位置。

改变布局参数

使用这种方式完成滑动个人觉得不是很优雅,不建议使用。这种方式是通过实现设置一个占位的View,当需要发生滑动是,改变这个占位View的布局参数,从而达到改变目标View位置的目的。

当然,也可以动态设置View的margin值,得到滑动View的目的。

三种方式对比:

  • scrollTo/scrollBy,原生方法,使用方便,能滑动内容但不能滑动View本身;
  • 动画,使用简单,能实现复杂的动画,Android 3.0之后使用无明显缺点,但之前的使用的话存在和scrollTo/scrollBy一样的问题。
  • 改变布局参数,使用简单,适用于有交互的场景。

View的弹性滑动

弹性滑动的实现方式有多种,使用Scroller、动画、通过延时策略。

Scroller实现

上面介绍了Scroller的基本使用方法,看看源码中关于Scroller的描述,发现它本身并不做任何移动View的动作,而是记录了一些滑动相关信息。基本使用方法中,调用startScroll后,调用invalidate方法,这回导致View的重绘,View的重绘又会调用draw方法,draw方法中又会调用computeScroll方法。

在重写的computeScroll方法中,调用了ScrollTo方法来实现View大的滑动,这之后又调用postInvalidate方法,又会发生重绘,如此循化。结束条件是mScroller.computeScrollOffset()返回true,看看computeScrollOffset()的实现:

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
/**
* Call this when you want to know the new location. If it returns true,
* the animation is not yet finished.
*/
public boolean computeScrollOffset() {
if (mFinished) {
return false;
}

int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);

if (timePassed < mDuration) {
switch (mMode) {
case SCROLL_MODE:
final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
mCurrX = mStartX + Math.round(x * mDeltaX);
mCurrY = mStartY + Math.round(x * mDeltaY);
break;
case FLING_MODE:
final float t = (float) timePassed / mDuration;
final int index = (int) (NB_SAMPLES * t);
float distanceCoef = 1.f;
float velocityCoef = 0.f;
if (index < NB_SAMPLES) {
final float t_inf = (float) index / NB_SAMPLES;
final float t_sup = (float) (index + 1) / NB_SAMPLES;
final float d_inf = SPLINE_POSITION[index];
final float d_sup = SPLINE_POSITION[index + 1];
velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
distanceCoef = d_inf + (t - t_inf) * velocityCoef;
}

mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;

mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
// Pin to mMinX <= mCurrX <= mMaxX
mCurrX = Math.min(mCurrX, mMaxX);
mCurrX = Math.max(mCurrX, mMinX);

mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
// Pin to mMinY <= mCurrY <= mMaxY
mCurrY = Math.min(mCurrY, mMaxY);
mCurrY = Math.max(mCurrY, mMinY);

if (mCurrX == mFinalX && mCurrY == mFinalY) {
mFinished = true;
}

break;
}
}
else {
mCurrX = mFinalX;
mCurrY = mFinalY;
mFinished = true;
}
return true;
}

简单说明下,当动画进行的时间小于动画持续时间,就不断更新动画相关的变量,继续动画;当进行时间大于等于规定动画持续时间,就结束动画了,就是这么简单。

动画实现

动画本身就是弹性的(因为动画内部都会有插值器),这里就不详细说明了。

使用延时策略

使用延时策略就是通过发送一系列的延时消息从而达到一种渐进式的效果,具体实现就是利用Handler或View的postDelay方法,当然也可以调用线程的Sleep方法。

这里以Handler为例介绍延时策略实现弹性滑动。

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
private static final int MSG_SCROLL_TO = 1;
private static final int FRAME_COUNT = 33;
private static final int DELAY_TIME = 330;
@SuppressLint("HandlerLeak")
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_SCROLL_TO: {
mCount++;
if (mCount <= FRAME_COUNT) {
float fraction = mCount / (float) FRAME_COUNT;
int scrollX = (int) (100 * fraction);
Log.d(Tag, "scrollX:" + scrollX);
llTestContainer.scrollTo(scrollX, 0);
mHandler.sendEmptyMessageDelayed(MSG_SCROLL_TO, DELAY_TIME);
}
break;
}
default: {
break;
}
}
super.handleMessage(msg);
}
};
mHandler.sendEmptyMessageDelayed(MSG_SCROLL_TO, DELAY_TIME);

上面的llTestContainer是移动的目标View,执行之后你会发现llTestContainer里面的内容向左移动了100像素。(注意并不是llTestContainer本身移动了)。

View的事件分发机制

View的事件分发是View这块的核心和难点所在。这里的事件指的就是点击屏幕产生的MotionEvent对象了。事件分发就是对MotionEvent传递的过程。这个过程由三个十分重要的方法来共同完成:dispatchTouchEventonInterceptTouchEventonTouchEvent。看这三个方法的具体介绍:

public boolean dispathcTouchEvent(MotionEvent event)

用来进行事件的分发。如果事件能够传递给View,那么这个方法一定会被调用。返回结果受当前View的onTouchEvent和子View的dispatchTouchEvent方法影响,表示是否消耗当前事件。

public boolean onInterceptTouchEvent(MotionEvent event)

判断是否进行事件拦截,(显然View没有这个方法,View关心的时处理或不处理这个事件)如果返回true,表示对当前事件进行拦截,事件交由当前View的onTouchEvent来处理,处理结果又onTouchEvent决定。如果返回false,则交由子View的dispatchTouchEvent处理。

public boolean onTouchEvent(MotionEvent event)

是否消费当前事件,true消费,false不消费,交由当前View的父View来处理。

上面三个函数的关系可以用以下伪代码表示:

1
2
3
4
5
6
7
8
9
public boolean dispatchTouchEvent(MotionEvent event) {
boolean consume = false;
if(onInterceptTouchEvent(event)) {
consume = onTouchEvent(event);
} else {
consume = child.dispatchTouchEvent(event)
}
return consume;
}

关于事件传递,给出如下结论:

  1. 同一个事件序列指的是从手指触摸屏幕的那一刻开始到手指离开屏幕的那一刻结束,即事件序列总是以ACTION_DOWN开始,ACTION_UP结束。
  2. 正常情况下一个事件序列只能被一个View消耗,除非你在View的onTouchEvent中强行将事件传递给了其他View。
  3. View(ViewGroup)一旦决定拦截,那么被拦截的这个事件所在序列就都由这个View(ViewGroup)处理,并且针对这个事件序列的其他事件,onInterceptTouchEvent不会再被调用。
  4. View(ViewGroup)一旦开始处理事件,就要负责到底,如果它不消耗ACTION_DOWN事件(即onTouchEvent返回false),那么事件序列的其它的事件都不会交给他处理,而会交由View(ViewGroup)的父元素处理,即调用父元素的onTouchEvent。(通俗的将,我这么信任你,你第一步都没有处理好,我后续的还能信任你吗?)
  5. 如果View(ViewGroup)消耗了事件序列的ACTION_DOWN事件(即在ACTION_DOWN时返回true),但其他事件都没消耗(即返回false),那么这个事件就会消失了,不会交给View的父元素的onTouchEvent处理,并且当前View还可以接受其他事件,只是这些事件最终都会传递给Activity处理。
  6. ViewGroup默认不拦截任何事件,它的onInterceptTouchEvent方法是默认返回false。
  7. View没有onInterceptTouchEvent方法,一旦有事件传递给他,那么它的onTouchEvent事件就会被调用
  8. View的onTouchEvent默认都会消耗事件(即return true),(除非它是不可点击的(clickable和longClickable同时为false)。View的longClickable属性默认为false,clickable属性视具体控件而定,如Button的clickable默认为true,而TextView的则默认为false。
  9. 即使View的enable属性为false,它也可以消耗事件(前提是它是可点击的),也就是说enable属性不影响onTouchEvent的返回值。
  10. View的onClick会发生的前提是当前的View是可点击的,并且它接受到了ACTION_DOWNACTION_UP事件。
  11. 事件传递是由外向内的,及Activity->Window->DecorView->ViewGroup…->最底层View,在这个过程中父级View可以通过onInterceptTouchEvent来拦截事件,而子View可以通过requestDisallowInterceptTouchEvent来干预父元素(除`ACTION_DOWN外)的事件分发。

从源码角度理解事件分发

Activity对点击事件的分发

上面总结的第十一条描述了事件的传递方向,那么位于第一层的Activity如何分发事件的呢?看源码:

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

显然事件都交给了Window来处理,如果getWindow().superDispatchTouchEvent(ev)返回true,则事件序列的处理就结束了,如果返回false,则意味着传递过程中所有View的onTouchEvent都返回false了,交由Activity的onTouchEvent来处理。

Window是个抽象类,其唯一实现类时PhoneWindow,所以我们看看PhoneWindow是如何处理事件的:

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

显然,交给了DecorView处理,看DecorView如何处理:

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

而DecorView是FrameLayout的子类,也就是说事件会交给FrameLayout(ViewGroup的子类)的dispatchTouchEvent处理,也就是说,事件最终都交给了ViewGroup处理。

顶级View的事件分发过程

这里结合源码来对上面的结论做说明。首先解释结论11,即子View可以干预父View的事件拦截过程(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
// Handle an initial down.
// 子View干预父View事件拦截但不能干预ACTION_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();
}

// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// 子View干预父View事件拦截的具体体现
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;
}

上面代码中,先看这个if判断:

1
2
3
4
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
...
}

可见能进行事件拦截的先决条件是要么当前事件是ACTION_DOWN,要么mFirstTouchTarget不为null,这个mFirstTouchTarget是在ViewGroup不拦截事件且指定那个子View来处理事件时会被赋值的,也就是说,如果ViewGroup决定拦截事件,mFirstTouchTarget != null 就不成立,那么当其他事件(非ACTION_DOWN事件)来临时,onInterceptTouchEvent都不会再调用,这就印证了上面的结论3。

在开始判断是否拦截之前,有一个ACTION_DOWN的判断,该判断就是印证结论11的子View不能干预父View对ACTION_DOWN事件的处理,因为这个判断中会对FLAG_DISALLOWED_INTERCEPT标志位重置。

根据上面代码,可以有如下总结:

  • onInterceptTouchEvent并不是每次都调用,如果我们想处理每个事件,我们必须写在dispatchTouchEvent中;
  • FLAG_DISALLOWED_INTERCEPT标志位为解决滑动提供了一种可能途径。

当ViewGroup不拦截事件时,事件会分发到它的子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
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;
}

// 如果child不能接收点击事件或者点击的点不包含在child的矩形区域内,不处理事件
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);
// 传递到子View的条件:1.可以接收事件;2.点击点在子View矩形区域内
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);
}

上面的dispatchTransformedTouchEvent方法,其实调用的就是子View的dispatchTouchEvent。如果子View的dispatchTouchEvent返回true,那么:

1
2
3
4
5
6
7
8
9
10
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
mLastTouchDownIndex = i;
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}

在里面的addTouchTarget方法中,mFirstTouchTarget被赋值,然后跳出循环。赋值的具体实现

1
2
3
4
5
6
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}

如果上面的for循环结束事件都没有被合适的处理(可能有两种情况:ViewGroup没有子元素或ViewGroup的子元素处理了事件但它的dispatchTouchEvent方法返回的时false,表示没消费事件,实际上已经处理了)这样的话ViewGroup会自己处理事件。代码片段如下:

1
2
3
4
5
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}

这里的第三个参数为空,此时就会调用super.dispatchTouchEvent,即View的dispatchTouchEvent方法了,事件交由了View来处理。接下来看看View(不包括ViewGroup)对事件的处理。

View对事件的处理

View(非ViewGroup)对事件的处理就比较简单了,先看View的dispatchTouchEvent代码片段:

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

这里可以很好的解释为什么说OnTouchListener的优先级比onTouchEvent的高了。

再看onTouchEvent的代码片段:

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
// 判断是否可点击,以下三种状态都是可点击状态
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

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.
// Disable状态的View,如果可点击,仍然会消耗点击事件,只是单纯的返回true表示我消耗了事件,没有做任 // 何事情
return clickable;
}
// 如果设置了代理,就调用代理的方法,是否消耗视代理对象的onTouchEvent返回值影响
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
// 如果是可点击的View或可展示Tooltip的View,则进行具体的时间处理
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)) {
performClick();
}
}
}

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;

...
}

return true;
}

看见那个performClick方法了没?这应该就是我们点击事件发生的地方,看看这个方法都做了什么:

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

sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

notifyEnterOrExitForAutoFillIfNeeded(true);

return result;
}

从上面的那个if判断就可以得出如下结论:

View.OnClickListener的执行是在View的onTouchEvent执行之后的,且在调用View的setOnClickListener方法的时候,改变了View的clickable属性。

到这里View的事件分发差不多分析完了,其实细看起来并不复杂哈。

View的滑动冲突

引起滑动冲突的原因

既然是滑动冲突,那么说明可滑动的View应该不止一个,引起冲突的原因大致可分为以下几种:

  1. 内部View和外部View都可滑动,滑动方向不一致;
  2. 内部View和外部View都可滑动,滑动方向一致;
  3. 上面两种情况的嵌套;

第一种情况,比较常见的ViewPager + Fragment组合,然后Fragment里面又是一个RecyclerView或ListView,本来这种情况应该是滑动冲突的,但我们平常这样使用的时候却没有出现问题。其实是因为ViewPager内部做了处理。

第二种情况,比较常见的就是ScrollView里面嵌套一个ListView或RecyclerView,都在竖直方向滑动,如果处理不好,可能只能有一个能滑动一个不能滑动,也有可能两个都能滑动,但会卡顿,通常这种情况我们是希望二者能同时滑动。

第三种情况,比较常见的就是SlideMenu + ViewPager+Fragment,然后Fragment里面又是一个RecyclerView或ListView,虽然看上去更复杂了,但其实它是1和2的组合,只要我们能将1和2逐个击破,也就能顺利解决问题了。

滑动冲突的一般处理规则

针对第一种场景,通常是左右互动时外部的ViewPager处理,上下滑动是,内部的ListView处理。具体即使先判断当前滑动是竖直滑动还是水平滑动,然后再将事件分发给合适的View处理。

针对第二种场景,我们就不能想第一种那样简单处理了,这个时候我们要结合具体的业务规则来判断事件的分发。

针对第三种场景,处理同2,得结合业务分析。

滑动冲突的解决方式

为什么会滑动冲突,因为系统不知道事件到底要以何种规则来交给不同的View来处理,所以解决滑动冲突的基础就是深入了解Android View的事件传递机制。其实核心方法就是onInterceptTouchEvent和requestDisallowInterceptTouchEvent。

外部拦截法

主要是重写外层View容器的onInterceptTouchEvent来决定事件的分发;

内部拦截法

主要是重写内部View的requestDisallowInterceptTouchEvent来干涉外部View容器的onInterceptTouchEvent对事件分发的处理。

总结

掌握View的基础知识,是自定义View的基础,掌握View的事件分发是定义出好控件的基础,同时也是解决好滑动冲突的基础。虽然View的事件分发看起来比较复杂,但是结合源码来分析,还是比较容易理解的,所以没事多看看源码,你会受益匪浅的。