一提到沉浸式狀態(tài)欄近顷,第一個浮現(xiàn)在腦海里的詞就是“碎片化”生音。碎片化是讓 Android 開發(fā)者很頭疼的問題,相信沒有哪位開發(fā)者會不喜歡“write once, run anywhere”的感覺窒升,碎片化讓我們不得不耗費(fèi)精力去校驗代碼在各個系統(tǒng)版本缀遍、各個機(jī)型上是否有效。
沉浸式狀態(tài)欄的實現(xiàn)
方法一:通過設(shè)置 Theme 主題設(shè)置狀態(tài)欄透明
因為 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è)想的效果绰筛。但這種方式實現(xiàn)有些問題,例如我們想設(shè)置狀態(tài)欄為藍(lán)色描融,只能通過設(shè)置最外層布局的背景為藍(lán)色來實現(xiàn)铝噩,然而一旦設(shè)置后,整個布局就都變成了藍(lán)色窿克,只能在下方的布局內(nèi)容里另外再設(shè)置白色背景骏庸,而這樣就存在過度繪制了。而且設(shè)置了 fitsSystemWindows=true
屬性的頁面年叮,在點(diǎn)擊 EditText 調(diào)出 軟鍵盤時具被,整個視圖都會被頂上去。
法二:布局里添加占位狀態(tài)欄
法一:在根布局加入一個占位狀態(tài)欄只损,這樣雖然整個內(nèi)容頁面時頂?shù)筋^的一姿,但是因為在內(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)于手動實現(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è)置睁蕾,實現(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)顏色黑色不變)粤咪,但因為設(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);
}
}
}
驗證其他使用場景
側(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. 第二種方案其實思路與第一種差不多悠垛,就是將原有的內(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 時可以完美適配嗎派草?測試后效果如下圖所示
[圖片上傳失敗...(image-88afe8-1517968447061)]
可以看到搀缠,通過添加指定顏色的占位狀態(tài)來達(dá)到沉浸效果的方案,在 4.4 系統(tǒng)上效果是正常的近迁,但是在 6.0 上艺普,在狀態(tài)欄和 Actionbar 之間會有陰影,這個陰影是主題的效果鉴竭。不知道大家還記不記得 Theme 主題里的幾個設(shè)計顏色的屬性:
[圖片上傳失敗...(image-3cdf33-1517968447061)]
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);
但是因為 setStatusBarColor()
方法的參數(shù)無法傳入 Drawble 读虏,所以這種方式是無法實現(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;
}
并且因為內(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);
}
}