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) 方法触发,详见第五行

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();
}

通过调用链可以发现

  1. animate 参数值为 true
  2. openState 被标记为 FLAG_IS_OPENING 状态
  3. 执行 ViewDragHelper 的 smoothSlideViewTo 方法
  4. 触发 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;
    }

在这个方法内,做了两件事

  1. 调用 Scroller 的 startScroll 方法进行滑动
  2. 将 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;
}
  1. 通过 mScroller.computeScrollOffset() 方法来判断 DrawerLayout 是否需要继续滑动
  2. deferCallbacks 通过调用链可知一直未 true
  3. 当 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

你可能感兴趣的:(DrawerLayout onDrawerOpened 响应时机)