首先請你們看幾張圖:
html
進行Android開發時,有兩種方式都會對狀態欄進行設置:Translucent Bar(透明欄)和Immersive Mode(沉浸模式)。二者的區別,比較直觀的一點,就是體如今屏幕中的View可點擊區域,以下所示。android
沉浸模式
隱藏status bar(狀態欄),使屏幕全屏,讓Activity接收全部的(整個屏幕的)觸摸事件。git
狀態欄着色
設置狀態欄顏色,狀態欄部分不接收處理觸摸事件。佈局侵入狀態欄的後面,必須啓用fitsSystemWindows
屬性來調整佈局纔不至於被狀態欄覆蓋。如透明狀態欄、與titlebar顏色一致的狀態欄,即如以前的圖所示。github
從3.x版本開始, 系統DecorView提供了setSystemUiVisibility方法, 能夠經過設置Flag更改SystemUI的屬性。各個設置的參數含義以下所示:api
參數 | api版本 | 含義 |
---|---|---|
View.SYSTEM_UI_FLAG_VISIBLE | 14 | 默認標記 |
View.SYSTEM_UI_FLAG_LOW_PROFILE | 14 | 低功耗模式, 會隱藏狀態欄圖標, 在4.0上能夠實現全屏 |
View.SYSTEM_UI_FLAG_LAYOUT_STABLE | 16 | 保持整個View穩定,常跟bar 懸浮、隱藏共用, 使View不會由於SystemUI的變化而作layout |
View.SYSTEM_UI_FLAG_FULLSCREEN | 16 | 狀態欄隱藏 |
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | 16 | 狀態欄上浮於Activity |
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | 14 | 隱藏導航欄 |
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | 16 | 導航欄上浮於Activity |
View.SYSTEM_UI_FLAG_IMMERSIVE | 19 | Kitkat新加入的Flag, 沉浸模式, 能夠隱藏掉status跟navigation bar, 而且在第一次會彈泡提醒, 它會覆蓋掉以前兩個隱藏bar的標記, 而且在bar出現的位置滑動能夠呼出bar |
View.SYSTEM_UI_FLAG_IMMERSIVE_STIKY | 19 | 與上面惟一的區別是, 呼出隱藏的bar後會自動再隱藏掉 |
綜上所述,要實現全屏沉浸模式,只需以下設置便可:bash
//Hide
getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LOW_PROFILE
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_IMMERSIVE_STIKY);複製代碼
實現沉浸效果以下圖所示。左邊是隱藏狀態欄和導航欄的效果,整個屏幕均可以接收觸摸反饋;右邊圖是滑動顯示的狀態欄和導航欄的效果,內容顯示在系統欄的下方,顯示一段時間後會自動隱藏。app
對於狀態欄着色,有兩個比較關鍵的系統節點須要關注,分別是4.4和5.0。基於兩個系統節點,咱們又能夠分紅三個階段進行討論。ide
4.4之前
狀態欄不支持設置顏色。函數
4.4 ~ 5.0
狀態欄支持透明效果,可是系統不提供接口進行顏色設置(有辦法,文章後面會介紹)。工具
5.0及以上
系統提供接口對狀態欄進行任意顏色設置。
在values、values-v1九、values-v21的style.xml都設置一個 Translucent System Bar 風格的Theme
values/style.xml
<style name="ImageTranslucentTheme" parent="AppTheme">
<!--在Android 4.4以前的版本上運行,直接跟隨系統主題-->
</style>複製代碼
values-v19/style.xml
<style name="ImageTranslucentTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:windowTranslucentStatus">true</item>
<item name="android:windowTranslucentNavigation">true</item>
</style>複製代碼
values-v21/style.xml
<style name="ImageTranslucentTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:windowTranslucentStatus">false</item>
<item name="android:windowTranslucentNavigation">true</item>
<item name="android:statusBarColor">@android:color/transparent</item>
</style>複製代碼
<activity android:name=".MainActivity"
android:theme="@style/ImageTranslucentTheme"
>
</activity>複製代碼
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@mipmap/paperscan_guide_step_two_inside"
android:fitsSystemWindows="true"
>
<TextView
android:text="透明狀態欄~"
android:textColor="@android:color/white"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>複製代碼
如此能夠獲得效果以下所示
那麼,爲何要將android:fitsSystemWindows設置爲true呢?若是不設置會怎麼樣?咱們能夠來測試下,下圖是不設置(默認爲false)的效果。可見內容跑到了狀態欄的下面,被狀態欄覆蓋了。因此,最直觀的一點咱們能夠發現,設置了android:fitsSystemWindows爲true,可讓內容不會頂到狀態欄的下面。文章Android沉浸式狀態欄實現對fitsSystemWindows
已經作了詳細的分析,你們有興趣能夠看看。
接着上面的問題,既然咱們實現透明狀態欄效果的頁面都須要設置fitsSystemWindows
屬性,因此咱們想到了一種方便的方法,在theme中加上以下的android:fitsSystemWindows設置:
<item name="android:fitsSystemWindows">true</item>複製代碼
運行發現真的能夠,顯示的內容也沒有和狀態欄重疊。可是,當咱們顯示一個toast的時候,發現問題了。
Toast.makeText(MainActivity.this,"toast sth...",Toast.LENGTH_SHORT).show();複製代碼
如圖所示,Toast打印出來的文字都向上偏移了。緣由是由於咱們是在Theme中設置的fitsSystemWindows屬性,會影響使用了該theme的activity或application的行爲,形成依附於Application Window的Window(好比Toast)錯位。針對Toast錯位的問題,解決方法也簡單,就是使用應用級別的上下文:
Toast.makeText(getApplicationContext(),"toast sth...",Toast.LENGTH_SHORT).show();複製代碼
雖然說Toast錯誤問題也是有方法能夠解決,可是若是這樣使用,不經意間會給咱們的應用埋下不少坑。因此個人建議是:不要濫用,只在有須要的地方添加fitsSystemWindows屬性。
正如前面提到的,只有4.4以上的系統才支持透明狀態欄設置,5.0以上的系統支持設置狀態欄任意顏色。因此5.0以上的系統設置狀態欄的顏色就很簡單了,直接調用系統接口就能夠了:
// 設置此flag纔可對狀態欄進行顏色設置
activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
// 取消設置透明狀態欄,否則顏色設置不生效
activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
// 設置狀態欄顏色
activity.getWindow().setStatusBarColor(color);複製代碼
而對於系統是4.4到5.0之間的機子,要設置狀態欄的顏色就稍微要繁瑣一點了。首先,須要設置頁面狀態欄爲透明;而後,新建一個和狀態欄高度一致的view,填充到DecorView上;最後,經過設置這個填充view的顏色,咱們就能實現相似對狀態欄顏色進行控制的效果了。
// 設置透明
activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
// 生成view填充DecorView
ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
View fakeStatusBarView = decorView.findViewById(FAKE_STATUS_BAR_VIEW_ID);
if (fakeStatusBarView != null) {
if (fakeStatusBarView.getVisibility() == View.GONE) {
fakeStatusBarView.setVisibility(View.VISIBLE);
}
fakeStatusBarView.setBackgroundColor(calculateStatusColor(color, statusBarAlpha));
} else {
decorView.addView(createStatusBarView(activity, color, statusBarAlpha));
}
// 設置fitsSystemWindows屬性
setRootView(activity);複製代碼
在文章的前面部分,在系統4.4~5.0的機子上,如何設置狀態欄顏色的原理其實已經作了說明。就是在透明的狀態欄下放置一個和狀態欄大小一致的view,經過更改view的顏色來實現更改狀態欄顏色的效果。那麼,當在5.0及以上的機子,當咱們經過以下代碼設置狀態欄顏色:
getWindow().setStatusBarColor(RED);複製代碼
其實,這裏調用的是PhoneWindow的setStatusBarColor方法,具體實現以下:
@Override
public void setStatusBarColor(int color) {
mStatusBarColor = color;
mForcedStatusBarColor = true;
if (mDecor != null) {
mDecor.updateColorViews(null, false /* animate */);
}
}複製代碼
最終調用的是DecorView的updateColorViews函數,DecorView是屬於Activity的PhoneWindow的內部對象,也就說,更新的對象從所謂的Window進入到了Activity自身的佈局視圖中,接着看DecorView,這裏只關注更改顏色。在方法updateColorViews中,調用了以下代碼,代碼實現的功能就是修改狀態欄顏色:
updateColorViewInt(mNavigationColorViewState, sysUiVisibility,
mWindow.mNavigationBarColor, navBarSize, navBarToRightEdge || navBarToLeftEdge,
navBarToLeftEdge,
0 /* sideInset */, animate && !disallowAnimate, false /* force */);複製代碼
這裏mStatusColorViewState其實就表明StatusBar的背景顏色對象,主要屬性包括顯示的條件以及顏色值:
private final ColorViewState mStatusColorViewState = new ColorViewState(
SYSTEM_UI_FLAG_FULLSCREEN, FLAG_TRANSLUCENT_STATUS,
Gravity.TOP,
Gravity.LEFT,
STATUS_BAR_BACKGROUND_TRANSITION_NAME,
com.android.internal.R.id.statusBarBackground,
FLAG_FULLSCREEN);複製代碼
再來看updateColorViewInt方法:
private void updateColorViewInt(final ColorViewState state, int sysUiVis, int color,
int size, boolean verticalBar, boolean seascape, int sideMargin,
boolean animate, boolean force) {
// 關鍵點1
state.present = (sysUiVis & state.systemUiHideFlag) == 0
&& (mWindow.getAttributes().flags & state.hideWindowFlag) == 0
&& ((mWindow.getAttributes().flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0
|| force);
// 關鍵點2
boolean show = state.present
&& (color & Color.BLACK) != 0
&& ((mWindow.getAttributes().flags & state.translucentFlag) == 0 || force);
boolean showView = show && !isResizing() && size > 0;
...
if (view == null) {
if (showView) {
// 關鍵3 設置view的顏色
state.view = view = new View(mContext);
view.setBackgroundColor(color);
view.setTransitionName(state.transitionName);
view.setId(state.id);
visibilityChanged = true;
view.setVisibility(INVISIBLE);
state.targetVisibility = VISIBLE;
LayoutParams lp = new LayoutParams(resolvedWidth, resolvedHeight,
resolvedGravity);
if (seascape) {
lp.leftMargin = sideMargin;
} else {
lp.rightMargin = sideMargin;
}
addView(view, lp);
updateColorViewTranslations();
}
} else {
int vis = showView ? VISIBLE : INVISIBLE;
visibilityChanged = state.targetVisibility != vis;
state.targetVisibility = vis;
LayoutParams lp = (LayoutParams) view.getLayoutParams();
int rightMargin = seascape ? 0 : sideMargin;
int leftMargin = seascape ? sideMargin : 0;
if (lp.height != resolvedHeight || lp.width != resolvedWidth
|| lp.gravity != resolvedGravity || lp.rightMargin != rightMargin
|| lp.leftMargin != leftMargin) {
lp.height = resolvedHeight;
lp.width = resolvedWidth;
lp.gravity = resolvedGravity;
lp.rightMargin = rightMargin;
lp.leftMargin = leftMargin;
view.setLayoutParams(lp);
}
if (showView) {
view.setBackgroundColor(color);
}
}
...
}複製代碼
先看下關鍵點1,這裏是根據SystemUI的配置決定是否顯示狀態欄背景顏色。若是狀態欄都不顯示,那就不必顯示背景色了。再看關鍵點2,若是狀態欄顯示,但背景是透明色,也不必添加背景顏色,即不知足(color & Color.BLACK) != 0。而後,看一下translucentFlag,默認狀況下,狀態欄背景色與translucent半透明效果互斥,半透明就統一用半透明顏色,不會再添加額外顏色。最後,再來看關鍵點3,其實很簡單,就是往DecorView上添加一個View,理論上DecorView也是一個FrameLayout,因此最終的實現就是在FrameLayout添加一個有背景色的View。
目前網上封裝好的狀態欄設置工具備很多,用得比較多的是StatusBarUtil,還有已經廢棄的SystemBarTint。可是有個問題,不管哪一個工具庫,都不敢保證可以百分百實現狀態欄着色的效果。所以,兼容性問題是一直存在的。經過實踐分析總結,兼容性問題主要有兩類:
當出現如上所述的問題時,咱們的處理方式通常是這樣(不侵入修改第三方庫):
if (RomUtil.isCompactSystemVersionForImmersion()) {
if (RomUtil.isCompactRomForImmersion()) {
StatusBarUtils.setColor(ActivityMain.this, Color.TRANSPARENT);
} else {
setTranslucentStatus(true);
tintManager = new SystemBarTintManager(this);
tintManager.setStatusBarTintEnabled(true);
tintManager.setStatusBarTintColor(Color.TRANSPARENT);//通知欄所需顏色
}
}複製代碼
看上面實現,會發現代碼邏輯很繁瑣。並且當一個新的地方須要設置狀態欄顏色時,相應的這一堆代碼都須要再寫一遍。顯然,這種方式不是太友好,固然,咱們能夠將條件判斷都修改到庫裏面,直接修改庫源碼。可是這又帶來另外一個問題,當庫有更新了,升級庫就很麻煩。
考慮到上述的一些問題,因此在咱們設計的庫中,將兼容性判斷的相關細節隱藏在庫中,使用的時候只需調用相關方法便可。庫對外提供了兼容性配置接口,用戶能夠自由配置,決定是否須要設置狀態欄顏色。即實現對庫無侵入的修改,即便升級了庫,相應的兼容性配置信息也不會丟失。整個庫設計的UML圖以下所示:
要使用庫也很是簡單,只須要以下幾步:
compile 'com.zr.statusbarmanager:library:1.0.0-beta'複製代碼
/**
* 檢測是否支持狀態欄設置
* @return
*/
boolean checkCompatiblity();
/**
* 檢測是否須要特殊設置的Rom
* @return
*/
boolean checkSpecialRom();複製代碼
上述的兩個方法,checkCompatiblity用來判斷是否支持設置狀態欄顏色。若是返回false,則不會對狀態欄設置顏色。checkSpecialRom在實際測試過程當中,有一款華爲手機,搭載EMUI 3.1系統,對應Android系統版本5.1。咱們發現用常規直接設置狀態欄顏色的方式設置是不生效的,可是用5.0如下系統設置狀態欄顏色方式設置就能夠生效。因此,該方法就是爲了此類特殊ROM而定義的。
固然庫也提供了一個默認的實現DefaultStatusBarCompatConfig
,這也是咱們在項目開發過程當中根據兼容性測試發現的一些問題進行的配置,絕對頗有參考價值。若是用戶本身實現配置,只須要以下設置:
public class StatusBarCompactConfig implements ICompatConfig {
@Override
public boolean checkCompatiblity() {
if (CompatUtil.checkCompatiblity()) {
// TODO: 17/7/28 自定義的兼容性判斷邏輯
} else {
return false;
}
}
@Override
public boolean checkSpecialRom() {
if (CompatUtil.checkSpecialRom()) {
return true;
} else {
// TODO: 17/7/28 自定義特殊ROM判斷邏輯
}
}
}複製代碼
其中CompatUtil
是庫提供的兼容性判斷工具類,若用戶在使用庫的時候須要帶上庫已經提供的相關兼容性信息,能夠調用工具類的相關方法。若是不須要,也能夠徹底本身實現。
StatusBarManager.getsInstance().init(new DefaultStatusBarCompatConfig());複製代碼
StatusBarManager.getsInstance().setColor(MainActivity.this, Color.TRANSPARENT);複製代碼
由於庫不能保證設置狀態欄顏色在全部機子上都生效,部分機子咱們能夠在兼容性測試的時候作到兼容,可是不保證不會有漏網之魚。這會致使一個問題,就是在這些不支持的手機上,沒有達到預期效果,下降了用戶體驗。這個時候若是可以發現設置顏色失敗,而後對這些不支持的機型可以作特殊處理,那就最好了。
因而想到的一種方案:
方案看着仍是可行的,那麼開始驗證。實際測試的時候,發現並非咱們想的那樣的。咱們測試是正確實現了狀態欄的設置,可是實際的視覺效果並無變成設置的顏色,仍是顯示的系統的狀態欄顏色。以下圖所示,紅框內是截屏圖像,發現與實際屏幕顯示的圖像狀態欄顏色並不一致。
猜想是有些ROM對狀態欄進行了定製,在狀態欄相同的位置覆蓋了一個相同的view,且這個view的層級比Decoeview高,截圖獲取的是view下方的DecorView視圖。(你們若是有什麼好的判斷的方式,期待交流~)
Android4.4的時候,加了個windowTranslucentStatus屬性,實現了狀態欄導航欄半透明效果,而Android5.0以後以上狀態欄、導航欄支持顏色隨意設定,因此,5.0以後通常不使用須要使用該屬性,並且設置狀態欄顏色與windowTranslucentStatus是互斥的。因此,默認狀況下android:windowTranslucentStatus是false。也就是說:‘windowTranslucentStatus’和‘windowTranslucentNavigation’設置爲true後就再設置‘statusBarColor’和‘navigationBarColor’就沒有效果了
boolean show = state.present
&& (color & Color.BLACK) != 0
&& ((mWindow.getAttributes().flags & state.translucentFlag) == 0 || force);複製代碼
能夠看到,添加背景View有一個必要條件
(mWindow.getAttributes().flags & state.translucentFlag) == 0複製代碼
也就是說一旦設置了
<item name="android:windowTranslucentStatus">true</item>複製代碼
相應的狀態欄或者導航欄的顏色設置就不在生效。不過它並不影響fitSystemWindow的邏輯。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
activity.getWindow().setStatusBarColor(calculateStatusColor(color, statusBarAlpha));
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
...
}複製代碼
以上代碼設置狀態欄顏色,當設置狀態欄透明時,咱們發現一個現象。在5.0以前的機子上,內容能夠延伸到狀態欄下面;而在5.0以上的機子上,頂部會有一塊空出的view,如圖所示。
爲何會致使這個現象呢?這裏要從WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
這個屬性提及。在5.0以前系統上,該屬性設置爲true,5.0及以後系統,屬性設置爲false。查看WindowManager中該屬性的註釋,發現以下一段話:
<p>When this flag is enabled for a window, it automatically sets
* the system UI visibility flags {@link View#SYSTEM_UI_FLAG_LAYOUT_STABLE} and
* {@link View#SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN}.</p>複製代碼
原來設置FLAG_TRANSLUCENT_STATUS
爲true以後,會自動設置SYSTEM_UI_FLAG_LAYOUT_STABLE
和SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
的系統UI屬性,經過前面沉浸設置,能夠知道這就是實現內容顯示到狀態欄下的設置。由於5.0以上系統FLAG_TRANSLUCENT_STATUS
爲false,固然內容也將不會顯示到導航欄下。因此,在5.0的機子上,須要加上此段設置代碼:
activity.getWindow().getDecorView().set(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);複製代碼
在Android項目開發過程當中,免不了要和系統欄打交道。以上是做者根據平時項目開發經驗、並結合網上查閱的資料對狀態欄相關設置進行的總結。但願對你們有幫助,歡迎你們交流討論~