Hiten's Blog.

解惑requestDisallowInterceptTouchEvent

字数统计: 1.4k阅读时长: 6 min
2016/11/28 Share

最近在看官方控件源码时,无意间看到某些代码,让我想起有很多用requestDisallowInterceptTouchEvent来解决ScrollView和ViewPager冲突的例子,包括任玉刚写的《Android开发艺术探索》一书也提到这种方式,但是关于requestDisallowInterceptTouchEvent,你真的了解了吗?

这是网上写的最多的用requestDisallowInterceptTouchEvent解决ScroolView相关的滑动冲突例子,确实是正确姿势;

1
2
3
4
5
6
7
8
9
10
11
publicbooleanonTouchEvent(MotionEventevent){
switch(event.getAction()){
caseMotionEvent.ACTION_MOVE:
parent.requestDisallowInterceptTouchEvent(true);
break;
caseMotionEvent.ACTION_UP:
caseMotionEvent.ACTION_CANCEL:
parent.requestDisallowInterceptTouchEvent(false);
break;
}
}

疑问一.

parent.requestDisallowInterceptTouchEvent的调用为什么要写在onTouchEvent方法中,而不是构造方法或生命周期方法?

看一眼ViewGroup中关于requestDisallowInterceptTouchEvent源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {

if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
// We're already in this state, assume our ancestors are too
return;
}

if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}

// Pass it up to our parent
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}

官方是用mGroupFlags 和FLAG_DISALLOW_INTERCEPT按位运算,最终得到(mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0这一bool值来标识是否拦截
现在再看看dispatchTouchEvent方法是怎样处理拦截事件的;
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

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

dispatchTouchEvent每次接受到点击事件时,会初始化触摸状态,然后判断disallowIntercept是否为true,如果为true,不执行onInterceptTouchEvent,下面再看看resetTouchState()方法是如何实现的:

1
2
3
4
5
6
7
8
9
/**
* Resets all touch state in preparation for a new cycle.
*/
private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
mNestedScrollAxes = SCROLL_AXIS_NONE;
}

resetTouchState()会重置mGroupFlags标识,看到这句代码没:mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;这句代码会重置mGroupFlags对FLAG_DISALLOW_INTERCEPT的状态,你可以理解成把disallowIntercept置为false,所以得到结论,在dispatchTouchEvent中,每次触发按下事件时,disallowIntercept置为false,所以就解释了为什么在子view中的构造方法或生命周期方法调用parent.requestDisallowInterceptTouchEvent会失效;

额外知识
为什么(mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0能标识一个指定的boolean类型,这就是考验你计算机基础是时候了,二进制知识和几个按位操作符 & | ~ ,你还记得吗?

1
2
3
4
mGroupFlags是一个16进制整形,可以标识很多状态,每一位标识一个状态
在ViewGroup中FLAG_DISALLOW_INTERCEPT的值为0x80000,化成二进制,就是1000000000000000000
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT操作得到的是 ??...(1/0)???????????????????,就是说mGroupFlags化成二进制的第20位标识的是DISALLOW_INTERCEPT,这样运算的好处是,不影响mGroupFlags其他位的数值
(mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0这个运算,唯一有效的就是第20位,因为其他位都是0,&的结果还是0,没有意义,所以第20位为0时,运算结果为false,第20位为1时,运算结果为true

疑问二.

既然parent已经做了拦截,事件又是如何传递到child view的onTouchEvent方法中的?

这样疑问可能有很多同学也纳闷过,无奈网上没人回答,其实翻看ScrollView源码就能看明白了,现在我们只有自己看ScrollView源码了;

onInterceptTouchEvent方法中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
@Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {
...此处省略很多代码
case MotionEvent.ACTION_DOWN: {
final int y = (int) ev.getY();
if (!inChild((int) ev.getX(), (int) y)) {
mIsBeingDragged = false;
recycleVelocityTracker();
break;
}

/*
* Remember location of down touch.
* ACTION_DOWN always refers to pointer index 0.
*/
mLastMotionY = y;
mActivePointerId = ev.getPointerId(0);

initOrResetVelocityTracker();
mVelocityTracker.addMovement(ev);
/*
* If being flinged and user touches the screen, initiate drag;
* otherwise don't. mScroller.isFinished should be false when
* being flinged. We need to call computeScrollOffset() first so that
* isFinished() is correct.
*/
mScroller.computeScrollOffset();
mIsBeingDragged = !mScroller.isFinished();
if (mIsBeingDragged && mScrollStrictSpan == null) {
mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll");
}
startNestedScroll(SCROLL_AXIS_VERTICAL);
break;
}
...此处省略很多代码
/* * The only time we want to intercept motion events is if we are in the * drag mode. */
return mIsBeingDragged;
}

ScrollView中定义了一个属性mIsBeingDragged,而onInterceptTouchEvent的返回值就是onInterceptTouchEvent,就意味着mIsBeingDragged为ture时,拦截事件,为false时,不拦截;

onInterceptTouchEvent方法中ACTION_MOVE处理:

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
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_MOVE: {
/*
* mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
* whether the user has moved far enough from his original down touch.
*/

/*
* Locally do absolute value. mLastMotionY is set to the y value
* of the down event.
*/
final int activePointerId = mActivePointerId;
if (activePointerId == INVALID_POINTER) {
// If we don't have a valid id, the touch down wasn't on content.
break;
}

final int pointerIndex = ev.findPointerIndex(activePointerId);
if (pointerIndex == -1) {
Log.e(TAG, "Invalid pointerId=" + activePointerId
+ " in onInterceptTouchEvent");
break;
}

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

可以看出,只有在满足条件yDiff>mTouchSlop时,才可以执行 mIsBeingDragged = true;这就说明,触摸事件不是一下子就拦截掉,在yDiff<=mTouchSlop这一段时机下, 子View是可以得到触摸事件的,这就解释了为什么在子View的onTouchEvent方法中,可以执行到parent.requestDisallowInterceptTouchEvent()这句代码;

其实这个谷歌完全可以在requestDisallowInterceptTouchEvent注释上写明白,结果并没有,带着疑惑,也促进了我们就看源码的习惯,嗯嗯!!!

总结

除了ScrollView,其实在很多Android原生的滑动布局的onInterceptTouchEvent都是这样处理拦截的,比如SwipeRefreshLayout,当然,自己想写一个滑动布局,大致也是参考这些,这样的写法可以说是约定成俗的,只是细心的你能不能察觉这一切了

CATALOG
  1. 1. 疑问一.
  2. 2. 疑问二.
  • 总结