本文转载自微信公众号「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。
总结:
这次知识点总结,希望可以给还没有使用沉浸式的同学一些帮助。如果你已经使用过沉浸式状态栏,可以对各个版本实现的原理有一个更深的了解。