Android进阶之沉浸式状态栏原理和使用详解

移动开发 Android
Android系统4.4之前状态栏一直是黑色的,在4.4中带来了 windowTranslucentStatus 这一特性,开始引出“沉浸式状态栏”这个概念。Google 在 Android 4.4 的 API 描述页面里提到了“Translucent system UI styling”,即透明化的系统UI风格。

[[416377]]

本文转载自微信公众号「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

  1. /** 
  2.   * Window flag: request a translucent status bar with minimal system-provided 
  3.   * background protection. 
  4.   * 
  5.   * <p>This flag can be controlled in your theme through the 
  6.   * {@link android.R.attr#windowTranslucentStatus} attribute; this attribute 
  7.   * is automatically set for you in the standard translucent decor themes 
  8.   * such as 
  9.   * {@link android.R.style#Theme_Holo_NoActionBar_TranslucentDecor}, 
  10.   * {@link android.R.style#Theme_Holo_Light_NoActionBar_TranslucentDecor}, 
  11.   * {@link android.R.style#Theme_DeviceDefault_NoActionBar_TranslucentDecor}, and 
  12.   * {@link android.R.style#Theme_DeviceDefault_Light_NoActionBar_TranslucentDecor}.</p> 
  13.   * 
  14.   * <p>When this flag is enabled for a window, it automatically sets 
  15.   * the system UI visibility flags {@link View#SYSTEM_UI_FLAG_LAYOUT_STABLE} and 
  16.   * {@link View#SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN}.</p> 
  17.   */ 
  18.  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,可以在代码中设置,如下:

  1. activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); 

或者可以在 theme 设置属性 windowTranslucentStatus,如下:

②.根据有需要,设置一个和 StatusBar 一样大小的占位 View,如果不设置则内容 View 会向上顶一个 StattusBar 的高度。

图片延伸到状态栏只需要设置FLAG_TRANSLUCENT_STATUS就可以

添加占位View的代码如下:

  1. //获取decorView 
  2.         ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView(); 
  3.         int count = decorView.getChildCount(); 
  4.         //判断是否已经添加了statusBarView 
  5.         if (count > 0 && decorView.getChildAt(count - 1) instanceof StatusBarView) { 
  6.             decorView.getChildAt(count - 1).setBackgroundColor(calculateStatusColor(color, statusBarAlpha)); 
  7.         } else { 
  8.             //新建一个和状态栏高宽的view 
  9.             StatusBarView statusView = createStatusBarView(activity, color, statusBarAlpha); 
  10.             decorView.addView(statusView); 
  11.         } 
  12.         ViewGroup rootView = (ViewGroup) ((ViewGroup) activity.findViewById(android.R.id.content)).getChildAt(0); 
  13.         //rootview不会为状态栏留出状态栏空间 
  14.         ViewCompat.setFitsSystemWindows(rootView,true); 
  15.         rootView.setClipToPadding(true); 
  16. private static StatusBarView createStatusBarView(Activity activity, int color, int alpha) { 
  17.         // 绘制一个和状态栏一样高的矩形 
  18.         StatusBarView statusBarView = new StatusBarView(activity); 
  19.         LinearLayout.LayoutParams params = 
  20.                 new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, getStatusBarHeight(activity)); 
  21.         statusBarView.setLayoutParams(params); 
  22.         statusBarView.setBackgroundColor(calculateStatusColor(color, alpha)); 
  23.         return statusBarView; 

2、Android 5.0(API 21)以上版本

Android 5.0 是一个里程碑式的版本,google 加入了一个比较重要的方法 setStatusBarColor (对应属性:android:statusBarColor), 通过这个方法,可以很轻松地实现沉浸式状态栏。方法如下:

  1. /** 
  2.      * Sets the color of the status bar to {@code color}. 
  3.      * 
  4.      * For this to take effect, 
  5.      * the window must be drawing the system bar backgrounds with 
  6.      * {@link android.view.WindowManager.LayoutParams#FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS} and 
  7.      * {@link android.view.WindowManager.LayoutParams#FLAG_TRANSLUCENT_STATUS} must not be set
  8.      * 
  9.      * If {@code color} is not opaque, consider setting 
  10.      * {@link android.view.View#SYSTEM_UI_FLAG_LAYOUT_STABLE} and 
  11.      * {@link android.view.View#SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN}. 
  12.      * <p> 
  13.      * The transitionName for the view background will be "android:status:background"
  14.      * </p> 
  15.      */ 
  16.     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() 的颜色填充相应的区域,实现代码如下:

  1. getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); 
  2. //注意要清除 FLAG_TRANSLUCENT_STATUS flag 
  3. getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); 
  4. getWindow().setStatusBarColor(getResources().getColor(android.R.color.holo_red_light)); 

也可以直接在 Theme 中使用,在 vlues-v21 文件夹下添加如下主题:

  1. <style name="MDTheme" parent="Theme.Design.Light.NoActionBar"
  2.         <item name="android:windowTranslucentStatus">false</item> 
  3.         <item name="android:windowDrawsSystemBarBackgrounds">true</item> 
  4.         <item name="android:statusBarColor">@android:color/holo_red_light</item> 
  5. </style> 

如果要让图片延申至状态栏,只需设置 windowTranslucentStatus,将 statusBarColor 设置为透明,同时设置 DecorView 的 属性:

  1. getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN |  
  2. 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,可以设置状态栏字色和图标浅黑色。

  1. /** 
  2.      * Flag for {@link #setSystemUiVisibility(int)}: Requests the status bar to draw in a mode that 
  3.      * is compatible with light status bar backgrounds. 
  4.      * 
  5.      * <p>For this to take effect, the window must request 
  6.      * {@link android.view.WindowManager.LayoutParams#FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS 
  7.      *         FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS} but not 
  8.      * {@link android.view.WindowManager.LayoutParams#FLAG_TRANSLUCENT_STATUS 
  9.      *         FLAG_TRANSLUCENT_STATUS}. 
  10.      * 
  11.      * @see android.R.attr#windowLightStatusBar 
  12.      */ 
  13.     public static final int SYSTEM_UI_FLAG_LIGHT_STATUS_BAR = 0x00002000; 

不过要想这个属性生效的前提是要先设置了FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS flag ,同时清除了FLAG_TRANSLUCENT_STATUS flag 才会生效。

(1)状态栏字体白色

  1. getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);//字体默认白色 
  2. getWindow().setStatusBarColor(android.R.color.transparent);//透明背景 

(2)状态栏字体黑色

  1. getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN|View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);//黑色字体 
  2. 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

不全屏显示

  1. getWindow().addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); 

全屏显示

  1. getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); 

2、状态栏字体颜色适配

  1. /*** 
  2.     * 状态栏字体适配方案 
  3.     * @param activity 
  4.     * @param dark 
  5.     */ 
  6.    public static void darkMode(Activity activity, boolean dark) { 
  7.        try { 
  8.            if (isFlyme4Later()) { 
  9.                //魅族 
  10.                darkModeForFlyme4(activity.getWindow(), dark); 
  11.            } else if (isMIUI6Later()) { 
  12.                //小米 
  13.                darkModeForMIUI6(activity.getWindow(), dark); 
  14.            } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 
  15.                //其他通用方案 
  16.                darkModeForM(activity.getWindow(), dark); 
  17.            } 
  18.        } catch (Exception e) { 
  19.        } 
  20.    } 
  21.  /*** 
  22.     * 状态栏字体适配方案 
  23.     * @param activity 
  24.     * @param dark 
  25.     */ 
  26.    public static void darkMode(Activity activity, boolean dark) { 
  27.        try { 
  28.            if (isFlyme4Later()) { 
  29.                //魅族 
  30.                darkModeForFlyme4(activity.getWindow(), dark); 
  31.            } else if (isMIUI6Later()) { 
  32.                //小米 
  33.                darkModeForMIUI6(activity.getWindow(), dark); 
  34.            } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 
  35.                //其他通用方案 
  36.                darkModeForM(activity.getWindow(), dark); 
  37.            } 
  38.        } catch (Exception e) { 
  39.        } 
  40.    } 
  41.   /** 
  42.     * 判断是否Flyme4以上 
  43.     */ 
  44.    public static boolean isFlyme4Later() { 
  45.        return Build.FINGERPRINT.contains("Flyme_OS_4"
  46.                || Build.VERSION.INCREMENTAL.contains("Flyme_OS_4"
  47.                || Pattern.compile("Flyme OS [4|5]", Pattern.CASE_INSENSITIVE).matcher(Build.DISPLAY).find(); 
  48.    } 
  49.  /** 
  50.     * 判断是否为MIUI6以上 
  51.     */ 
  52.    @SuppressLint("PrivateApi"
  53.    public static boolean isMIUI6Later() { 
  54.        try { 
  55.            Class<?> clz = Class.forName("android.os.SystemProperties"); 
  56.            Method mtd = clz.getMethod("get", String.class); 
  57.            String val = (String) mtd.invoke(null"ro.miui.ui.version.name"); 
  58.            assert val != null
  59.            val = val.replaceAll("[vV]"""); 
  60.            int version = Integer.parseInt(val); 
  61.            return version >= 6; 
  62.        } catch (Exception e) { 
  63.            return false
  64.        } 
  65.    } 
  66.   /** 
  67.     * android 6.0设置字体颜色 
  68.     */ 
  69.    @RequiresApi(Build.VERSION_CODES.M) 
  70.    private static void darkModeForM(Window window, boolean dark) { 
  71.        window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); 
  72.        window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); 
  73.        window.setStatusBarColor(Color.TRANSPARENT); 
  74.        int systemUiVisibility = window.getDecorView().getSystemUiVisibility(); 
  75.        if (dark) { 
  76.            systemUiVisibility |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR; 
  77.        } else { 
  78.            systemUiVisibility &= ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR; 
  79.        } 
  80.        window.getDecorView().setSystemUiVisibility(systemUiVisibility); 
  81.    } 

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"既可

  1. /** 
  2.      * 获取状态栏高度 
  3.      */ 
  4.     public static int getStatusBarHeight(Context context) { 
  5.         int result = 24; 
  6.         int resId = context.getResources().getIdentifier("status_bar_height""dimen""android"); 
  7.         if (resId > 0) { 
  8.             result = context.getResources().getDimensionPixelSize(resId); 
  9.         } else { 
  10.             result = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 
  11.                     result, Resources.getSystem().getDisplayMetrics()); 
  12.         } 
  13.         return result; 
  14.     } 

四、沉浸式轮子方案

其实网上有很多成熟的沉浸式方案,我们也没有必要封装,主要是要了解其中的知识点,遇到问题好排查问题

网上的轮子StatusBarUtil

有以下的功能:

1、设置状态栏颜色

  1. StatusBarUtil.setColor(Activity activity, int color) 

设置状态栏半透明

2、StatusBarUtil.setTranslucent(Activity activity, int statusBarAlpha)

设置状态栏全透明

  1. StatusBarUtil.setTransparent(Activity activity) 

3、为包含 DrawerLayout 的界面设置状态栏颜色(也可以设置半透明和全透明)

  1. StatusBarUtil.setColorForDrawerLayout(Activity activity, DrawerLayout drawerLayout, int color) 

4、为使用 ImageView 作为头部的界面设置状态栏透明

  1. StatusBarUtil.setTranslucentForImageView(Activity activity, int statusBarAlpha, View needOffsetView) 

5、在 Fragment 中使用

6、通过传入 statusBarAlpha 参数,可以改变状态栏的透明度值,默认值是112。

总结:

 

这次知识点总结,希望可以给还没有使用沉浸式的同学一些帮助。如果你已经使用过沉浸式状态栏,可以对各个版本实现的原理有一个更深的了解。

 

责任编辑:武晓燕 来源: Android开发编程
相关推荐

2017-02-17 11:00:57

状态栏Android

2016-11-29 11:20:08

Android

2017-12-05 12:44:57

Android沉浸式状态栏APP

2022-11-23 14:47:29

北向开发鸿蒙

2014-06-06 14:03:13

iOS状态栏提示控件原理

2021-09-07 06:40:25

AndroidLiveData原理

2013-07-18 16:09:10

自定义iOS状态栏iOS开发iOS学习

2021-09-01 06:48:16

AndroidGlide缓存

2021-09-06 13:12:05

前端JavaScript编程

2021-08-05 20:39:34

AndroidKotlinStandard.kt

2012-12-24 14:42:48

iOS自定义状态栏

2021-08-10 20:41:33

AndroidApp流程

2015-02-12 14:49:36

CGToast状态栏提示Status

2021-06-15 15:04:38

Android 12安卓通话

2021-08-17 13:41:11

AndroidView事件

2013-11-20 15:08:32

iOS开发状态栏

2021-09-02 07:00:01

Glide流程Android

2021-08-25 07:43:17

AndroidSurfaceViewTextureView

2021-06-15 15:28:31

谷歌Android开发

2021-08-11 17:15:17

AndroidActivity场景
点赞
收藏

51CTO技术栈公众号