遇到问题的场景
简要说明一下我的使用场景,现在有两个页面 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 的回调
这个时候我点击返回键,回到 A 页面,发现 DrawerLayout 已经打开,并且打印了 onDrawerOpened 日志
从表现上看当 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) 方法触发,详见👇第五行
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 原理的文章 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