DrawerLayout onDrawerOpened 響應(yīng)時(shí)機(jī)


遇到問題的場景

簡要說明一下我的使用場景,現(xiàn)在有兩個(gè)頁面 A 和 B重归,由 A 頁面 startActivity 啟動 B 頁面妈橄。A 頁面的根布局是 DrawerLayout 涌哲,B 頁面有個(gè)按鈕用來發(fā)送廣播,A 頁面接收到 B 頁面發(fā)送的廣播之后剩燥,調(diào)用 DrawerLayout 的 openDrawer 方法打開抽屜慢逾,然后在 void onDrawerOpened(View drawerView) 回調(diào)方法中打印日志。

A 頁面代碼

我省略了一些模板代碼灭红,只保留了關(guān)鍵代碼

public class MainActivity extends AppCompatActivity {
    DrawerLayout drawer;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //...
        drawer =  findViewById(R.id.drawer_layout);
        
        // 給 DrawerLayout 添加一個(gè)回調(diào)方法
        drawer.addDrawerListener(new DrawerLayout.SimpleDrawerListener() {
            @Override
            public void onDrawerOpened(View drawerView) {
                Log.e("MainActivity", "onDrawerOpened");
            }
        });

        OpenDrawerReceiver receiver = new OpenDrawerReceiver();
        IntentFilter intentFilter = new IntentFilter("open_drawer");
        //注冊 open_drawer 廣播
        registerReceiver(receiver, intentFilter);

    }

    //...

    // 跳轉(zhuǎn)到 B 頁面
    public void jumpToSecond(View view) {
        startActivity(new Intent(this, SecondActivity.class));
    }

    public class OpenDrawerReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            Log.e("MainActivity", "onReceive");
            //接收到 B 頁面的廣播之后侣滩,打開抽屜
            drawer.openDrawer(GravityCompat.START);
        }
    }
}

B 頁面的代碼

public class SecondActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);
    }

    //onClick 方法
    //發(fā)送一個(gè)打開抽屜的廣播
    public void openDrawer(View view) {
        Log.e("MainActivity", "sendBroadcast");
        Intent intent = new Intent("open_drawer");
        sendBroadcast(intent);
    }
}

運(yùn)行結(jié)果

當(dāng)我在 B 頁面點(diǎn)擊按鈕發(fā)送廣播的時(shí)候,Logcat 的打印結(jié)果是這樣的变擒,可以發(fā)現(xiàn)君珠,A 頁面收到了廣播,也調(diào)用了 openDrawer 方法娇斑,但是并沒有觸發(fā) onDrawerOpened 的回調(diào)


image

這個(gè)時(shí)候我點(diǎn)擊返回鍵策添,回到 A 頁面,發(fā)現(xiàn) DrawerLayout 已經(jīng)打開毫缆,并且打印了 onDrawerOpened 日志

image

從表現(xiàn)上看當(dāng) DrawerLayout 被覆蓋的時(shí)候唯竹,并不會觸發(fā) onDrawerOpened 回調(diào),當(dāng)頁面重新可見的時(shí)候才會觸發(fā)苦丁,接下來從源碼里來看看為什么

逆向查看 onDrawerOpened 的調(diào)用鏈

既然 onDrawerOpened 回調(diào)沒有被觸發(fā)浸颓,那我們就看看 onDrawerOpened 的調(diào)用鏈:

SimpleDrawerListener

public abstract static class SimpleDrawerListener implements DrawerListener {
        @Override
        public void onDrawerSlide(View drawerView, float slideOffset) {
        }

        @Override
        public void onDrawerOpened(View drawerView) {
        }

        @Override
        public void onDrawerClosed(View drawerView) {
        }

        @Override
        public void onDrawerStateChanged(int newState) {
        }
    }

我實(shí)現(xiàn)的是 SimpleDrawerListener 這個(gè)抽象類,并且復(fù)寫了 onDrawerOpened 這個(gè)方法

dispatchOnDrawerOpened

通過 find usage 可以發(fā)現(xiàn)旺拉,onDrawerOpened 方法會在 dispatchOnDrawerOpened 方法中被調(diào)用

// 省略部分代碼
 void dispatchOnDrawerOpened(View drawerView) {
        final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams();
        if ((lp.openState & LayoutParams.FLAG_IS_OPENED) == 0) {
            lp.openState = LayoutParams.FLAG_IS_OPENED;
            if (mListeners != null) {
                int listenerCount = mListeners.size();
                for (int i = listenerCount - 1; i >= 0; i--) {
                    mListeners.get(i).onDrawerOpened(drawerView);
                }
            }
        }
    }

可以發(fā)現(xiàn)如果當(dāng)前 openState 不包含打開狀態(tài)产上,并且 DrawerListener 列表不為空,就會循環(huán)取出列表中的 DrawerListener蛾狗,并調(diào)用 onDrawerOpened 方法

updateDrawerState

繼續(xù)通過 find usage 發(fā)現(xiàn) dispatchOnDrawerOpened 方法會在 updateDrawerState 內(nèi)部被調(diào)用:

// 同樣省略部分代碼
void updateDrawerState(int forGravity, @State int activeState, View activeDrawer) {
        if (activeDrawer != null && activeState == STATE_IDLE) {
            final LayoutParams lp = (LayoutParams) activeDrawer.getLayoutParams();
            if (lp.onScreen == 0) {
                dispatchOnDrawerClosed(activeDrawer);
            } else if (lp.onScreen == 1) {
                dispatchOnDrawerOpened(activeDrawer);
            }
        }
    }

可以看到當(dāng) activeState == STATE_IDLE蒂秘,也就是 DrawerLayout 被置為閑置的時(shí)候,會觸發(fā)這個(gè)回調(diào)淘太。

因此我們繼續(xù)看 updateDrawerState 方法被調(diào)用(方法 activeState 參數(shù)值是 STATE_IDLE)的地方

ViewDragCallback#onViewDragStateChanged

updateDrawerState 方法在三處被調(diào)用,其中兩處根據(jù)調(diào)用邏輯不會被觸發(fā),因此我們只需要關(guān)注最后一處調(diào)用地方

 private class ViewDragCallback extends ViewDragHelper.Callback {
    //省略其他方法實(shí)現(xiàn)
    @Override
    public void onViewDragStateChanged(int state) {
        updateDrawerState(mAbsGravity, state,mDragger.getCapturedView());
    }
 }

updateDrawerState 方法會在 ViewDragCallback 類中的 onViewDragStateChanged 方法內(nèi)被調(diào)用蒲牧,state 參數(shù)也同時(shí)由該方法指定撇贺,接下來我們關(guān)心 onViewDragStateChanged 回調(diào)函數(shù)的觸發(fā)時(shí)機(jī)

ViewDragHelper#setDragState

onViewDragStateChanged 回調(diào)函數(shù)由 ViewDragHelper 內(nèi)部的 setDragState(int state) 方法觸發(fā),詳見??第五行

void setDragState(int state) {
    mParentView.removeCallbacks(mSetIdleRunnable);
    if (mDragState != state) {
        mDragState = state;
        mCallback.onViewDragStateChanged(state);
        if (mDragState == STATE_IDLE) {
            mCapturedView = null;
        }
    }
}

按照上述思路冰抢,我只需要去查找 setDragState(STATE_IDLE); 這個(gè)代碼調(diào)的地方就行松嘶,但是調(diào)用這行代碼的地方有 5 處,這個(gè)時(shí)候我決定再從打開 DrawerLayout 的地方挎扰,正向的再來看看代碼的調(diào)用鏈

正向查看 openDrawer 的調(diào)用鏈

A 頁面在收到廣播之后翠订,會調(diào)用 drawer.openDrawer(GravityCompat.START); 方法來打開 DrawerLayout

//1.
public void openDrawer(@EdgeGravity int gravity) {
    openDrawer(gravity, true);
}

//2.
public void openDrawer(@EdgeGravity int gravity, boolean animate){
    final View drawerView = findDrawerWithGravity(gravity);
    if (drawerView == null) {
        throw new IllegalArgumentException("No drawer view found with gravity "+ gravityToString(gravity));
    }
    openDrawer(drawerView, animate);
}
//3.
public void openDrawer(View drawerView, boolean animate) {
    //省略...
    final LayoutParams lp = (LayoutParams)drawerView.getLayoutParams();
    if (mFirstLayout) {
        lp.onScreen = 1.f;
        lp.openState = LayoutParams.FLAG_IS_OPENED;

        updateChildrenImportantForAccessibility(drawerView, true);
    } else if (animate) {
        lp.openState |= LayoutParams.FLAG_IS_OPENING;

        if (checkDrawerViewAbsoluteGravity(drawerView,Gravity.LEFT)) {
            mLeftDragger.smoothSlideViewTo(drawerView, 0,drawerView.getTop());
        } else {
            mRightDragger.smoothSlideViewTo(drawerView, getWidth() - drawerView.getWidth(),
                    drawerView.getTop());
        }
    } else {
        moveDrawerToOffset(drawerView, 1.f);
        updateDrawerState(lp.gravity, STATE_IDLE, drawerView);
        drawerView.setVisibility(VISIBLE);
    }
    invalidate();
}

通過調(diào)用鏈可以發(fā)現(xiàn)

  1. animate 參數(shù)值為 true
  2. openState 被標(biāo)記為 FLAG_IS_OPENING 狀態(tài)
  3. 執(zhí)行 ViewDragHelper 的 smoothSlideViewTo 方法
  4. 觸發(fā) invalidate

ViewDragHelper#smoothSlideViewTo

讓我們來看看 smoothSlideViewTo 的內(nèi)部邏輯:

public boolean smoothSlideViewTo(View child, int finalLeft, int finalTop) {
    //省略...
    boolean continueSliding = forceSettleCapturedViewAt(finalLeft, finalTop, 0, 0);
    //省略...
    return continueSliding;
}

這里我們先不關(guān)心這個(gè) boolean 類型的返回值,先來看看內(nèi)部的 forceSettleCapturedViewAt 方法實(shí)現(xiàn)

forceSettleCapturedViewAt

private boolean forceSettleCapturedViewAt(int finalLeft, int finalTop, int xvel, int yvel) {
        // 省略...
        final int duration = computeSettleDuration(mCapturedView, dx, dy, xvel, yvel);
        mScroller.startScroll(startLeft, startTop, dx, dy, duration);

        setDragState(STATE_SETTLING);
        return true;
    }

在這個(gè)方法內(nèi)遵倦,做了兩件事

  1. 調(diào)用 Scroller 的 startScroll 方法進(jìn)行滑動
  2. 將 DrawerLayout 置為 STATE_SETTLING 狀態(tài)

Scroller 的作用

整個(gè)正向調(diào)用鏈和逆向調(diào)用鏈都已經(jīng)分析完了尽超,但是好像沒有串聯(lián)起來,最關(guān)鍵的代碼 setDragState(STATE_IDLE);我們并沒有在正向調(diào)用鏈中的分析中看到調(diào)用的地方

如果你也有這個(gè)疑問請先看一下郭神這篇文章梧躺,介紹 Scroller 原理的文章 https://blog.csdn.net/guolin_blog/article/details/48719871

這個(gè)時(shí)候在看上文正向調(diào)用鏈中似谁,在 openDrawer 方法中我們最終調(diào)用 startScroll 方法之后,調(diào)用 invalidate 方法觸發(fā) DrawerLayout 的重繪掠哥,在重繪的過程中又會調(diào)用到 computeScroll 方法

DrawerLayout#computeScroll

@Override
public void computeScroll() {
    //省略...
    boolean leftDraggerSettling = mLeftDragger.continueSettling(true);
    boolean rightDraggerSettling = mRightDragger.continueSettling(true);
    if (leftDraggerSettling || rightDraggerSettling) {
        ViewCompat.postInvalidateOnAnimation(this);
    }
}

這端代碼的意思是巩踏,Left 和 Right 兩個(gè) ViewDragHelper 只要有一個(gè)處于 STATE_SETTLING 狀態(tài),就會繼續(xù)重繪续搀,緊接著又會觸發(fā) computeScroll 方法的調(diào)用塞琼,那么什么時(shí)候會停止這個(gè)無限的調(diào)用呢?只要上述兩個(gè) boolean 全為 false 即可

因?yàn)槲覀兊?DrawerLayout 是從左側(cè)打開禁舷,因此 rightDraggerSettling 這個(gè)值始終為 false彪杉,我們只需要關(guān)心 mLeftDragger.continueSettling(true); 這行代碼即可

ViewDragHelper#continueSettling

public boolean continueSettling(boolean deferCallbacks) {
    if (mDragState == STATE_SETTLING) {
        boolean keepGoing = mScroller.computeScrollOffset();
        if (!keepGoing) {
            if (deferCallbacks) {
                mParentView.post(mSetIdleRunnable);
            } else {
                setDragState(STATE_IDLE);
            }
        }
    }

    return mDragState == STATE_SETTLING;
}
  1. 通過 mScroller.computeScrollOffset() 方法來判斷 DrawerLayout 是否需要繼續(xù)滑動
  2. deferCallbacks 通過調(diào)用鏈可知一直未 true
  3. 當(dāng) DrawerLayout 不再繼續(xù)滑動的時(shí)候會 post 一個(gè) Runnable 對象
private final Runnable mSetIdleRunnable = new Runnable() {
    @Override
    public void run() {
        setDragState(STATE_IDLE);
    }
};

可以看見這個(gè) Runnable 對象的 run 方法會調(diào)用我們一直在尋找的 setDragState(STATE_IDLE); 這樣整個(gè)調(diào)用鏈就形成了一個(gè)閉環(huán)

解答

文章內(nèi)容僅從遇到的單一場景出發(fā),來分析 onDrawerOpened 回調(diào)的執(zhí)行時(shí)機(jī)及其調(diào)用鏈榛了,并不是 DrawerLayout 和 ViewDragHelper 的原理分析在讶,因此在分析調(diào)用的時(shí)候,很多分支邏輯沒有展開霜大,僅關(guān)心當(dāng)前場景所涉及的調(diào)用鏈

我們現(xiàn)在已經(jīng)清楚整個(gè)調(diào)用鏈了构哺,DrawerLayout 內(nèi)部滑動本質(zhì)上通過 Scroller 來實(shí)現(xiàn),通過不斷的重繪战坤,計(jì)算位移曙强,滑動,重繪... 這個(gè)一個(gè)流程來完成 DrawerLayout 的滑動

那為什么會出現(xiàn)最開始我們調(diào)用了 openDrawer 方法之后途茫,并沒有收到打開的回調(diào)碟嘴,而是在 B 頁面銷毀后才收到呢?

答:這是因?yàn)樵?B 頁面打開的時(shí)候,A 頁面的 DrawerLayout 并沒有進(jìn)行繪制囊卜,因此也就無法觸發(fā)上述的循環(huán)娜扇,直到 A 頁面重新可見后才會執(zhí)行上述流程错沃,最終收到回調(diào)
[1]: http://static.zybuluo.com/xiezhen/7am43j2i7mq8pl6j57t79ymh/send_open_drawer.png
[2]: http://static.zybuluo.com/xiezhen/hit0x1aqd1kend1fw2wrz47w/close_second_activity.png

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市雀瓢,隨后出現(xiàn)的幾起案子枢析,更是在濱河造成了極大的恐慌,老刑警劉巖刃麸,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件醒叁,死亡現(xiàn)場離奇詭異,居然都是意外死亡泊业,警方通過查閱死者的電腦和手機(jī)把沼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來吁伺,“玉大人饮睬,你說我怎么就攤上這事∠潋穑” “怎么了续捂?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長宦搬。 經(jīng)常有香客問我牙瓢,道長,這世上最難降的妖魔是什么间校? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任矾克,我火速辦了婚禮,結(jié)果婚禮上憔足,老公的妹妹穿的比我還像新娘胁附。我一直安慰自己,他們只是感情好滓彰,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布控妻。 她就那樣靜靜地躺著,像睡著了一般揭绑。 火紅的嫁衣襯著肌膚如雪弓候。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天他匪,我揣著相機(jī)與錄音菇存,去河邊找鬼。 笑死邦蜜,一個(gè)胖子當(dāng)著我的面吹牛依鸥,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播悼沈,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼贱迟,長吁一口氣:“原來是場噩夢啊……” “哼姐扮!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起关筒,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤溶握,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后蒸播,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡萍肆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年袍榆,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片塘揣。...
    茶點(diǎn)故事閱讀 40,030評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡包雀,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出亲铡,到底是詐尸還是另有隱情才写,我是刑警寧澤,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布奖蔓,位于F島的核電站赞草,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏吆鹤。R本人自食惡果不足惜厨疙,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望疑务。 院中可真熱鬧沾凄,春花似錦、人聲如沸知允。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽温鸽。三九已至保屯,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間嗤朴,已是汗流浹背配椭。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留雹姊,地道東北人股缸。 一個(gè)月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像吱雏,于是被迫代替她去往敵國和親敦姻。 傳聞我的和親對象是個(gè)殘疾皇子瘾境,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評論 2 355

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

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,162評論 25 707
  • 滑動返回是ios設(shè)備中默認(rèn)支持的一種滑動退出效果,由于IPhone設(shè)備沒有返回鍵镰惦,所以滑動退出使用起來十分方便迷守。而...
    健叔閱讀 7,536評論 2 18
  • 年年月月里 相互取暖 假裝寒暄 有誰記得 這世界你曾經(jīng)來過 人來人往 是浮華時(shí)代的集體失憶
    留子堯閱讀 199評論 0 2
  • 子曰:“不仁者不可以久處約,不可以長處樂旺入。仁者安仁兑凿,知者利仁∫瘃”意思是礼华,孔子說:“一個(gè)沒有道德修養(yǎng)的人,不能長久過...
    文豆米閱讀 1,769評論 0 1
  • dsafasd
    Bric閱讀 148評論 0 1