遇到問題的場景
簡要說明一下我的使用場景,現(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)
這個(gè)時(shí)候我點(diǎn)擊返回鍵策添,回到 A 頁面,發(fā)現(xiàn) DrawerLayout 已經(jīng)打開毫缆,并且打印了 onDrawerOpened 日志
從表現(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)
- animate 參數(shù)值為 true
- openState 被標(biāo)記為 FLAG_IS_OPENING 狀態(tài)
- 執(zhí)行 ViewDragHelper 的 smoothSlideViewTo 方法
- 觸發(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)遵倦,做了兩件事
- 調(diào)用 Scroller 的 startScroll 方法進(jìn)行滑動
- 將 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;
}
- 通過 mScroller.computeScrollOffset() 方法來判斷 DrawerLayout 是否需要繼續(xù)滑動
- deferCallbacks 通過調(diào)用鏈可知一直未 true
- 當(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