Android触摸事件

在阅读Android触摸事件处理的源码的时候看到了这么一个方法:event.getPointerIdBits()。这个方法是不对外暴露的。但是我们可以看到他的源码:

1
2
3
4
5
6
7
8
public final int getPointerIdBits() {
int idBits = 0;
final int pointerCount = nativeGetPointerCount(mNativePtr);
for (int i = 0; i < pointerCount; i++) {
idBits |= 1 << nativeGetPointerId(mNativePtr, i);
}
return idBits;
}

可以看到他先是调用了nativeGetPointerCount()这个C方法获取到当前手指头触摸在屏幕上的总数。想获取手指头触摸在屏幕的总数我们可以通过方法:event.getPointerCount()这是官方暴露给我们的方法,他的源码就是调用了方法nativeGetPointerCount()

1
2
3
public final int getPointerCount() {
return nativeGetPointerCount(mNativePtr);
}

继续回到方法getPointerIdBits,可以看到里面调用了方法nativeGetPointerId()。这个方法就是获取到指定位置的手指id。这个方法官方也提供了一个公开方法让我们调用event.getPointerId(i)

1
2
3
public final int getPointerId(int pointerIndex) {
return nativeGetPointerId(mNativePtr, pointerIndex);
}

所以这么一拆分,我们可以将方法getPointerIdBits拆成这么写:

1
2
3
4
5
6
7
8
public final int getPointerIdBits() {
int idBits = 0;
final int pointerCount = getPointerCount();
for (int i = 0; i < pointerCount; i++) {
idBits |= 1 << getPointerId(i);
}
return idBits;
}

这么一看我们似乎明白了getPointerIdBits()是干嘛的了。我们可以通过这个方法获取到是有哪些手指头放在屏幕上了。

这个最好我们自己试验下。getPointerId(index)他是获取当前这个index对应的手指id。比如我先大拇指按在屏幕上,这个时候他的getPointerId的值就是0。然后保持大拇指按着,在按下无名指这个时候这个无名指的getPointerId就是1。如果这个时候你抬起大拇指那么调用getPointerId得到的值是1。这个方法就是用来获取在屏幕上手指的id的。默认这个id从0开始。

一个触摸事件在ViewRootImpl产生,调用mView.dispatchPointerEvent(event)这个方法将事件分发给视图层。

1
2
3
4
5
6
7
8
// View.java
public final boolean dispatchPointerEvent(MotionEvent event) {
if (event.isTouchEvent()) {
return dispatchTouchEvent(event);
} else {
return dispatchGenericMotionEvent(event);
}
}

首先映入眼帘的是方法dispatchTouchEvent,这里我直接先看ViewGroup的这个方法:

1
2
3
4
5
6
7
8
9
10
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
// 核心代码
}
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
}

再看ViewGroup的dispatchTouchEvent方法的时候,首先所有的核心方法都被包括在了一个判断里面onFilterTouchEventForSecurity。这个方法意思就是执行一个安全策略,当不符合条件的时候,你可以屏蔽你的应用控件处理这个事件。这是一个公开方法,在自定义view的时候可以重写这个方法。默认这个方法是在当应用不可见的时候才返回false。

这个onInterceptTouchEvent方法的触发条件:

1
2
3
4
5
6
7
8
9
10
11
12
//ViewGroup.dispatchTouchEvent
// 当时down事件或者mFirstTouchTarget不为空
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
}

onInterceptTouchEvent方法返回true的时候,也就是拦截这个事件,交给自己来处理。

1
2
3
4
5
6
7
// 当onIterceptTouchEvent返回true。第一次的时候这个mFirstTouchEvent肯定是等于null的。所以。第一次
// mFirstTouchTarget == null 一定为true所以就会进入dispatchTransformedTouchEvent分发事件方法
//dispatchTransformedTouchEvent这个方法回去判断是调用谁的touchevent
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, cancel=canceled, child=null,
desiredPointerIdBits=TouchTarget.ALL_POINTER_IDS);
}
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
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
// 这个getPointerIdBits获取的结果是所有手指头对应的id数的总和。id数分别为0 1 2 3 ...
// 然后getPointerIdBits就将这些id向左进行了对应的id数目的偏移,然后将他们加了起来用来判断手指头的数量是否改变了并且也可以知道改变了谁。 这个上面讲了。
// 默认getPointerIdBits获取的值等于1 因为既然到了这里代表有一根手指触摸屏幕了那么他的id=0.然后1<<0 的结果是1. 所以getPointerIdBits的结果就是1.
final int oldPointerIdBits = event.getPointerIdBits();
// 默认desiredPointerIdBits = -1
// 所以默认第一次这么一与运算,这个 newPointerIdBits = 1
final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;

if (newPointerIdBits == 0) {
return false;
}

final MotionEvent transformedEvent;
// 默认第一次这是相等的 1==1
if (newPointerIdBits == oldPointerIdBits) {
// 在viewgroup里面如果onInterceptor为true那么child一定是null
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
// 进入view的dispatchTouchEvent
handled = super.dispatchTouchEvent(event);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY);

handled = child.dispatchTouchEvent(event);

event.offsetLocation(-offsetX, -offsetY);
}
return handled;
}
transformedEvent = MotionEvent.obtain(event);
} else {
transformedEvent = event.split(newPointerIdBits);
}

// Perform any necessary transformations and dispatch.
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);
}

// Done.
transformedEvent.recycle();
return handled;
}

这里会进入到view的dispatchTouchEvent方法。然后会触发onTouchEvent方法。

哎整个android应用就是一个递归调用,递归啊,我最害怕的递归啊,整个触摸事件的传递就是一个递归调用的过程。当触发disPatchTouchEvent就是咱们递归的开始。这里我给你演示下三成view的情况。

img-0

首先1触发dispatchTouchEvent方法,当这次是一个按下的事件或者找到了事件传递链。那么就进入自己的事件拦截方法判断是不是要拦截事件。

如果不拦截事件,则遍历全部孩子,这里1的孩子只有2。然后调用孩子2的dispatchTouchEvent方法如果孩子2这个方法返回true。那么就代表找到了这个事件传递链。

整个事件传递其实只需要看两个方法dispatchTouchEventdispatchTransformedTouchEvent

dispatchTouchEvent他的返回值代表这次事件传递是否找到了消费者。找到了消费者就可以为我们的mFirstTouchTarget设置上值了。最终去处理这个事件的视图的视图他的mFirstTouchTarget的值是null。

现在在回来看下ViewGroup的dispatchTouchEvent:

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
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
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);
} else {
intercepted = false;
}
} else {
intercepted = true;
}
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
if (newTouchTarget == null && childrenCount != 0) {
for (int i = childrenCount - 1; i >= 0; i--) {
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
}
}
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
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;
}
}
return handled;
}

嘿嘿被我这么一删,就可以很清晰的看到整个执行流程了。

首选判断如果是按下事件或者mFirstTouchTarget!=null 那么就执行onInterceptTouchEvent方法判断是否拦截事件。

然后不拦截的话,然后这个事件是一个按下事件,就去遍历所有的孩子执行方法dispatchTransformedTouchEvent里面去调用了孩子的dispatchTouchEvent方法。直到某个dispatchTouchevent方法返回了true。返回true才代表找到了消费这个事件的人。然后我们的mFirstTouchTarget就有值了。然后接着是滑动事件。最先收到事件的当然也是最外层的父容易。然后由于他的mFirstTouchTarget值不为空所以还是会执行他自己的onInterceptTouchEvent判断是否拦截事件。由于这次不是按压事件了。所以直接按着mFirstTouchTarget的调用链执行dispatchTransformedTouchEvent。

我们再来回顾下父视图什么时候会执行onInterceptTouchEvent方法:

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

我们可以看到在点击事件的时候,是一定会走拦截方法的。然后还有一个情况就是当mFirstTouchTarget不等于null的时候。那什么时候mFirstTouchTarget不等于null呢?答:在触发点击事件的时候,这个事件在某个子视图的onTouchEvent的down事件返回true或者自己的onTouchEvent在处理down事件的时候返回了true。这样子我们的整整一个事件调用链的mFirstTouchTarget的值就不会为null。既然不会为null了,那么接下来的滑动事件抬起事件,在onInterceptTouchevent里面就能接收到了。为什么我会产生对这个的深入理解,那是因为我在看NestedScrollView的源码的时候,发现NestedScrollView只重写了onInterceptTouchEvent方法:

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
public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
if ((action == MotionEvent.ACTION_MOVE) && mIsBeingDragged) {
return true;
}
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_MOVE: {
final int activePointerId = mActivePointerId;
if (activePointerId == INVALID_POINTER) {
break;
}
final int pointerIndex = ev.findPointerIndex(activePointerId);
if (pointerIndex == -1) {
break;
}

final int y = (int) ev.getY(pointerIndex);
final int yDiff = Math.abs(y - mLastMotionY);
if (yDiff > mTouchSlop
&& (getNestedScrollAxes() & ViewCompat.SCROLL_AXIS_VERTICAL) == 0) {
mIsBeingDragged = true;
mLastMotionY = y;
initVelocityTrackerIfNotExists();
mVelocityTracker.addMovement(ev);
mNestedYOffset = 0;
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
break;
}

case MotionEvent.ACTION_DOWN: {
final int y = (int) ev.getY();
if (!inChild((int) ev.getX(), y)) {
mIsBeingDragged = false;
recycleVelocityTracker();
break;
}
mLastMotionY = y;
mActivePointerId = ev.getPointerId(0);

initOrResetVelocityTracker();
mVelocityTracker.addMovement(ev);
mScroller.computeScrollOffset();
mIsBeingDragged = !mScroller.isFinished();
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);
break;
}

case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
mIsBeingDragged = false;
mActivePointerId = INVALID_POINTER;
recycleVelocityTracker();
if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, getScrollRange())) {
ViewCompat.postInvalidateOnAnimation(this);
}
stopNestedScroll(ViewCompat.TYPE_TOUCH);
break;
case MotionEvent.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
break;
}
/*
* 我们唯一只想拦截拖动事件
*/
return mIsBeingDragged;
}

可以看到在onInterceptTouchEvent里面只在拖动事件的时候返回true。那问题来了。如果NestedScrollView的所有孩子都没有对down事件进行返回true的处理的话,那么在NestedScrollView的onInterceptTouchEvent不是只能接收到down事件了,关于move up事件都不会到这里来了。这方面NestedScrollView当然考虑到了,我们可以看NestedScrollView的onTouchEvent事件,他直接返回了一个true。不管孩子他们对事件处不处理,反正到了我这里我全部是要处理的。

1
2
3
4
5
6
7
8
9
10
public boolean onTouchEvent(MotionEvent ev) {
switch (actionMasked) {
case MotionEvent.ACTION_DOWN: {
if (getChildCount() == 0) {
return false;
}
}
}
return true
}

他这么一处理,NestedScrollview就再也不怕他的onInterceptTouchEvent不会执行了。

那现在来分析下NestedScrollView是如何实现滑动的,并且解决嵌套滑动的问题的呢。

关于嵌套滑动,谷歌官方提供了一组类供我们使用。

  • NestedScrollingChildHelper
  • NestedScrollingParentHelper

然后还有一组接口:

  • NestedScrollingParent
  • NestedScrollingParent2
  • NestedScrollingParent3
  • NestedScrollingChild
  • NestedScrollingChild2
  • NestedScrollingChild3

他的实现原理就是,父类继承接口NestedScrollingParent。子类继承接口NestedScrollingChild。在滑动的时候,子类主动告诉父类子类想要消耗多少距离,消耗不了的距离告诉父类,从而实现嵌套滑动的功能。使用规则就是在down时间的时候调用ChildHelper.startNestedScroll(axes,type)。然后再滑动的时候调用ChildHelper.dispatchNestedPreScrollView()。然后在UP事件调用ChildHelper.stopNestedScroll()