一提到沉浸式狀態欄,第一個浮現在腦海里的詞就是“碎片化”。碎片化是讓 Android 開發者很頭疼的問題,相信沒有哪位開發者會不喜歡“write once, run anywhere”的感覺,碎片化讓我們不得不耗費精力去校驗代碼在各個系統版本、各個機型上是否有效。因此以前我一直把沉浸式狀態欄看作一塊難啃的骨頭,?但是該面對的問題遲早還是要面對,所以,不如就此開始吧。
沉浸式狀態欄的實現
方法一:通過設置 Theme 主題設置狀態欄透明
因為 API21 之后(也就是 android 5.0 之后)的狀態欄,會默認覆蓋一層半透明遮罩。且為了保持4.4以前系統正常使用,故需要三份 style 文件,即默認的values(不設置狀態欄透明)、values-v19、values-v21(解決半透明遮罩問題)。
//valuse
<style name="TranslucentTheme" parent="AppTheme">
</style>
// values-v19。v19 開始有 android:windowTranslucentStatus 這個屬性
<style name="TranslucentTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:windowTranslucentStatus">true</item>
<item name="android:windowTranslucentNavigation">true</item>
</style>
// values-v21。5.0 以上提供了 setStatusBarColor() 方法設置狀態欄顏色。
<style name="TranslucentTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:windowTranslucentStatus">false</item>
<item name="android:windowTranslucentNavigation">true</item>
<!--Android 5.x開始需要把顏色設置透明,否則導航欄會呈現系統默認的淺灰色-->
<item name="android:statusBarColor">@android:color/transparent</item>
</style>
由圖可見,設置之后布局的內容延伸到了狀態欄。但有些場景下,我們還是需要狀態欄那塊位置存在的(然而不存在的)。有三種解決方法:
法一:設置 fitsSystemWindows 屬性
引用一下官方對該屬性的解釋吧:
android:fitsSystemWindows
Boolean internal attribute to adjust view layout based on system windows such as the status bar. If true, adjusts the padding of this view to leave space for the system windows. Will only take effect if this view is in a non-embedded activity.
當該屬性設置 true 時,會在屏幕最上方預留出狀態欄高度的 padding。
在布局的最外層設置 android:fitsSystemWindows="true"
屬性。當然,也可以通過代碼設置:
/**
* 設置頁面最外層布局 FitsSystemWindows 屬性
* @param activity
* @param value
*/
public static void setFitsSystemWindows(Activity activity, boolean value) {
ViewGroup contentFrameLayout = (ViewGroup) activity.findViewById(android.R.id.content);
View parentView = contentFrameLayout.getChildAt(0);
if (parentView != null && Build.VERSION.SDK_INT >= 14) {
parentView.setFitsSystemWindows(value);
}
}
通過該設置保留狀態欄高度的 paddingTop
后,再設置狀態欄的顏色。就可以達到設想的效果。但這種方式實現有些問題,例如我們想設置狀態欄為藍色,只能通過設置最外層布局的背景為藍色來實現,然而一旦設置后,整個布局就都變成了藍色,只能在下方的布局內容里另外再設置白色背景,而這樣就存在過度繪制了。而且設置了 fitsSystemWindows=true
屬性的頁面,在點擊 EditText 調出 軟鍵盤時,整個視圖都會被頂上去。
法二:布局里添加占位狀態欄
法一:在根布局加入一個占位狀態欄,這樣雖然整個內容頁面時頂到頭的,但是因為在內容布局里添加了一個占位狀態欄,所以效果與設想的一致。
<View
android:id="@+id/statusBarView"
android:background="@color/blue"
android:layout_width="match_parent"
android:layout_height="wrap_content"></View>
通過反射獲取狀態欄高度:
/**
* 利用反射獲取狀態欄高度
* @return
*/
public int getStatusBarHeight() {
int result = 0;
//獲取狀態欄高度的資源id
int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0) {
result = getResources().getDimensionPixelSize(resourceId);
}
return result;
}
設置占位視圖高度
View statusBar = findViewById(R.id.statusBarView);
ViewGroup.LayoutParams layoutParams = statusBar.getLayoutParams();
layoutParams.height = getStatusBarHeight();
當然,除了從布局文件中添加這一方式之外,一樣可以在代碼中添加。比較推薦使用代碼添加的方式,方便封裝使用。
/**
* 添加狀態欄占位視圖
*
* @param activity
*/
private void addStatusViewWithColor(Activity activity, int color) {
ViewGroup contentView = (ViewGroup) activity.findViewById(android.R.id.content);
View statusBarView = new View(activity);
ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
getStatusBarHeight(activity));
statusBarView.setBackgroundColor(color);
contentView.addView(statusBarView, lp);
}
法三:代碼中設置 paddingTop 并添加占位狀態欄
手動給根視圖設置一個 paddingTop
,高度為狀態欄高度,相當于手動實現了 fitsSystemWindows=true
的效果,然后再在根視圖加入一個占位視圖,其高度也設置為狀態欄高度。
//設置 paddingTop
ViewGroup rootView = (ViewGroup) mActivity.getWindow().getDecorView().findViewById(android.R.id.content);
rootView.setPadding(0, getStatusBarHeight(mActivity), 0, 0);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
//5.0 以上直接設置狀態欄顏色
activity.getWindow().setStatusBarColor(color);
} else {
//根布局添加占位狀態欄
ViewGroup decorView = (ViewGroup) mActivity.getWindow().getDecorView();
View statusBarView = new View(activity);
ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
getStatusBarHeight(activity));
statusBarView.setBackgroundColor(color);
decorView.addView(statusBarView, lp);
}
個人認為最優解應該是第三種方法,通過這種方法達到沉浸式的效果后面也可以很方便地拓展出漸變色的狀態欄。
方法二:代碼中設置
通過在代碼中設置,實現方法一中在 Theme 主題樣式里設置的屬性,便于封裝。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
int flagTranslucentStatus = WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS;
int flagTranslucentNavigation = WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Window window = getWindow();
WindowManager.LayoutParams attributes = window.getAttributes();
attributes.flags |= flagTranslucentNavigation;
window.setAttributes(attributes);
getWindow().setStatusBarColor(Color.TRANSPARENT);
} else {
Window window = getWindow();
WindowManager.LayoutParams attributes = window.getAttributes();
attributes.flags |= flagTranslucentStatus | flagTranslucentNavigation;
window.setAttributes(attributes);
}
}
但是從圖片中也看到了,該方案會導致一個問題就是導航欄顏色變灰。
經測試,在 5.x 以下導航欄透明是可以生效的,但 5.x 以上導航欄會變灰色(正常情況下我們期望導航欄保持默認顏色黑色不變),但因為設置了FLAG_TRANSLUCENT_NAVIGATION
,所以即使代碼中設置 getWindow().setNavigationBarColor(Color.BLACK);
也是不起作用的。但如果不設置該 FLAG ,狀態欄又無法被置為隱藏和設置透明。
方案二:全屏模式的延伸
通過設置 FLAG ,讓應用內容占用系統狀態欄的空間,經測試該方式不會影響對導航欄的設置。
/**
* 通過設置全屏,設置狀態欄透明
*
* @param activity
*/
private void fullScreen(Activity activity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
//5.x開始需要把顏色設置透明,否則導航欄會呈現系統默認的淺灰色
Window window = activity.getWindow();
View decorView = window.getDecorView();
//兩個 flag 要結合使用,表示讓應用的主體內容占用系統狀態欄的空間
int option = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
decorView.setSystemUiVisibility(option);
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
window.setStatusBarColor(Color.TRANSPARENT);
//導航欄顏色也可以正常設置
// window.setNavigationBarColor(Color.TRANSPARENT);
} else {
Window window = activity.getWindow();
WindowManager.LayoutParams attributes = window.getAttributes();
int flagTranslucentStatus = WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS;
int flagTranslucentNavigation = WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION;
attributes.flags |= flagTranslucentStatus;
// attributes.flags |= flagTranslucentNavigation;
window.setAttributes(attributes);
}
}
}
驗證其他使用場景
側滑菜單
使用 AS 自動創建 Navigation Drawer Activity
,布局結構為:
- DrawerLayout
- include :內容布局,默認使用 ToolBar
- NavigationView :側滑布局
這里只調用了 fullScreen()
, 測試一下運行結果如何:
可以看到都有不盡如人意的地方,4.4 系統中內容視圖是可以正常延伸到狀態欄中,但側滑菜單中卻在上方出現了白條,而在 6.0 中側滑菜單上會有半透明遮罩。針對 6.0 側滑菜單半透明遮罩問題,通過設置為 NavigationView
設置屬性 app:insetForeground="#00000000"
即可解決。針對 4.4 側滑菜單白條問題,經過測試,通過對最外層布局設置 setFitsSystemWindows(true)
和 setClipToPadding(false)
可以解決,所以這里對之前的 fitsSystemWindows
方法稍加修改:
/**
* 設置頁面最外層布局 FitsSystemWindows 屬性
*
* @param activity
*/
private void fitsSystemWindows(Activity activity) {
ViewGroup contentFrameLayout = (ViewGroup) activity.findViewById(android.R.id.content);
View parentView = contentFrameLayout.getChildAt(0);
if (parentView != null && Build.VERSION.SDK_INT >= 14) {
//布局預留狀態欄高度的 padding
parentView.setFitsSystemWindows(true);
if (parentView instanceof DrawerLayout) {
DrawerLayout drawer = (DrawerLayout) parentView;
//將主頁面頂部延伸至status bar;雖默認為false,但經測試,DrawerLayout需顯示設置
drawer.setClipToPadding(false);
}
}
}
這樣是解決了上述的問題,既然延伸內容沒問題了,那就開開心心地像上面一樣調用 addStatusViewWithColor()
方法增加個占位狀態欄,解決一下內容頂到頭的問題吧:
可以看到,效果依然不是我們想要的,雖然占位狀態欄是有了,但是卻也覆蓋到了側滑菜單上,并且即使設置了 android:fitsSystemWindows="true"
也并沒有什么卵用,內容布局依然頂到了頭部。這里有兩種解決方法:1. 第一種方案是網上提到比較多的,改變 ToolBar
的高度,并增加狀態欄高度的 paddingTop
,這也是
ImmersionBar 庫采用的方案。2. 第二種方案其實思路與第一種差不多,就是將原有的內容布局從 DrawerLayout
中移除,并添加到線性布局(布局中已有占位狀態欄),之后再將這個線性布局添加到 DrawerLayout
中成為新的內容布局,此謂貍貓換太子。
/**
* 是否是最外層布局為 DrawerLayout 的側滑菜單
* @param drawerLayout 是否最外層布局為 DrawerLayout
* @param contentId 內容視圖的 id
* @return
*/
public StatusBarUtils setIsDrawerLayout(boolean drawerLayout, int contentId) {
mIsDrawerLayout = drawerLayout;
mContentResourseIdInDrawer = contentId;
return this;
}
/**
* 添加狀態欄占位視圖
*
* @param activity
*/
private void addStatusViewWithColor(Activity activity, int color) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
if (isDrawerLayout()) {
//要在內容布局增加狀態欄,否則會蓋在側滑菜單上
ViewGroup rootView = (ViewGroup) activity.findViewById(android.R.id.content);
//DrawerLayout 則需要在第一個子視圖即內容試圖中添加padding
View parentView = rootView.getChildAt(0);
LinearLayout linearLayout = new LinearLayout(activity);
linearLayout.setOrientation(LinearLayout.VERTICAL);
View statusBarView = new View(activity);
ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
getStatusBarHeight(activity));
statusBarView.setBackgroundColor(color);
//添加占位狀態欄到線性布局中
linearLayout.addView(statusBarView, lp);
//側滑菜單
DrawerLayout drawer = (DrawerLayout) parentView;
//內容視圖
View content = activity.findViewById(mContentResourseIdInDrawer);
//將內容視圖從 DrawerLayout 中移除
drawer.removeView(content);
//添加內容視圖
linearLayout.addView(content, content.getLayoutParams());
//將帶有占位狀態欄的新的內容視圖設置給 DrawerLayout
drawer.addView(linearLayout, 0);
} else {
//設置 paddingTop
ViewGroup rootView = (ViewGroup) mActivity.getWindow().getDecorView().findViewById(android.R.id.content);
rootView.setPadding(0, getStatusBarHeight(mActivity), 0, 0);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
//直接設置狀態欄顏色
activity.getWindow().setStatusBarColor(color);
} else {
//增加占位狀態欄
ViewGroup decorView = (ViewGroup) mActivity.getWindow().getDecorView();
View statusBarView = new View(activity);
ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
getStatusBarHeight(activity));
statusBarView.setBackgroundColor(color);
decorView.addView(statusBarView, lp);
}
}
}
}
一番操作后,效果如下:
對于內容視圖未使用到 ToolBar
的情況方案二依然可以適用。
ActionBar
上述代碼在使用 ActionBar 時可以完美適配嗎?測試后效果如下圖所示
可以看到,通過添加指定顏色的占位狀態來達到沉浸效果的方案,在 4.4 系統上效果是正常的,但是在 6.0 上,在狀態欄和 Actionbar 之間會有陰影,這個陰影是主題的效果。不知道大家還記不記得 Theme 主題里的幾個設計顏色的屬性:
colorPrimary
指定 ActionBar 的顏色,colorPrimaryDark
指定狀態欄顏色,經過測試,在主題里將二者設為統一顏色,狀態欄和 ActionBar 之間不會有黑邊。自然,我們除了在 Theme 主題里設置,還可以直接在代碼里通過上文提到過的代碼修改 5.x 以上系統的狀態欄顏色:
Window window = activity.getWindow();
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
window.setStatusBarColor(Color.BLUE);
但是因為 setStatusBarColor()
方法的參數無法傳入 Drawble ,所以這種方式是無法實現漸變色狀態欄的效果的。所以還是應該聚焦在怎么解決 ActionBar 陰影的問題,上面說了,既然這個陰影是 Theme 的效果,那就肯定有移除這種效果的方法,一種解決方法是更改主題為 ActionBar 不帶陰影的主題樣式:
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="android:windowContentOverlay">@null</item>
//更改 ActionBar 風格樣式
<item name="actionBarStyle">@style/ActionBarStyleWithoutShadow</item>
</style>
//ActionBar 不帶陰影的主題樣式
<style name="ActionBarStyleWithoutShadow" parent="android:Theme.Holo.ActionBar">
<item name="background">@color/blue</item>
</style>
還有第二種更簡單的方式,那就是直接在代碼里設置去除陰影:
/**
* 去除 ActionBar 陰影
*/
public StatusBarUtils clearActionBarShadow() {
if (Build.VERSION.SDK_INT >= 21) {
ActionBar supportActionBar = ((AppCompatActivity) mActivity).getSupportActionBar();
if (supportActionBar != null) {
supportActionBar.setElevation(0);
}
}
return this;
}
并且因為內容是位于 ActionBar 之下的,我們還必須給內容視圖是指一個 paddingTop,高度為狀態欄高度+ActionBar 高度,才可以使內容正常顯示。我們給 ActionBar 設置一個漸變色試試看:
//drawble 文件夾內新建 shape 漸變色
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="0"
android:centerX="0.7"
android:endColor="@color/shape2"
android:startColor="@color/shape1"
android:centerColor="@color/shape3"
android:type="linear" />
</shape>
//ActionBar 設置漸變背景色
getSupportActionBar().setBackgroundDrawable(getResources().getDrawable(R.drawable.shape));
//占位狀態欄 設置漸變背景色
View statusBarView = new View(activity);
...
//增加占位狀態欄方法同上,只是在設置 statusBarView 背景上有 color 和 drawble 之分
statusBarView.setBackground(drawable);
if (isActionBar()) {
//要增加內容視圖的 paddingTop,否則內容被 ActionBar 遮蓋
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
ViewGroup rootView = (ViewGroup) mActivity.getWindow().getDecorView().findViewById(android.R.id.content);
rootView.setPadding(0, getStatusBarHeight(mActivity) + getActionBarHeight(mActivity), 0, 0);
}
}
至此,嘗試適配了幾種比較常見的使用場景的沉浸式狀態欄,效果也都還比較符合預期。真正去處理這個問題時會發現其實問題也沒有想象中的那么復雜。最后附上 Github 源碼。
Stay hungry. Stay foolish.
下篇博客再見。