一提到沉浸式狀態(tài)欄祝谚,第一個浮現(xiàn)在腦海里的詞就是“碎片化”赏胚。碎片化是讓 Android 開發(fā)者很頭疼的問題邓萨,相信沒有哪位開發(fā)者會不喜歡“write once, run anywhere”的感覺蔗蹋,碎片化讓我們不得不耗費(fèi)精力去校驗(yàn)代碼在各個系統(tǒng)版本朋沮、各個機(jī)型上是否有效蛇券。因此以前我一直把沉浸式狀態(tài)欄看作一塊難啃的骨頭,?但是該面對的問題遲早還是要面對朽们,所以怀读,不如就此開始吧诉位。
沉浸式狀態(tài)欄的實(shí)現(xiàn)
方法一:通過設(shè)置 Theme 主題設(shè)置狀態(tài)欄透明
因?yàn)?API21 之后(也就是 android 5.0 之后)的狀態(tài)欄骑脱,會默認(rèn)覆蓋一層半透明遮罩。且為了保持4.4以前系統(tǒng)正常使用苍糠,故需要三份 style 文件叁丧,即默認(rèn)的values(不設(shè)置狀態(tài)欄透明)、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() 方法設(shè)置狀態(tài)欄顏色稚瘾。
<style name="TranslucentTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:windowTranslucentStatus">false</item>
<item name="android:windowTranslucentNavigation">true</item>
<!--Android 5.x開始需要把顏色設(shè)置透明,否則導(dǎo)航欄會呈現(xiàn)系統(tǒng)默認(rèn)的淺灰色-->
<item name="android:statusBarColor">@android:color/transparent</item>
</style>
由圖可見姚炕,設(shè)置之后布局的內(nèi)容延伸到了狀態(tài)欄摊欠。但有些場景下,我們還是需要狀態(tài)欄那塊位置存在的(然而不存在的)柱宦。有三種解決方法:
法一:設(shè)置 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.
當(dāng)該屬性設(shè)置 true 時些椒,會在屏幕最上方預(yù)留出狀態(tài)欄高度的 padding。
在布局的最外層設(shè)置 android:fitsSystemWindows="true"
屬性掸刊。當(dāng)然免糕,也可以通過代碼設(shè)置:
/**
* 設(shè)置頁面最外層布局 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);
}
}
通過該設(shè)置保留狀態(tài)欄高度的 paddingTop
后,再設(shè)置狀態(tài)欄的顏色。就可以達(dá)到設(shè)想的效果石窑。但這種方式實(shí)現(xiàn)有些問題牌芋,例如我們想設(shè)置狀態(tài)欄為藍(lán)色,只能通過設(shè)置最外層布局的背景為藍(lán)色來實(shí)現(xiàn)松逊,然而一旦設(shè)置后姜贡,整個布局就都變成了藍(lán)色,只能在下方的布局內(nèi)容里另外再設(shè)置白色背景棺棵,而這樣就存在過度繪制了楼咳。而且設(shè)置了 fitsSystemWindows=true
屬性的頁面,在點(diǎn)擊 EditText 調(diào)出 軟鍵盤時烛恤,整個視圖都會被頂上去母怜。
法二:布局里添加占位狀態(tài)欄
法一:在根布局加入一個占位狀態(tài)欄,這樣雖然整個內(nèi)容頁面時頂?shù)筋^的缚柏,但是因?yàn)樵趦?nèi)容布局里添加了一個占位狀態(tài)欄苹熏,所以效果與設(shè)想的一致。
<View
android:id="@+id/statusBarView"
android:background="@color/blue"
android:layout_width="match_parent"
android:layout_height="wrap_content"></View>
通過反射獲取狀態(tài)欄高度:
/**
* 利用反射獲取狀態(tài)欄高度
* @return
*/
public int getStatusBarHeight() {
int result = 0;
//獲取狀態(tài)欄高度的資源id
int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0) {
result = getResources().getDimensionPixelSize(resourceId);
}
return result;
}
設(shè)置占位視圖高度
View statusBar = findViewById(R.id.statusBarView);
ViewGroup.LayoutParams layoutParams = statusBar.getLayoutParams();
layoutParams.height = getStatusBarHeight();
當(dāng)然币喧,除了從布局文件中添加這一方式之外轨域,一樣可以在代碼中添加。比較推薦使用代碼添加的方式杀餐,方便封裝使用干发。
/**
* 添加狀態(tài)欄占位視圖
*
* @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);
}
法三:代碼中設(shè)置 paddingTop 并添加占位狀態(tài)欄
手動給根視圖設(shè)置一個 paddingTop
,高度為狀態(tài)欄高度史翘,相當(dāng)于手動實(shí)現(xiàn)了 fitsSystemWindows=true
的效果枉长,然后再在根視圖加入一個占位視圖,其高度也設(shè)置為狀態(tài)欄高度琼讽。
//設(shè)置 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 以上直接設(shè)置狀態(tài)欄顏色
activity.getWindow().setStatusBarColor(color);
} else {
//根布局添加占位狀態(tài)欄
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);
}
個人認(rèn)為最優(yōu)解應(yīng)該是第三種方法必峰,通過這種方法達(dá)到沉浸式的效果后面也可以很方便地拓展出漸變色的狀態(tài)欄。
方法二:代碼中設(shè)置
通過在代碼中設(shè)置钻蹬,實(shí)現(xiàn)方法一中在 Theme 主題樣式里設(shè)置的屬性吼蚁,便于封裝。
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);
}
}
但是從圖片中也看到了问欠,該方案會導(dǎo)致一個問題就是導(dǎo)航欄顏色變灰肝匆。
經(jīng)測試,在 5.x 以下導(dǎo)航欄透明是可以生效的溅潜,但 5.x 以上導(dǎo)航欄會變灰色(正常情況下我們期望導(dǎo)航欄保持默認(rèn)顏色黑色不變)术唬,但因?yàn)樵O(shè)置了FLAG_TRANSLUCENT_NAVIGATION
,所以即使代碼中設(shè)置 getWindow().setNavigationBarColor(Color.BLACK);
也是不起作用的滚澜。但如果不設(shè)置該 FLAG 粗仓,狀態(tài)欄又無法被置為隱藏和設(shè)置透明。
方案二:全屏模式的延伸
通過設(shè)置 FLAG ,讓應(yīng)用內(nèi)容占用系統(tǒng)狀態(tài)欄的空間借浊,經(jīng)測試該方式不會影響對導(dǎo)航欄的設(shè)置塘淑。
/**
* 通過設(shè)置全屏,設(shè)置狀態(tài)欄透明
*
* @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開始需要把顏色設(shè)置透明蚂斤,否則導(dǎo)航欄會呈現(xiàn)系統(tǒng)默認(rèn)的淺灰色
Window window = activity.getWindow();
View decorView = window.getDecorView();
//兩個 flag 要結(jié)合使用存捺,表示讓應(yīng)用的主體內(nèi)容占用系統(tǒng)狀態(tài)欄的空間
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);
//導(dǎo)航欄顏色也可以正常設(shè)置
// 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);
}
}
}
驗(yàn)證其他使用場景
側(cè)滑菜單
使用 AS 自動創(chuàng)建 Navigation Drawer Activity
,布局結(jié)構(gòu)為:
- DrawerLayout
- include :內(nèi)容布局曙蒸,默認(rèn)使用 ToolBar
- NavigationView :側(cè)滑布局
這里只調(diào)用了 fullScreen()
捌治, 測試一下運(yùn)行結(jié)果如何:
可以看到都有不盡如人意的地方,4.4 系統(tǒng)中內(nèi)容視圖是可以正常延伸到狀態(tài)欄中纽窟,但側(cè)滑菜單中卻在上方出現(xiàn)了白條肖油,而在 6.0 中側(cè)滑菜單上會有半透明遮罩。針對 6.0 側(cè)滑菜單半透明遮罩問題臂港,通過設(shè)置為 NavigationView
設(shè)置屬性 app:insetForeground="#00000000"
即可解決森枪。針對 4.4 側(cè)滑菜單白條問題,經(jīng)過測試审孽,通過對最外層布局設(shè)置 setFitsSystemWindows(true)
和 setClipToPadding(false)
可以解決县袱,所以這里對之前的 fitsSystemWindows
方法稍加修改:
/**
* 設(shè)置頁面最外層布局 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) {
//布局預(yù)留狀態(tài)欄高度的 padding
parentView.setFitsSystemWindows(true);
if (parentView instanceof DrawerLayout) {
DrawerLayout drawer = (DrawerLayout) parentView;
//將主頁面頂部延伸至status bar;雖默認(rèn)為false,但經(jīng)測試,DrawerLayout需顯示設(shè)置
drawer.setClipToPadding(false);
}
}
}
這樣是解決了上述的問題,既然延伸內(nèi)容沒問題了佑力,那就開開心心地像上面一樣調(diào)用 addStatusViewWithColor()
方法增加個占位狀態(tài)欄式散,解決一下內(nèi)容頂?shù)筋^的問題吧:
可以看到搓萧,效果依然不是我們想要的杂数,雖然占位狀態(tài)欄是有了宛畦,但是卻也覆蓋到了側(cè)滑菜單上瘸洛,并且即使設(shè)置了 android:fitsSystemWindows="true"
也并沒有什么卵用,內(nèi)容布局依然頂?shù)搅祟^部次和。這里有兩種解決方法:1. 第一種方案是網(wǎng)上提到比較多的反肋,改變 ToolBar
的高度,并增加狀態(tài)欄高度的 paddingTop
踏施,這也是
ImmersionBar 庫采用的方案石蔗。2. 第二種方案其實(shí)思路與第一種差不多,就是將原有的內(nèi)容布局從 DrawerLayout
中移除畅形,并添加到線性布局(布局中已有占位狀態(tài)欄)养距,之后再將這個線性布局添加到 DrawerLayout
中成為新的內(nèi)容布局顽馋,此謂貍貓換太子根时。
/**
* 是否是最外層布局為 DrawerLayout 的側(cè)滑菜單
* @param drawerLayout 是否最外層布局為 DrawerLayout
* @param contentId 內(nèi)容視圖的 id
* @return
*/
public StatusBarUtils setIsDrawerLayout(boolean drawerLayout, int contentId) {
mIsDrawerLayout = drawerLayout;
mContentResourseIdInDrawer = contentId;
return this;
}
/**
* 添加狀態(tài)欄占位視圖
*
* @param activity
*/
private void addStatusViewWithColor(Activity activity, int color) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
if (isDrawerLayout()) {
//要在內(nèi)容布局增加狀態(tài)欄灯节,否則會蓋在側(cè)滑菜單上
ViewGroup rootView = (ViewGroup) activity.findViewById(android.R.id.content);
//DrawerLayout 則需要在第一個子視圖即內(nèi)容試圖中添加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);
//添加占位狀態(tài)欄到線性布局中
linearLayout.addView(statusBarView, lp);
//側(cè)滑菜單
DrawerLayout drawer = (DrawerLayout) parentView;
//內(nèi)容視圖
View content = activity.findViewById(mContentResourseIdInDrawer);
//將內(nèi)容視圖從 DrawerLayout 中移除
drawer.removeView(content);
//添加內(nèi)容視圖
linearLayout.addView(content, content.getLayoutParams());
//將帶有占位狀態(tài)欄的新的內(nèi)容視圖設(shè)置給 DrawerLayout
drawer.addView(linearLayout, 0);
} else {
//設(shè)置 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) {
//直接設(shè)置狀態(tài)欄顏色
activity.getWindow().setStatusBarColor(color);
} else {
//增加占位狀態(tài)欄
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);
}
}
}
}
一番操作后,效果如下:
對于內(nèi)容視圖未使用到 ToolBar
的情況方案二依然可以適用诊笤。
ActionBar
上述代碼在使用 ActionBar 時可以完美適配嗎?測試后效果如下圖所示
可以看到扣猫,通過添加指定顏色的占位狀態(tài)來達(dá)到沉浸效果的方案革娄,在 4.4 系統(tǒng)上效果是正常的,但是在 6.0 上溯警,在狀態(tài)欄和 Actionbar 之間會有陰影浑玛,這個陰影是主題的效果寻仗。不知道大家還記不記得 Theme 主題里的幾個設(shè)計(jì)顏色的屬性:
colorPrimary
指定 ActionBar 的顏色,colorPrimaryDark
指定狀態(tài)欄顏色弄慰,經(jīng)過測試,在主題里將二者設(shè)為統(tǒng)一顏色蝶锋,狀態(tài)欄和 ActionBar 之間不會有黑邊曹动。自然,我們除了在 Theme 主題里設(shè)置牲览,還可以直接在代碼里通過上文提到過的代碼修改 5.x 以上系統(tǒng)的狀態(tài)欄顏色:
Window window = activity.getWindow();
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
window.setStatusBarColor(Color.BLUE);
但是因?yàn)?setStatusBarColor()
方法的參數(shù)無法傳入 Drawble 墓陈,所以這種方式是無法實(shí)現(xiàn)漸變色狀態(tài)欄的效果的。所以還是應(yīng)該聚焦在怎么解決 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 風(fēng)格樣式
<item name="actionBarStyle">@style/ActionBarStyleWithoutShadow</item>
</style>
//ActionBar 不帶陰影的主題樣式
<style name="ActionBarStyleWithoutShadow" parent="android:Theme.Holo.ActionBar">
<item name="background">@color/blue</item>
</style>
還有第二種更簡單的方式仔拟,那就是直接在代碼里設(shè)置去除陰影:
/**
* 去除 ActionBar 陰影
*/
public StatusBarUtils clearActionBarShadow() {
if (Build.VERSION.SDK_INT >= 21) {
ActionBar supportActionBar = ((AppCompatActivity) mActivity).getSupportActionBar();
if (supportActionBar != null) {
supportActionBar.setElevation(0);
}
}
return this;
}
并且因?yàn)閮?nèi)容是位于 ActionBar 之下的,我們還必須給內(nèi)容視圖是指一個 paddingTop飒赃,高度為狀態(tài)欄高度+ActionBar 高度利花,才可以使內(nèi)容正常顯示。我們給 ActionBar 設(shè)置一個漸變色試試看:
//drawble 文件夾內(nèi)新建 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 設(shè)置漸變背景色
getSupportActionBar().setBackgroundDrawable(getResources().getDrawable(R.drawable.shape));
//占位狀態(tài)欄 設(shè)置漸變背景色
View statusBarView = new View(activity);
...
//增加占位狀態(tài)欄方法同上载佳,只是在設(shè)置 statusBarView 背景上有 color 和 drawble 之分
statusBarView.setBackground(drawable);
if (isActionBar()) {
//要增加內(nèi)容視圖的 paddingTop,否則內(nèi)容被 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);
}
}
至此炒事,嘗試適配了幾種比較常見的使用場景的沉浸式狀態(tài)欄,效果也都還比較符合預(yù)期蔫慧。真正去處理這個問題時會發(fā)現(xiàn)其實(shí)問題也沒有想象中的那么復(fù)雜挠乳。最后附上 Github 源碼。
Stay hungry. Stay foolish.
下篇博客再見姑躲。