DrawerLayout onDrawerOpened 響應時機
遇到問題的場景
簡要說明一下我的使用場景,現在有兩個頁面 A 和 B,由 A 頁面 startActivity 啟動 B 頁面。A 頁面的根佈局是 DrawerLayout ,B 頁面有個按鈕用來發送廣播,A 頁面接收到 B 頁面傳送的廣播之後,呼叫 DrawerLayout 的 openDrawer 方法開啟抽屜,然後在 void onDrawerOpened(View drawerView) 回撥方法中列印日誌。
A 頁面程式碼
我省略了一些模板程式碼,只保留了關鍵程式碼
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 新增一個回撥方法 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); } //... // 跳轉到 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 方法 //傳送一個開啟抽屜的廣播 public void openDrawer(View view) { Log.e("MainActivity", "sendBroadcast"); Intent intent = new Intent("open_drawer"); sendBroadcast(intent); } }
執行結果
當我在 B 頁面點選按鈕傳送廣播的時候,Logcat 的列印結果是這樣的,可以發現,A 頁面收到了廣播,也呼叫了 openDrawer 方法,但是並沒有觸發 onDrawerOpened 的回撥

image
這個時候我點選返回鍵,回到 A 頁面,發現 DrawerLayout 已經開啟,並且列印了 onDrawerOpened 日誌

image
從表現上看當 DrawerLayout 被覆蓋的時候,並不會觸發 onDrawerOpened 回撥,當頁面重新可見的時候才會觸發,接下來從原始碼裡來看看為什麼
逆向檢視 onDrawerOpened 的呼叫鏈
既然 onDrawerOpened 回撥沒有被觸發,那我們就看看 onDrawerOpened 的呼叫鏈:
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) { } }
我實現的是 SimpleDrawerListener 這個抽象類,並且複寫了 onDrawerOpened 這個方法
dispatchOnDrawerOpened
通過 find usage 可以發現,onDrawerOpened 方法會在 dispatchOnDrawerOpened 方法中被呼叫
// 省略部分程式碼 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); } } } }
可以發現如果當前 openState 不包含開啟狀態,並且 DrawerListener 列表不為空,就會迴圈取出列表中的 DrawerListener,並呼叫 onDrawerOpened 方法
updateDrawerState
繼續通過 find usage 發現 dispatchOnDrawerOpened 方法會在 updateDrawerState 內部被呼叫:
// 同樣省略部分程式碼 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); } } }
可以看到當 activeState == STATE_IDLE,也就是 DrawerLayout 被置為閒置的時候,會觸發這個回撥。
因此我們繼續看 updateDrawerState 方法被呼叫(方法 activeState 引數值是 STATE_IDLE)的地方
ViewDragCallback#onViewDragStateChanged
updateDrawerState 方法在三處被呼叫,其中兩處根據呼叫邏輯不會被觸發,因此我們只需要關注最後一處呼叫地方
private class ViewDragCallback extends ViewDragHelper.Callback { //省略其他方法實現 @Override public void onViewDragStateChanged(int state) { updateDrawerState(mAbsGravity, state,mDragger.getCapturedView()); } }
updateDrawerState 方法會在 ViewDragCallback 類中的 onViewDragStateChanged 方法內被呼叫,state 引數也同時由該方法指定,接下來我們關心 onViewDragStateChanged 回撥函式的觸發時機
ViewDragHelper#setDragState
onViewDragStateChanged 回撥函式由 ViewDragHelper 內部的 setDragState(int state) 方法觸發,詳見:point_down:第五行
void setDragState(int state) { mParentView.removeCallbacks(mSetIdleRunnable); if (mDragState != state) { mDragState = state; mCallback.onViewDragStateChanged(state); if (mDragState == STATE_IDLE) { mCapturedView = null; } } }
按照上述思路,我只需要去查詢 setDragState(STATE_IDLE);
這個程式碼調的地方就行,但是呼叫這行程式碼的地方有 5 處,這個時候我決定再從開啟 DrawerLayout 的地方,正向的再來看看程式碼的呼叫鏈
正向檢視 openDrawer 的呼叫鏈
A 頁面在收到廣播之後,會呼叫 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(); }
通過呼叫鏈可以發現
- animate 引數值為 true
- openState 被標記為 FLAG_IS_OPENING 狀態
- 執行 ViewDragHelper 的 smoothSlideViewTo 方法
- 觸發 invalidate
ViewDragHelper#smoothSlideViewTo
讓我們來看看 smoothSlideViewTo 的內部邏輯:
public boolean smoothSlideViewTo(View child, int finalLeft, int finalTop) { //省略... boolean continueSliding = forceSettleCapturedViewAt(finalLeft, finalTop, 0, 0); //省略... return continueSliding; }
這裡我們先不關心這個 boolean 型別的返回值,先來看看內部的 forceSettleCapturedViewAt 方法實現
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; }
在這個方法內,做了兩件事
- 呼叫 Scroller 的 startScroll 方法進行滑動
- 將 DrawerLayout 置為 STATE_SETTLING 狀態
Scroller 的作用
整個正向呼叫鏈和逆向呼叫鏈都已經分析完了,但是好像沒有串聯起來,最關鍵的程式碼 setDragState(STATE_IDLE);
我們並沒有在正向呼叫鏈中的分析中看到呼叫的地方
如果你也有這個疑問請先看一下郭神這篇文章,介紹 Scroller 原理的文章 ofollow,noindex">https://blog.csdn.net/guolin_blog/article/details/48719871
這個時候在看上文正向呼叫鏈中,在 openDrawer 方法中我們最終呼叫 startScroll 方法之後,呼叫 invalidate 方法觸發 DrawerLayout 的重繪,在重繪的過程中又會呼叫到 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 兩個 ViewDragHelper 只要有一個處於 STATE_SETTLING 狀態,就會繼續重繪,緊接著又會觸發 computeScroll 方法的呼叫,那麼什麼時候會停止這個無限的呼叫呢?只要上述兩個 boolean 全為 false 即可
因為我們的 DrawerLayout 是從左側開啟,因此 rightDraggerSettling 這個值始終為 false,我們只需要關心 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; }
- 通過 mScroller.computeScrollOffset() 方法來判斷 DrawerLayout 是否需要繼續滑動
- deferCallbacks 通過呼叫鏈可知一直未 true
- 當 DrawerLayout 不再繼續滑動的時候會 post 一個 Runnable 物件
private final Runnable mSetIdleRunnable = new Runnable() { @Override public void run() { setDragState(STATE_IDLE); } };
可以看見這個 Runnable 物件的 run 方法會呼叫我們一直在尋找的 setDragState(STATE_IDLE); 這樣整個呼叫鏈就形成了一個閉環
解答
文章內容僅從遇到的單一場景出發,來分析 onDrawerOpened 回撥的執行時機及其呼叫鏈,並不是 DrawerLayout 和 ViewDragHelper 的原理分析,因此在分析呼叫的時候,很多分支邏輯沒有展開,僅關心當前場景所涉及的呼叫鏈
我們現在已經清楚整個呼叫鏈了,DrawerLayout 內部滑動本質上通過 Scroller 來實現,通過不斷的重繪,計算位移,滑動,重繪... 這個一個流程來完成 DrawerLayout 的滑動
那為什麼會出現最開始我們呼叫了 openDrawer 方法之後,並沒有收到開啟的回撥,而是在 B 頁面銷燬後才收到呢?
答:這是因為在 B 頁面開啟的時候,A 頁面的 DrawerLayout 並沒有進行繪製,因此也就無法觸發上述的迴圈,直到 A 頁面重新可見後才會執行上述流程,最終收到回撥
[1]: http://static.zybuluo.com/xiezhen/7am43j2i7mq8pl6j57t79ymh/send_open_drawer.png
[2]: http://static.zybuluo.com/xiezhen/hit0x1aqd1kend1fw2wrz47w/close_second_activity.png