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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容