ScrollView的嵌套滑動沖突的解決

前言

做程序開發(fā),基礎(chǔ)很重要。同樣是擰螺絲人家擰出來的可以經(jīng)久不壞,你擰出來的遇到點風(fēng)浪就開始顫抖,可見基本功的重要性。此系列,專門收錄一些看似基礎(chǔ),但是沒那么簡單的小細節(jié),同時提供權(quán)威解決方案。喜歡的同志們點個贊就是對我最大的鼓勵!先行謝過!

網(wǎng)上可能有一些其他文章,提供了解決方案,但是要么就是沒有提供可運行demo,要么就是demo不夠純粹,讓人探索起來受到其他代碼因素的影響,無法專注于當前這個知識點(比如,我只是想了解Activity的生命周期,你把生命周期探究的過程混入到一個很復(fù)雜的大雜燴Demo中,讓人一眼就沒有了閱讀Demo代碼的欲望),所以我覺得有必要做一個專題,用最純粹的方式展示一個的解決方案.

正文

記得有一次要使用多個ScrollView嵌套的時候,需要同時讓兩層ScrollView的滑動都能生效。但是,當我直接套了兩層ScrollView之后,發(fā)現(xiàn)內(nèi)層的滑動完全無效了。

研究一番之后發(fā)現(xiàn)解決方案其實非常簡單。

效果

多層ScrollView嵌套.gif

不墨跡,直接給出源碼工程github.

關(guān)鍵代碼

android的事件分發(fā)滑動沖突的基礎(chǔ)知識,這里不再贅述。
兩種解決方案:
1,自定義外層ScrollView的攔截行為. 重寫onInterceptTouchEvent,直接返回false,外層不再攔截事件。

public class OutsideScrollView extends ScrollView {

    public OutsideScrollView(Context context) {
        this(context, null);
    }

    public OutsideScrollView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public OutsideScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return false;
    }

}

2、自定義內(nèi)層 ScrollView的攔截行為,調(diào)用 getParent().requestDisallowInterceptTouchEvent(true);不允許外層對它的事件進行攔截.


public class InsideScrollView extends ScrollView {
    public InsideScrollView(Context context) {
        this(context, null);
    }

    public InsideScrollView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public InsideScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    //如果我不允許外部攔截我呢?
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        getParent().requestDisallowInterceptTouchEvent(true);
        return super.onInterceptTouchEvent(ev);
    }

}

原理

先來解決 第一個疑問 不進行上面的處理時內(nèi)部的ScrollView滑動不了呢?

解讀一下ScrollView的源碼,發(fā)現(xiàn),它重寫了 ViewonInterceptTouchEventonTouchEvent。

image.png

上圖中,我們能在重寫的onInterceptTouchEvent方法中找到兩處return truetrue則攔截或者消費,false則放行或不消費,整個事件分發(fā)機制都是這個套路,記住就行了)。
第二處,調(diào)用的是父類,也就是FrameLayout的攔截返回值,一般都會返回false放行,不理會即可。
只看第一處,首先,指定攔截ACTION_MOVE事件,并且還有另一個條件。
mIsBeingDragged - 是否正在拖拽。看看這個值什么時候會變成 true,找到下面這個地方(其實還有另一處,在onTouchEvent中,但是現(xiàn)在還沒到事件回傳的時候,所以不用看)
image.png

讓它變成true判定條件為:
if (yDiff > mTouchSlop && (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) {
yDiff > mTouchSlop的意思是,Y軸上的滑動距離,要大于設(shè)備規(guī)定的最小滑動距離.
(getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0 的意思是,此視圖組的嵌套滾動的當前軸是否是縱向(看了注釋之后理解的,這里不能debug很蛋疼). getNestedScrollAxes()的值應(yīng)該是0,因為搜索了全文,發(fā)現(xiàn)針對mNestedScrollAxes值的變動,在類內(nèi)部就只有賦值為0的情況,而&與運算,只要有0,就可以斷言整個都是0了,所以==0,成立。
兩者都是true,則進入if。 進入之后:mIsBeingDragged = true; 便會執(zhí)行。
當?shù)谝粋€move執(zhí)行之后,mIsBeingDragged 已經(jīng)是true。當?shù)诙€move來的時候,ScrollView便會阻攔后面所有的move。 這就是內(nèi)層ScrollView不能滑動的原因。

第二個疑問:為什么自定義外層 scrollView,重寫 onInterceptTouchEvent 直接 return false之后,內(nèi)層就能正常滑動呢,而且手指在內(nèi)層滑動時,外層是不動的?

重寫了onInterceptTouchEvent 直接 return false,那原本scrollViewonInterceptTouchEvent過程則不會執(zhí)行?,F(xiàn)在,所有的事件直接透傳,那么內(nèi)層ScrollView就可以收到事件,自然就有了滑動效果。但是,當手指在內(nèi)層滑動時,外層不受影響。這是為何。
答案在 ScrollViewonTouchEvent方法內(nèi)(代碼太長,我就不貼全部了)

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        initVelocityTrackerIfNotExists();

        MotionEvent vtev = MotionEvent.obtain(ev);

        final int actionMasked = ev.getActionMasked();

        if (actionMasked == MotionEvent.ACTION_DOWN) {
            mNestedYOffset = 0;
        }
        vtev.offsetLocation(0, mNestedYOffset);

        switch (actionMasked) {
            case MotionEvent.ACTION_DOWN: {
                if (getChildCount() == 0) {
                    return false;
                }
                .... 省略N 行代碼
                break;
            }
            ... 省略N 行代碼
        }

        if (mVelocityTracker != null) {
            mVelocityTracker.addMovement(vtev);
        }
        vtev.recycle();
        return true;
    }

很明確,ScrollViewonTouchEvent,消費掉了除DOWN之外的所有事件。所以外層ScrollView收不到move,自然就沒有任何反應(yīng)。

第三個疑問:內(nèi)層攔截 getParent().requestDisallowInterceptTouchEvent(true)到底做了什么,讓外層無法攔截事件?

先看getParent, 眾所周知,View不是一個獨立個體,它是一個樹形結(jié)構(gòu),有一個parent節(jié)點,也有Nchild節(jié)點。這個getParent實際上就是得到自己的父View。
看看ViewGrouprequestDisallowInterceptTouchEvent

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

可以看到入?yún)ⅲ?code>disallowIntercept的值,改變了全局變量mGroupFlags 的值。并且,這個方法將disallowIntercept的值向父View傳遞。
全局變量mGroupFlags 什么時候用到呢?
進入ViewGroupdispatchTouchEvent方法:

image.png

可以斷定,之前傳入的disallowIntercept 入?yún)⒅?,一定可以影響到這里的局部變量boolean disallowIntercept的值,并且如果之前傳入true,這里就會得到true你問我為什么會斷定?因為這是在書上看到的。。。具體過程涉及到數(shù)字的位運算,賊復(fù)雜,在這里說不清楚,以后做專題的時候再講吧).
如果之前傳入的是true,那么這里就會執(zhí)行else 中的 intercepted = false; 也就是,不會執(zhí)行這個
intercepted = onInterceptTouchEvent(ev); 明白了吧? 如果內(nèi)層調(diào)用了requestDisallowInterceptTouchEvent(true),在父viewdispatchTouchEvent中,就不會執(zhí)行onInterceptTouchEvent.


值得一提的是,requestDisallowInterceptTouchEvent(true) 方法內(nèi)部,調(diào)用了mParent.requestDisallowInterceptTouchEvent(disallowIntercept);,讓這個bool值會一直向上傳遞,也就是說,如果一個子view調(diào)用了這個方法,那么它的父,父的父。。。節(jié)點,都不會攔截它的事件。

結(jié)語

閱讀源碼是一個痛苦的過程,隨時隨地會發(fā)現(xiàn)自己的知識盲區(qū)。但是,不讀源碼,就不知道源碼的深淺,就無法進階成高級工(super)程(ma)師(nong),努力吧,騷年!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容