美文网首页Android开发经验谈Android开发程序员
DrawerLayout onDrawerOpened 响应时机

DrawerLayout onDrawerOpened 响应时机

作者: realxz | 来源:发表于2018-11-29 10:36 被阅读36次

    遇到问题的场景

    简要说明一下我的使用场景,现在有两个页面 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 响应时机

        本文链接:https://www.haomeiwen.com/subject/srsxcqtx.html