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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,409评论 25 707
  • 滑动返回是ios设备中默认支持的一种滑动退出效果,由于IPhone设备没有返回键,所以滑动退出使用起来十分方便。而...
    健叔阅读 7,426评论 2 18
  • 年年月月里 相互取暖 假装寒暄 有谁记得 这世界你曾经来过 人来人往 是浮华时代的集体失忆
    留子尧阅读 195评论 0 2
  • 子曰:“不仁者不可以久处约,不可以长处乐。仁者安仁,知者利仁。”意思是,孔子说:“一个没有道德修养的人,不能长久过...
    文豆米阅读 1,733评论 0 1
  • dsafasd
    Bric阅读 146评论 0 1