解惑requestDisallowInterceptTouchEvent

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

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

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源码:

 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部分源码:


 // 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()方法是如何实现的:

/**
     * 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类型,这就是考验你计算机基础是时候了,二进制知识和几个按位操作符 & | ~ ,你还记得吗?

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处理:

@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处理:

 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,当然,自己想写一个滑动布局,大致也是参考这些,这样的写法可以说是约定成俗的,只是细心的你能不能察觉这一切了

推荐阅读更多精彩内容