緣起
我對 Android 的狀態(tài)欄和導(dǎo)航欄一直有種情結(jié),在我做 Android 開發(fā)之前,我就喜歡通過一些 Xposed 插件來讓狀態(tài)欄和導(dǎo)航欄變色或者透明,以消除那丑丑的兩個黑條。
從 fitsSystemWindows 方法說起,View 里面有個方法,叫setFitsSystemWindows,這個方法有什么用呢,當(dāng)給 Activity 設(shè)置全屏的時候,如果給 contentView 的最外層設(shè)置 setFitsSystemWindows(true),那么 contentView 就不會侵入狀態(tài)欄和導(dǎo)航欄,否則,就會侵入。什么意思呢?舉個例子,在 Android 5.0 以上,如果在 Activity 中做以下操作
int flag = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
? ? ? ? | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
? ? ? ? | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
getWindow().getDecorView().setSystemUiVisibility(flag);
getWindow().setStatusBarColor(Color.parseColor("#66000000"));
getWindow().setNavigationBarColor(Color.parseColor("#66000000"));
就會看到這樣的效果

如果再在后面加上
findViewById(R.id.contentView).setFitsSystemWindows(true);
效果就會變成這樣

以上兩個圖已經(jīng)很形象的說明了侵入和非侵入的區(qū)別?,F(xiàn)在來解釋一下為什么狀態(tài)欄和導(dǎo)航欄設(shè)置會耦合,從上面的兩個圖可以看出setFitsSystemWindows(true)方法是對狀態(tài)欄和導(dǎo)航欄同時生效的,就是說要么都侵入,要么都不侵入,那么問題來了,如果我現(xiàn)在要求布局侵入到狀態(tài)欄而不侵入到導(dǎo)航欄要怎么辦呢,其實也是可以實現(xiàn)的,只需要在上面的代碼中把View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION去掉,即
int flag = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
? ? ? ? | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
getWindow().getDecorView().setSystemUiVisibility(flag);
getWindow().setStatusBarColor(Color.parseColor("#66000000"));
findViewById(R.id.contentView).setFitsSystemWindows(false);
這樣就能達(dá)到只設(shè)置狀態(tài)欄,而導(dǎo)航欄不受影響的效果,同樣的,如果需要讓布局只侵入導(dǎo)航欄而狀態(tài)欄不受影響,只需要
int flag = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
? ? ? ? | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
getWindow().getDecorView().setSystemUiVisibility(flag);
getWindow().setNavigationBarColor(Color.parseColor("#66000000"));
findViewById(R.id.contentView).setFitsSystemWindows(false);
這樣看來好像并沒有什么問題,各種情況都能解決,但是,如果現(xiàn)在需求是這樣的,首先剛進入 Activity 的時候,讓布局只侵入到導(dǎo)航欄,然后當(dāng)點擊某個按鈕的時候,導(dǎo)航欄保持被侵入不變,同時,讓布局侵入到狀態(tài)欄,這時候如果還用上面的方法,就會導(dǎo)致導(dǎo)航欄被還原成初始狀態(tài),如圖所示

這是因為在多次調(diào)用 setSystemUiVisibility 方法時,只有最后一次才是有效的,前面的調(diào)用都會被最后一次覆蓋掉,那要怎么解決這個問題呢,有種方法就是保存當(dāng)前 Activity 的狀態(tài)欄和導(dǎo)航欄的狀態(tài),當(dāng)每次需要設(shè)置的時候,都根據(jù)保存的狀態(tài)重新設(shè)置 setSystemUiVisibility 的參數(shù),保證之前設(shè)置過的效果不受影響,但是這樣操作起來太過繁瑣,我選擇了另一種很簡單的方法。
UltimateBarX 的核心原理
其實方法很簡單,當(dāng)?shù)谝淮卧O(shè)置狀態(tài)欄或?qū)Ш綑诘臅r候,不管需要什么效果,都讓布局侵入到狀態(tài)欄和導(dǎo)航欄,然后根據(jù)要不要侵入來設(shè)置decorView 的 topPadding 和 bottomPadding,比如上面提到的需求,就可以在剛進入 Activity 的時候,就讓布局同時侵入到狀態(tài)欄和導(dǎo)航欄,然后給decorView 設(shè)置一個狀態(tài)欄高度的 topPadding,看起來效果就是布局沒有侵入到狀態(tài)欄了,當(dāng)點擊按鈕需要讓狀態(tài)欄被侵入的時候,只需再把decorView 的 topPadding 設(shè)為0,而不用再管導(dǎo)航欄,也不用保存導(dǎo)航欄之前設(shè)置的狀態(tài)了,簡單粗暴。
UltimateBarX 的用法
UltimateBarX 的用法也很簡單,這次我花了很長的時間思考怎樣讓方法調(diào)用起來更簡單,同時日后維護起來更方便,首先在 build.gradle 中添加
dependencies {
? ? implementation 'com.zackratos.ultimatebarx:ultimatebarx:0.1.1'
}
如果需要設(shè)置狀態(tài)了,可以在 Activity 中
UltimateBarX.create(UltimateBarX.STATUS_BAR)? ? ? ? // 設(shè)置狀態(tài)欄
? ? .fitWindow(true)? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? // 布局是否侵入狀態(tài)欄(true 不侵入,false 侵入)
? ? .bgColor(Color.BLACK)? ? ? ? ? ? ? ? ? ? ? ? ? // 狀態(tài)欄背景顏色(色值)
? ? .bgColorRes(R.color.deepSkyBlue)? ? ? ? ? ? ? ? // 狀態(tài)欄背景顏色(資源id)
? ? .bgRes(R.drawable.bg_gradient)? ? ? ? ? ? ? ? ? // 狀態(tài)欄背景 drawable
? ? .light(false)? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? // light模式(狀態(tài)欄字體灰色 Android 6.0 以上支持)
? ? .apply(this);
方法非常簡單,注釋也寫的很清楚了,這里有三個設(shè)置背景的方法,寫一個就行了,多寫也只有一個會生效,優(yōu)先級 bgRes > bgColor > bgColorRes,如果需要設(shè)置導(dǎo)航欄,在 create 里面?zhèn)魅?UltimateBarX.NAVIGATION_BAR即可,其他不變,狀態(tài)欄和導(dǎo)航欄完全獨立設(shè)置,互不影響,做到了真正的解耦。
如果要實現(xiàn)上面所說的需求,只需要在剛進入 Activity 的時候
UltimateBarX.create(UltimateBarX.NAVIGATION_BAR)
? ? .fitWindow(false)
? ? .bgColor(Color.parseColor("#66000000"))
? ? .apply(this);
設(shè)置布局侵入到導(dǎo)航欄,然后點擊按鈕,讓布局侵入到狀態(tài)欄,只需
UltimateBarX.create(UltimateBarX.STATUS_BAR)
? ? .fitWindow(false)
? ? .bgColor(Color.parseColor("#66000000"))
? ? .apply(this);
關(guān)于 light 方法
這里面有個 light 方法,用來設(shè)置狀態(tài)欄或者導(dǎo)航欄的 light 模式,當(dāng) light 模式為 true 時,狀態(tài)欄的字體會變灰,對應(yīng)的導(dǎo)航欄的按鈕會變灰,這里有個問題,就是設(shè)置 light 模式也需要調(diào)用 decorView 的setSystemUiVisibility 方法,這就意味著 light 模式是需要保存狀態(tài)的,如果不保存,第一次設(shè)置狀態(tài)欄為 light 模式,第二次再設(shè)置導(dǎo)航欄的時候,狀態(tài)欄的 light 模式就會被清除,這個前面已經(jīng)解釋過了。
那如何保存狀態(tài)呢,為了使侵入性更低,可以用一個單例對象來保存每個 Activity 的狀態(tài)欄和導(dǎo)航欄的 light 狀態(tài),這樣又會有一個問題,在 Activity 退出的時候,必須把當(dāng)前 Activity 對象從單例里面移除,否則會造成內(nèi)存泄漏,那么怎樣監(jiān)聽 Activity 退出呢,常用的做法就是在 Activity 里面添加一個看不見的 Fragment,當(dāng) Fragment 的 onDestroy 方法被調(diào)用的時候,說明 Activity 的 onDestroy 被調(diào)用了,即 Activity 已退出。
不過現(xiàn)在已經(jīng)不用這么麻煩了,Google 爸爸在 JetPack 組件里面,給我們提供了非常好用的工具「Lifecycle」,可以無侵入的監(jiān)聽 Activity 和 Fragment 的生命周期,這里就使用 Lifecycle 來實現(xiàn)的,非常方便,其實 Lifecycle 的實現(xiàn)原理也是在內(nèi)部添加一個 Fragment 來監(jiān)聽的。
最后
一點題外話:
我們有《Android學(xué)習(xí)、面試;文檔、視頻資源免費獲取》,可復(fù)制鏈接后用石墨文檔 App 或小程序打開鏈接或者私信我資料領(lǐng)取。
https://shimo.im/docs/TG8PDh9D96WGTT8W