DrawerLayout onDrawerOpened 響應(yīng)時(shí)機(jī)


遇到問題的場(chǎng)景

簡(jiǎn)要說明一下我的使用場(chǎng)景,現(xiàn)在有兩個(gè)頁面 A 和 B,由 A 頁面 startActivity 啟動(dòng) 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");
        //注冊(cè) 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)


image

這個(gè)時(shí)候我點(diǎn)擊返回鍵,回到 A 頁面,發(fā)現(xiàn) DrawerLayout 已經(jīng)打開,并且打印了 onDrawerOpened 日志

image

從表現(xiàn)上看當(dāng) DrawerLayout 被覆蓋的時(shí)候,并不會(huì)觸發(fā) onDrawerOpened 回調(diào),當(dāng)頁面重新可見的時(shí)候才會(huì)觸發(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 方法會(huì)在 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ì)循環(huán)取出列表中的 DrawerListener,并調(diào)用 onDrawerOpened 方法

updateDrawerState

繼續(xù)通過 find usage 發(fā)現(xiàn) dispatchOnDrawerOpened 方法會(huì)在 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í)候,會(huì)觸發(fā)這個(gè)回調(diào)。

因此我們繼續(xù)看 updateDrawerState 方法被調(diào)用(方法 activeState 參數(shù)值是 STATE_IDLE)的地方

ViewDragCallback#onViewDragStateChanged

updateDrawerState 方法在三處被調(diào)用,其中兩處根據(jù)調(diào)用邏輯不會(huì)被觸發(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 方法會(huì)在 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 頁面在收到廣播之后,會(huì)調(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)

  1. animate 參數(shù)值為 true
  2. openState 被標(biāo)記為 FLAG_IS_OPENING 狀態(tài)
  3. 執(zhí)行 ViewDragHelper 的 smoothSlideViewTo 方法
  4. 觸發(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),做了兩件事

  1. 調(diào)用 Scroller 的 startScroll 方法進(jìn)行滑動(dòng)
  2. 將 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è)疑問請(qǐng)先看一下郭神這篇文章,介紹 Scroller 原理的文章 https://blog.csdn.net/guolin_blog/article/details/48719871

這個(gè)時(shí)候在看上文正向調(diào)用鏈中,在 openDrawer 方法中我們最終調(diào)用 startScroll 方法之后,調(diào)用 invalidate 方法觸發(fā) DrawerLayout 的重繪,在重繪的過程中又會(huì)調(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),就會(huì)繼續(xù)重繪,緊接著又會(huì)觸發(fā) computeScroll 方法的調(diào)用,那么什么時(shí)候會(huì)停止這個(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;
}
  1. 通過 mScroller.computeScrollOffset() 方法來判斷 DrawerLayout 是否需要繼續(xù)滑動(dòng)
  2. deferCallbacks 通過調(diào)用鏈可知一直未 true
  3. 當(dāng) DrawerLayout 不再繼續(xù)滑動(dòng)的時(shí)候會(huì) post 一個(gè) Runnable 對(duì)象
private final Runnable mSetIdleRunnable = new Runnable() {
    @Override
    public void run() {
        setDragState(STATE_IDLE);
    }
};

可以看見這個(gè) Runnable 對(duì)象的 run 方法會(huì)調(diào)用我們一直在尋找的 setDragState(STATE_IDLE); 這樣整個(gè)調(diào)用鏈就形成了一個(gè)閉環(huán)

解答

文章內(nèi)容僅從遇到的單一場(chǎng)景出發(fā),來分析 onDrawerOpened 回調(diào)的執(zhí)行時(shí)機(jī)及其調(diào)用鏈,并不是 DrawerLayout 和 ViewDragHelper 的原理分析,因此在分析調(diào)用的時(shí)候,很多分支邏輯沒有展開,僅關(guān)心當(dāng)前場(chǎng)景所涉及的調(diào)用鏈

我們現(xiàn)在已經(jīng)清楚整個(gè)調(diào)用鏈了,DrawerLayout 內(nèi)部滑動(dòng)本質(zhì)上通過 Scroller 來實(shí)現(xiàn),通過不斷的重繪,計(jì)算位移,滑動(dòng),重繪... 這個(gè)一個(gè)流程來完成 DrawerLayout 的滑動(dòng)

那為什么會(huì)出現(xiàn)最開始我們調(diào)用了 openDrawer 方法之后,并沒有收到打開的回調(diào),而是在 B 頁面銷毀后才收到呢?

答:這是因?yàn)樵?B 頁面打開的時(shí)候,A 頁面的 DrawerLayout 并沒有進(jìn)行繪制,因此也就無法觸發(fā)上述的循環(huán),直到 A 頁面重新可見后才會(huì)執(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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,283評(píng)論 25 708
  • 滑動(dòng)返回是ios設(shè)備中默認(rèn)支持的一種滑動(dòng)退出效果,由于IPhone設(shè)備沒有返回鍵,所以滑動(dòng)退出使用起來十分方便。而...
    健叔閱讀 7,939評(píng)論 2 18
  • 年年月月里 相互取暖 假裝寒暄 有誰記得 這世界你曾經(jīng)來過 人來人往 是浮華時(shí)代的集體失憶
    留子堯閱讀 224評(píng)論 0 2
  • 子曰:“不仁者不可以久處約,不可以長(zhǎng)處樂。仁者安仁,知者利仁。”意思是,孔子說:“一個(gè)沒有道德修養(yǎng)的人,不能長(zhǎng)久過...
    文豆米閱讀 2,034評(píng)論 0 1
  • dsafasd
    Bric閱讀 167評(píng)論 0 1

友情鏈接更多精彩內(nèi)容