ScrollView的嵌套滑動沖突的解決

前言

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

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

正文

記得有一次要使用多個ScrollView嵌套的時候母谎,需要同時讓兩層ScrollView的滑動都能生效。但是京革,當(dāng)我直接套了兩層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 的意思是晌砾,此視圖組的嵌套滾動的當(dāng)前軸是否是縱向(看了注釋之后理解的坎拐,這里不能debug很蛋疼). getNestedScrollAxes()的值應(yīng)該是0,因為搜索了全文养匈,發(fā)現(xiàn)針對mNestedScrollAxes值的變動哼勇,在類內(nèi)部就只有賦值為0的情況,而&與運算呕乎,只要有0积担,就可以斷言整個都是0了,所以==0楣嘁,成立磅轻。
兩者都是true,則進入if逐虚。 進入之后:mIsBeingDragged = true; 便會執(zhí)行聋溜。
當(dāng)?shù)谝粋€move執(zhí)行之后,mIsBeingDragged 已經(jīng)是true叭爱。當(dāng)?shù)诙€move來的時候撮躁,ScrollView便會阻攔后面所有的move。 這就是內(nèi)層ScrollView不能滑動的原因买雾。

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

重寫了onInterceptTouchEvent 直接 return false叙赚,那原本scrollViewonInterceptTouchEvent過程則不會執(zhí)行。現(xiàn)在僚饭,所有的事件直接透傳震叮,那么內(nèi)層ScrollView就可以收到事件,自然就有了滑動效果鳍鸵。但是苇瓣,當(dāng)手指在內(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)系作者
  • 序言:七十年代末猜丹,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子硅卢,更是在濱河造成了極大的恐慌射窒,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件将塑,死亡現(xiàn)場離奇詭異脉顿,居然都是意外死亡,警方通過查閱死者的電腦和手機点寥,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進店門艾疟,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人敢辩,你說我怎么就攤上這事蔽莱。” “怎么了戚长?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵盗冷,是天一觀的道長。 經(jīng)常有香客問我同廉,道長仪糖,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任迫肖,我火速辦了婚禮锅劝,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘蟆湖。我一直安慰自己故爵,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布帐姻。 她就那樣靜靜地躺著稠集,像睡著了一般奶段。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上剥纷,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天痹籍,我揣著相機與錄音,去河邊找鬼晦鞋。 笑死蹲缠,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的悠垛。 我是一名探鬼主播线定,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼确买!你這毒婦竟也來了斤讥?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤湾趾,失蹤者是張志新(化名)和其女友劉穎芭商,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體搀缠,經(jīng)...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨居荒郊野嶺守林人離奇死亡铛楣,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了艺普。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片簸州。...
    茶點故事閱讀 40,102評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖歧譬,靈堂內(nèi)的尸體忽然破棺而出岸浑,到底是詐尸還是另有隱情,我是刑警寧澤缴罗,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布助琐,位于F島的核電站,受9級特大地震影響面氓,放射性物質(zhì)發(fā)生泄漏兵钮。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一舌界、第九天 我趴在偏房一處隱蔽的房頂上張望掘譬。 院中可真熱鬧,春花似錦呻拌、人聲如沸葱轩。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽靴拱。三九已至垃喊,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間袜炕,已是汗流浹背本谜。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留偎窘,地道東北人乌助。 一個月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像陌知,于是被迫代替她去往敵國和親他托。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,044評論 2 355

推薦閱讀更多精彩內(nèi)容