Android進階之沉浸式狀態(tài)欄原理和使用詳解
本文轉(zhuǎn)載自微信公眾號「Android開發(fā)編程」,作者Android開發(fā)編程。轉(zhuǎn)載本文請聯(lián)系A(chǔ)ndroid開發(fā)編程公眾號。
前言
沉浸式就是要給用戶提供完全沉浸的體驗,使用戶有一種置身于虛擬世界之中的感覺;
這種體驗在各類游戲中被廣泛應(yīng)用,絕大部分的游戲都會在打開后,使得屏幕被完全被游戲占據(jù),讓玩家沉浸其中,從體驗上沉浸式效果會更好一些;
網(wǎng)上很多人都寫過沉浸式原理的文章,講解的都不是很清晰,讓人很費解;
今天我們就來徹底的總結(jié)下沉浸式實現(xiàn)和原理;
一、沉浸式概念和為何要用沉浸式
1、沉浸式概念
- Android系統(tǒng)4.4之前狀態(tài)欄一直是黑色的,在4.4中帶來了 windowTranslucentStatus 這一特性,開始引出“沉浸式狀態(tài)欄”這個概念。Google 在 Android 4.4 的 API 描述頁面里提到了“Translucent system UI styling”,即透明化的系統(tǒng)UI風(fēng)格。
- “沉浸式狀態(tài)欄”準確來說應(yīng)該是“透明欄”,是 4.4 新定義的設(shè)計規(guī)范;
- 簡單來說就是在軟件打開的時候通知欄和軟件頂部顏色融為一體,這樣可以使軟件和系統(tǒng)本身更加符合,同時通知欄的顏色不再是白色、黑色簡單的兩種了;
- 沉浸式表示全屏顯示手機屏幕是沒有手機里面自帶的任何控件;
2、為何要用沉浸式
- 如果App里面目前都沒有做沉浸式狀態(tài)欄,會導(dǎo)致狀態(tài)欄呈黑色條狀,而且下面這個的黑色(白色)條狀與App主界面有很明顯的區(qū)別。這樣在一定程度上犧牲了視覺高度,界面面積變小,最主要的是用戶的視覺和體驗;
- 說白了,用戶體驗好,用的爽,留存就高,那么領(lǐng)導(dǎo)肯定讓開發(fā)沉浸式主題樣式;
二、沉浸式原理和兼容
從Android4.4 到現(xiàn)在(Android 7.1),關(guān)于沉浸式大概可以分成三個階段:
- Android4.4(API 19) - Android 5.0(API 21):這個階段可以實現(xiàn)沉浸式,但是表現(xiàn)得還不是很好,實現(xiàn)方式為: 通過FLAG_TRANSLUCENT_STATUS設(shè)置狀態(tài)欄為透明并且為全屏模式,然后通過添加一個與StatusBar 一樣大小的View,將View 的 background 設(shè)置為我們想要的顏色,從而來實現(xiàn)沉浸式;
- Android 5.0(API 21)以上版本:在Android 5.0的時候,加入了一個重要的屬性和方法 android:statusBarColor (對應(yīng)方法為 setStatusBarColor),通過這個方法我們就可以輕松實現(xiàn)沉浸式。也就是說,從Android5.0開始,系統(tǒng)才真正的支持沉浸式;
- Android 6.0(API 23)以上版本:其實Android6.0以上的實現(xiàn)方式和Android 5.0 +是一樣,為什么要將它歸為一個單獨重要的階段呢?是因為從Android 6.0(API 23)開始,我們可以改狀態(tài)欄的繪制模式,可以顯示白色或淺黑色的內(nèi)容和圖標(除了魅族手機,魅族自家有做源碼更改,6.0以下就能實現(xiàn));
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;
設(shè)置狀態(tài)欄透明,并且變?yōu)槿聊J?。?dāng)這個屬性有效的時候,會自動設(shè)置 system ui visibility的標志SYSTEM_UI_FLAG_LAYOUT_STABLE和SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 。
通過 FLAG_TRANSLUCENT_STATUS 設(shè)置狀態(tài)欄為透明并且為全屏模式,然后通過添加一個與 StatusBar 一樣大小的 View,將 View 的 backgroud 設(shè)置為我們想要的顏色,從而實現(xiàn)沉浸式。
①, 設(shè)置 FLAG_TRANSLUCENT_STATUS,可以在代碼中設(shè)置,如下:
- activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
或者可以在 theme 設(shè)置屬性 windowTranslucentStatus,如下:
②.根據(jù)有需要,設(shè)置一個和 StatusBar 一樣大小的占位 View,如果不設(shè)置則內(nèi)容 View 會向上頂一個 StattusBar 的高度。
圖片延伸到狀態(tài)欄只需要設(shè)置FLAG_TRANSLUCENT_STATUS就可以
添加占位View的代碼如下:
- //獲取decorView
- ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
- int count = decorView.getChildCount();
- //判斷是否已經(jīng)添加了statusBarView
- if (count > 0 && decorView.getChildAt(count - 1) instanceof StatusBarView) {
- decorView.getChildAt(count - 1).setBackgroundColor(calculateStatusColor(color, statusBarAlpha));
- } else {
- //新建一個和狀態(tài)欄高寬的view
- StatusBarView statusView = createStatusBarView(activity, color, statusBarAlpha);
- decorView.addView(statusView);
- }
- ViewGroup rootView = (ViewGroup) ((ViewGroup) activity.findViewById(android.R.id.content)).getChildAt(0);
- //rootview不會為狀態(tài)欄留出狀態(tài)欄空間
- ViewCompat.setFitsSystemWindows(rootView,true);
- rootView.setClipToPadding(true);
- private static StatusBarView createStatusBarView(Activity activity, int color, int alpha) {
- // 繪制一個和狀態(tài)欄一樣高的矩形
- 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 (對應(yīng)屬性:android:statusBarColor), 通過這個方法,可以很輕松地實現(xiàn)沉浸式狀態(tài)欄。方法如下:
- /**
- * 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 一起使用,必須設(shè)置 FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS,并且不能設(shè)置 FLAG_TRANSLUCENT_STATUS (Android 4.4 才用這個)。
設(shè)置了 FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS 表明 Window 負責(zé)系統(tǒng) bar 的 background 繪制,繪制透明背景的系統(tǒng) bar(狀態(tài)欄和導(dǎo)航欄),然后用 getStatusBarColor() 和 getNavigationBarColor() 的顏色填充相應(yīng)的區(qū)域,實現(xiàn)代碼如下:
- 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>
如果要讓圖片延申至狀態(tài)欄,只需設(shè)置 windowTranslucentStatus,將 statusBarColor 設(shè)置為透明,同時設(shè)置 DecorView 的 屬性:
- getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN |
- View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
3、Android 6.0 +
其實Android6.0以上的實現(xiàn)方式和Android 5.0 +是一樣,為什么要將它歸為一個單獨重要的階段呢?是因為從Android 6.0(API 23)開始,我們可以改狀態(tài)欄的繪制模式,可以顯示白色或淺黑色的內(nèi)容和圖標;
使用Android6.0 以上版本沉浸式的時候會遇到一個問題,那就是 Android 系統(tǒng)狀態(tài)欄的字色和圖標顏色為白色,當(dāng)狀態(tài)欄顏色接近淺色的時候,狀態(tài)欄上的內(nèi)容就看不清了;
Android 6.0 新添加了一個屬性來解決這個問題,屬性是 SYSTEM_UI_FLAG_LIGHT_STATUS_BAR,可以設(shè)置狀態(tài)欄字色和圖標淺黑色。
- /**
- * 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;
不過要想這個屬性生效的前提是要先設(shè)置了FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS flag ,同時清除了FLAG_TRANSLUCENT_STATUS flag 才會生效。
(1)狀態(tài)欄字體白色
- getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);//字體默認白色
- getWindow().setStatusBarColor(android.R.color.transparent);//透明背景
(2)狀態(tài)欄字體黑色
- getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN|View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);//黑色字體
- getWindow().setStatusBarColor(android.R.color.transparent);//透明背景
三、實際沉浸式開發(fā)中的難點分析
1、沉浸式中常用的flag總結(jié)
①. View.SYSTEM_UI_FLAG_FULLSCREEN:Activity全屏顯示,且狀態(tài)欄被隱藏覆蓋掉
②.View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN:Activity全屏顯示,但狀態(tài)欄不會被隱藏覆蓋,狀態(tài)欄依然可見,
③. View.SYSTEM_UI_FLAG_LAYOUT_STABLE
使用了SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN和SYSTEM_UI_FLAG_LAYOUT_STABLE,注意兩個Flag必須要結(jié)合在一起使用,表示會讓應(yīng)用的主體內(nèi)容占用系統(tǒng)狀態(tài)欄的空間
④. View.SYSTEM_UI_FLAG_HIDE_NAVIGATION:隱藏虛擬按鍵(導(dǎo)航欄)。有些手機會用虛擬按鍵來代替物理按鍵。
⑤. View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION:隱藏導(dǎo)航欄 效果同View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
⑥. 有的手機默認全屏顯示,有時需要強制不顯示全屏就用以下flag
不全屏顯示
- getWindow().addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
全屏顯示
- getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
2、狀態(tài)欄字體顏色適配
- /***
- * 狀態(tài)欄字體適配方案
- * @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) {
- }
- }
- /***
- * 狀態(tài)欄字體適配方案
- * @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設(shè)置字體顏色
- */
- @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理解和用法
- 在實現(xiàn)沉浸式狀態(tài)欄時,我們會用到android:fitsSystemWindows="true"這個屬性;
- 設(shè)置了透明狀態(tài)欄(StatusBar)或者導(dǎo)航欄(NavigationBar)之后,activity的內(nèi)容會延伸至對應(yīng)的區(qū)域,使得該區(qū)域出現(xiàn)重疊現(xiàn)象,這對內(nèi)容包含交互控件的情況影響尤其巨大,為了解決這個情況,fitsSystemWindows屬性出現(xiàn)了,我們可以為任何view添加此屬性,設(shè)置了該屬性的view的所有padding屬性將失效,并且系統(tǒng)會根據(jù)情況給該view添加paddingTop和paddingBottom(當(dāng)設(shè)置透明狀態(tài)欄時,系統(tǒng)會為該view添加一個值等于狀態(tài)欄高度的paddingTop,當(dāng)設(shè)置了透明導(dǎo)航欄時,系統(tǒng)會為該view添加一個值等于導(dǎo)航欄高度的paddingBottom);
- 在默認情況下,多個view設(shè)置該屬性時,只有最外層的view才會起作用;我們也可以通過覆寫自定義view的一些方法來決定自身的處理,及子view是否有機會截斷并對fitsSystemWindows做出自己的反應(yīng),如DrawerLayout、CoordinatorLayout和CollapsingToolbarLayout就使用了自定義fitsSystemWindow(難怪給drawerLayout設(shè)置該屬性時和我們理解的行為不一致)
- 要實現(xiàn)的效果有以下兩種:背景圖片填滿了整個屏幕、狀態(tài)欄和actionBar顏色一致。
我們只需要把內(nèi)容延伸至狀態(tài)欄和導(dǎo)航欄,然后給根布局設(shè)置圖片背景,若需要內(nèi)容不出現(xiàn)在狀態(tài)欄和導(dǎo)航欄區(qū)域則再添加android:fitsSystemWindows="true"既可
- /**
- * 獲取狀態(tài)欄高度
- */
- 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;
- }
四、沉浸式輪子方案
其實網(wǎng)上有很多成熟的沉浸式方案,我們也沒有必要封裝,主要是要了解其中的知識點,遇到問題好排查問題
網(wǎng)上的輪子StatusBarUtil
有以下的功能:
1、設(shè)置狀態(tài)欄顏色
- StatusBarUtil.setColor(Activity activity, int color)
設(shè)置狀態(tài)欄半透明
2、StatusBarUtil.setTranslucent(Activity activity, int statusBarAlpha)
設(shè)置狀態(tài)欄全透明
- StatusBarUtil.setTransparent(Activity activity)
3、為包含 DrawerLayout 的界面設(shè)置狀態(tài)欄顏色(也可以設(shè)置半透明和全透明)
- StatusBarUtil.setColorForDrawerLayout(Activity activity, DrawerLayout drawerLayout, int color)
4、為使用 ImageView 作為頭部的界面設(shè)置狀態(tài)欄透明
- StatusBarUtil.setTranslucentForImageView(Activity activity, int statusBarAlpha, View needOffsetView)
5、在 Fragment 中使用
6、通過傳入 statusBarAlpha 參數(shù),可以改變狀態(tài)欄的透明度值,默認值是112。
總結(jié):
這次知識點總結(jié),希望可以給還沒有使用沉浸式的同學(xué)一些幫助。如果你已經(jīng)使用過沉浸式狀態(tài)欄,可以對各個版本實現(xiàn)的原理有一個更深的了解。