Android進階之沉浸式狀態欄原理和使用詳解
本文轉載自微信公眾號「Android開發編程」,作者Android開發編程。轉載本文請聯系Android開發編程公眾號。
前言
沉浸式就是要給用戶提供完全沉浸的體驗,使用戶有一種置身于虛擬世界之中的感覺;
這種體驗在各類游戲中被廣泛應用,絕大部分的游戲都會在打開后,使得屏幕被完全被游戲占據,讓玩家沉浸其中,從體驗上沉浸式效果會更好一些;
網上很多人都寫過沉浸式原理的文章,講解的都不是很清晰,讓人很費解;
今天我們就來徹底的總結下沉浸式實現和原理;
一、沉浸式概念和為何要用沉浸式
1、沉浸式概念
- Android系統4.4之前狀態欄一直是黑色的,在4.4中帶來了 windowTranslucentStatus 這一特性,開始引出“沉浸式狀態欄”這個概念。Google 在 Android 4.4 的 API 描述頁面里提到了“Translucent system UI styling”,即透明化的系統UI風格。
- “沉浸式狀態欄”準確來說應該是“透明欄”,是 4.4 新定義的設計規范;
- 簡單來說就是在軟件打開的時候通知欄和軟件頂部顏色融為一體,這樣可以使軟件和系統本身更加符合,同時通知欄的顏色不再是白色、黑色簡單的兩種了;
- 沉浸式表示全屏顯示手機屏幕是沒有手機里面自帶的任何控件;
2、為何要用沉浸式
- 如果App里面目前都沒有做沉浸式狀態欄,會導致狀態欄呈黑色條狀,而且下面這個的黑色(白色)條狀與App主界面有很明顯的區別。這樣在一定程度上犧牲了視覺高度,界面面積變小,最主要的是用戶的視覺和體驗;
- 說白了,用戶體驗好,用的爽,留存就高,那么領導肯定讓開發沉浸式主題樣式;
二、沉浸式原理和兼容
從Android4.4 到現在(Android 7.1),關于沉浸式大概可以分成三個階段:
- Android4.4(API 19) - Android 5.0(API 21):這個階段可以實現沉浸式,但是表現得還不是很好,實現方式為: 通過FLAG_TRANSLUCENT_STATUS設置狀態欄為透明并且為全屏模式,然后通過添加一個與StatusBar 一樣大小的View,將View 的 background 設置為我們想要的顏色,從而來實現沉浸式;
- Android 5.0(API 21)以上版本:在Android 5.0的時候,加入了一個重要的屬性和方法 android:statusBarColor (對應方法為 setStatusBarColor),通過這個方法我們就可以輕松實現沉浸式。也就是說,從Android5.0開始,系統才真正的支持沉浸式;
- Android 6.0(API 23)以上版本:其實Android6.0以上的實現方式和Android 5.0 +是一樣,為什么要將它歸為一個單獨重要的階段呢?是因為從Android 6.0(API 23)開始,我們可以改狀態欄的繪制模式,可以顯示白色或淺黑色的內容和圖標(除了魅族手機,魅族自家有做源碼更改,6.0以下就能實現);
1、Android4.4(API 19)- Android 5.0(API 21)
Android在4.4新增了一個重要的屬性:FLAG_TRANSLUCENT_STATUS
- /**
- * Window flag: request a translucent status bar with minimal system-provided
- * background protection.
- *
- * <p>This flag can be controlled in your theme through the
- * {@link android.R.attr#windowTranslucentStatus} attribute; this attribute
- * is automatically set for you in the standard translucent decor themes
- * such as
- * {@link android.R.style#Theme_Holo_NoActionBar_TranslucentDecor},
- * {@link android.R.style#Theme_Holo_Light_NoActionBar_TranslucentDecor},
- * {@link android.R.style#Theme_DeviceDefault_NoActionBar_TranslucentDecor}, and
- * {@link android.R.style#Theme_DeviceDefault_Light_NoActionBar_TranslucentDecor}.</p>
- *
- * <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>
- */
- public static final int FLAG_TRANSLUCENT_STATUS = 0x04000000;
設置狀態欄透明,并且變為全屏模式。當這個屬性有效的時候,會自動設置 system ui visibility的標志SYSTEM_UI_FLAG_LAYOUT_STABLE和SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 。
通過 FLAG_TRANSLUCENT_STATUS 設置狀態欄為透明并且為全屏模式,然后通過添加一個與 StatusBar 一樣大小的 View,將 View 的 backgroud 設置為我們想要的顏色,從而實現沉浸式。
①, 設置 FLAG_TRANSLUCENT_STATUS,可以在代碼中設置,如下:
- activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
或者可以在 theme 設置屬性 windowTranslucentStatus,如下:
②.根據有需要,設置一個和 StatusBar 一樣大小的占位 View,如果不設置則內容 View 會向上頂一個 StattusBar 的高度。
圖片延伸到狀態欄只需要設置FLAG_TRANSLUCENT_STATUS就可以
添加占位View的代碼如下:
- //獲取decorView
- ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
- int count = decorView.getChildCount();
- //判斷是否已經添加了statusBarView
- if (count > 0 && decorView.getChildAt(count - 1) instanceof StatusBarView) {
- decorView.getChildAt(count - 1).setBackgroundColor(calculateStatusColor(color, statusBarAlpha));
- } else {
- //新建一個和狀態欄高寬的view
- StatusBarView statusView = createStatusBarView(activity, color, statusBarAlpha);
- decorView.addView(statusView);
- }
- ViewGroup rootView = (ViewGroup) ((ViewGroup) activity.findViewById(android.R.id.content)).getChildAt(0);
- //rootview不會為狀態欄留出狀態欄空間
- ViewCompat.setFitsSystemWindows(rootView,true);
- rootView.setClipToPadding(true);
- private static StatusBarView createStatusBarView(Activity activity, int color, int alpha) {
- // 繪制一個和狀態欄一樣高的矩形
- StatusBarView statusBarView = new StatusBarView(activity);
- LinearLayout.LayoutParams params =
- new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, getStatusBarHeight(activity));
- statusBarView.setLayoutParams(params);
- statusBarView.setBackgroundColor(calculateStatusColor(color, alpha));
- return statusBarView;
- }
2、Android 5.0(API 21)以上版本
Android 5.0 是一個里程碑式的版本,google 加入了一個比較重要的方法 setStatusBarColor (對應屬性:android:statusBarColor), 通過這個方法,可以很輕松地實現沉浸式狀態欄。方法如下:
- /**
- * Sets the color of the status bar to {@code color}.
- *
- * For this to take effect,
- * the window must be drawing the system bar backgrounds with
- * {@link android.view.WindowManager.LayoutParams#FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS} and
- * {@link android.view.WindowManager.LayoutParams#FLAG_TRANSLUCENT_STATUS} must not be set.
- *
- * If {@code color} is not opaque, consider setting
- * {@link android.view.View#SYSTEM_UI_FLAG_LAYOUT_STABLE} and
- * {@link android.view.View#SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN}.
- * <p>
- * The transitionName for the view background will be "android:status:background".
- * </p>
- */
- public abstract void setStatusBarColor(@ColorInt int color);
不過,要想這個方法生效,必須還要配合一個 Flag 一起使用,必須設置 FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS,并且不能設置 FLAG_TRANSLUCENT_STATUS (Android 4.4 才用這個)。
設置了 FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS 表明 Window 負責系統 bar 的 background 繪制,繪制透明背景的系統 bar(狀態欄和導航欄),然后用 getStatusBarColor() 和 getNavigationBarColor() 的顏色填充相應的區域,實現代碼如下:
- getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
- //注意要清除 FLAG_TRANSLUCENT_STATUS flag
- getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
- getWindow().setStatusBarColor(getResources().getColor(android.R.color.holo_red_light));
也可以直接在 Theme 中使用,在 vlues-v21 文件夾下添加如下主題:
- <style name="MDTheme" parent="Theme.Design.Light.NoActionBar">
- <item name="android:windowTranslucentStatus">false</item>
- <item name="android:windowDrawsSystemBarBackgrounds">true</item>
- <item name="android:statusBarColor">@android:color/holo_red_light</item>
- </style>
如果要讓圖片延申至狀態欄,只需設置 windowTranslucentStatus,將 statusBarColor 設置為透明,同時設置 DecorView 的 屬性:
- getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN |
- View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
3、Android 6.0 +
其實Android6.0以上的實現方式和Android 5.0 +是一樣,為什么要將它歸為一個單獨重要的階段呢?是因為從Android 6.0(API 23)開始,我們可以改狀態欄的繪制模式,可以顯示白色或淺黑色的內容和圖標;
使用Android6.0 以上版本沉浸式的時候會遇到一個問題,那就是 Android 系統狀態欄的字色和圖標顏色為白色,當狀態欄顏色接近淺色的時候,狀態欄上的內容就看不清了;
Android 6.0 新添加了一個屬性來解決這個問題,屬性是 SYSTEM_UI_FLAG_LIGHT_STATUS_BAR,可以設置狀態欄字色和圖標淺黑色。
- /**
- * Flag for {@link #setSystemUiVisibility(int)}: Requests the status bar to draw in a mode that
- * is compatible with light status bar backgrounds.
- *
- * <p>For this to take effect, the window must request
- * {@link android.view.WindowManager.LayoutParams#FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS
- * FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS} but not
- * {@link android.view.WindowManager.LayoutParams#FLAG_TRANSLUCENT_STATUS
- * FLAG_TRANSLUCENT_STATUS}.
- *
- * @see android.R.attr#windowLightStatusBar
- */
- public static final int SYSTEM_UI_FLAG_LIGHT_STATUS_BAR = 0x00002000;
不過要想這個屬性生效的前提是要先設置了FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS flag ,同時清除了FLAG_TRANSLUCENT_STATUS flag 才會生效。
(1)狀態欄字體白色
- getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);//字體默認白色
- getWindow().setStatusBarColor(android.R.color.transparent);//透明背景
(2)狀態欄字體黑色
- getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN|View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);//黑色字體
- getWindow().setStatusBarColor(android.R.color.transparent);//透明背景
三、實際沉浸式開發中的難點分析
1、沉浸式中常用的flag總結
①. View.SYSTEM_UI_FLAG_FULLSCREEN:Activity全屏顯示,且狀態欄被隱藏覆蓋掉
②.View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN:Activity全屏顯示,但狀態欄不會被隱藏覆蓋,狀態欄依然可見,
③. View.SYSTEM_UI_FLAG_LAYOUT_STABLE
使用了SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN和SYSTEM_UI_FLAG_LAYOUT_STABLE,注意兩個Flag必須要結合在一起使用,表示會讓應用的主體內容占用系統狀態欄的空間
④. View.SYSTEM_UI_FLAG_HIDE_NAVIGATION:隱藏虛擬按鍵(導航欄)。有些手機會用虛擬按鍵來代替物理按鍵。
⑤. View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION:隱藏導航欄 效果同View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
⑥. 有的手機默認全屏顯示,有時需要強制不顯示全屏就用以下flag
不全屏顯示
- getWindow().addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
全屏顯示
- getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
2、狀態欄字體顏色適配
- /***
- * 狀態欄字體適配方案
- * @param activity
- * @param dark
- */
- public static void darkMode(Activity activity, boolean dark) {
- try {
- if (isFlyme4Later()) {
- //魅族
- darkModeForFlyme4(activity.getWindow(), dark);
- } else if (isMIUI6Later()) {
- //小米
- darkModeForMIUI6(activity.getWindow(), dark);
- } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- //其他通用方案
- darkModeForM(activity.getWindow(), dark);
- }
- } catch (Exception e) {
- }
- }
- /***
- * 狀態欄字體適配方案
- * @param activity
- * @param dark
- */
- public static void darkMode(Activity activity, boolean dark) {
- try {
- if (isFlyme4Later()) {
- //魅族
- darkModeForFlyme4(activity.getWindow(), dark);
- } else if (isMIUI6Later()) {
- //小米
- darkModeForMIUI6(activity.getWindow(), dark);
- } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- //其他通用方案
- darkModeForM(activity.getWindow(), dark);
- }
- } catch (Exception e) {
- }
- }
- /**
- * 判斷是否Flyme4以上
- */
- public static boolean isFlyme4Later() {
- return Build.FINGERPRINT.contains("Flyme_OS_4")
- || Build.VERSION.INCREMENTAL.contains("Flyme_OS_4")
- || Pattern.compile("Flyme OS [4|5]", Pattern.CASE_INSENSITIVE).matcher(Build.DISPLAY).find();
- }
- /**
- * 判斷是否為MIUI6以上
- */
- @SuppressLint("PrivateApi")
- public static boolean isMIUI6Later() {
- try {
- Class<?> clz = Class.forName("android.os.SystemProperties");
- Method mtd = clz.getMethod("get", String.class);
- String val = (String) mtd.invoke(null, "ro.miui.ui.version.name");
- assert val != null;
- val = val.replaceAll("[vV]", "");
- int version = Integer.parseInt(val);
- return version >= 6;
- } catch (Exception e) {
- return false;
- }
- }
- /**
- * android 6.0設置字體顏色
- */
- @RequiresApi(Build.VERSION_CODES.M)
- private static void darkModeForM(Window window, boolean dark) {
- window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
- window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
- window.setStatusBarColor(Color.TRANSPARENT);
- int systemUiVisibility = window.getDecorView().getSystemUiVisibility();
- if (dark) {
- systemUiVisibility |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
- } else {
- systemUiVisibility &= ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
- }
- window.getDecorView().setSystemUiVisibility(systemUiVisibility);
- }
3、fitsSystemWindows理解和用法
- 在實現沉浸式狀態欄時,我們會用到android:fitsSystemWindows="true"這個屬性;
- 設置了透明狀態欄(StatusBar)或者導航欄(NavigationBar)之后,activity的內容會延伸至對應的區域,使得該區域出現重疊現象,這對內容包含交互控件的情況影響尤其巨大,為了解決這個情況,fitsSystemWindows屬性出現了,我們可以為任何view添加此屬性,設置了該屬性的view的所有padding屬性將失效,并且系統會根據情況給該view添加paddingTop和paddingBottom(當設置透明狀態欄時,系統會為該view添加一個值等于狀態欄高度的paddingTop,當設置了透明導航欄時,系統會為該view添加一個值等于導航欄高度的paddingBottom);
- 在默認情況下,多個view設置該屬性時,只有最外層的view才會起作用;我們也可以通過覆寫自定義view的一些方法來決定自身的處理,及子view是否有機會截斷并對fitsSystemWindows做出自己的反應,如DrawerLayout、CoordinatorLayout和CollapsingToolbarLayout就使用了自定義fitsSystemWindow(難怪給drawerLayout設置該屬性時和我們理解的行為不一致)
- 要實現的效果有以下兩種:背景圖片填滿了整個屏幕、狀態欄和actionBar顏色一致。
我們只需要把內容延伸至狀態欄和導航欄,然后給根布局設置圖片背景,若需要內容不出現在狀態欄和導航欄區域則再添加android:fitsSystemWindows="true"既可
- /**
- * 獲取狀態欄高度
- */
- public static int getStatusBarHeight(Context context) {
- int result = 24;
- int resId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
- if (resId > 0) {
- result = context.getResources().getDimensionPixelSize(resId);
- } else {
- result = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
- result, Resources.getSystem().getDisplayMetrics());
- }
- return result;
- }
四、沉浸式輪子方案
其實網上有很多成熟的沉浸式方案,我們也沒有必要封裝,主要是要了解其中的知識點,遇到問題好排查問題
網上的輪子StatusBarUtil
有以下的功能:
1、設置狀態欄顏色
- StatusBarUtil.setColor(Activity activity, int color)
設置狀態欄半透明
2、StatusBarUtil.setTranslucent(Activity activity, int statusBarAlpha)
設置狀態欄全透明
- StatusBarUtil.setTransparent(Activity activity)
3、為包含 DrawerLayout 的界面設置狀態欄顏色(也可以設置半透明和全透明)
- StatusBarUtil.setColorForDrawerLayout(Activity activity, DrawerLayout drawerLayout, int color)
4、為使用 ImageView 作為頭部的界面設置狀態欄透明
- StatusBarUtil.setTranslucentForImageView(Activity activity, int statusBarAlpha, View needOffsetView)
5、在 Fragment 中使用
6、通過傳入 statusBarAlpha 參數,可以改變狀態欄的透明度值,默認值是112。
總結:
這次知識點總結,希望可以給還沒有使用沉浸式的同學一些幫助。如果你已經使用過沉浸式狀態欄,可以對各個版本實現的原理有一個更深的了解。