Android 沉浸式状态栏和全面屏遇到刘海屏


在做项目的时候图片全屏预览时发现使用全面屏后,图片会闪动一下( 总结部分有答案),带着问题来梳理一下Android 全面屏、沉浸式状态栏和刘海屏的概念和方法。

普通的手机:
在这里插入图片描述

刘海屏的手机:

在这里插入图片描述

一、沉浸式状态栏

沉浸式状态栏就是ContentView中的内容占用系统状态栏的空间,状态栏和导航栏依然可见。
沉浸式状态栏对 Android 版本要求不一样,所以我们需要通过不同版本来进行判定区分,在 Android 4.4 一下,可以对 StatusBar 和 NavigationBar 进行显示和隐藏操作。直到 Android 4.4 ,才真正实现沉浸式状态栏。从 Android 4.4 (API 19)到 Android 12 (API 31),可以分为3个阶段:

1.1 Android 4.4(API 19)- Android 5.0( API 21)

这个阶段实现沉浸式是通过 WindowManager 的 FLAG_TRANSLUCENT_STATUS,这个窗口标志:允许窗口内容扩展到屏幕的顶部区域。
这种情况也是分两种情况:

  • 其一: 允许窗口内容扩展到屏幕的顶部区域,只需要设置 FLAG_TRANSLUCENT_STATUS标志;
  • 其二: 如果不想让内容窗口扩展到系统状态栏上,也就是说不让系统状态栏遮挡住内容。就设置 FLAG_TRANSLUCENT_STATUS标志而且添加一个与状态栏一样大小的 View ,将 View 的 background 设置我们需要的颜色;从而来实现沉浸式。
        /**
         * 窗口标志:请求具有最少系统提供的背景保护的半透明状态栏。
         *
         * 可以通过 {@link android.R.attrwindowTranslucentStatus} 属性在您的主题中控制此标志;
         * 此属性会在标准的半透明装饰主题中为您自动设置,例如
         * {@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}.
         *
         * 当为窗口启用此标志时,它会自动设置系统 UI 可见性标志 view.setSystemUiVisibility(int visibility) 
         * {@link View.SYSTEM_UI_FLAG_LAYOUT_STABLE} 和 {@link View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN}。
         *
         * 使用带有半透明颜色的 {@link WindowsetStatusBarColor(int)} 代替
         */
        @Deprecated
        public static final int FLAG_TRANSLUCENT_STATUS = 0x04000000;

上面的注释的比较清楚,当在在 theme 中设置属性 windowTranslucentStatus 为 true 的时候生效:

    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <!-- API 19 以上生效-->
        <item name="android:windowTranslucentStatus" tools:targetApi="kitkat">true</item>
    </style>

当我们在代码里设置

  window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)

还有一点值得注意的是:设置这个标志会自动调用系统的 systemUiVisibility 来设置全屏模式

 window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                    or View.SYSTEM_UI_FLAG_LAYOUT_STABLE)

效果如下:

在这里插入图片描述

当我们不希望ContentView 中的内容被遮挡,我们可以添加一个和状态栏高度一样的 View,来控制这个 View 的颜色,以此来实现沉浸式。

    private fun addStatusViewWithColor19(activity: Activity, color: Int) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {

            // 设置 paddingTop
            val rootView = activity.window.decorView.findViewById<View>(android.R.id.content) as ViewGroup
            rootView.setPadding(0, "状态栏高度", 0, 0)

            // 增加占位状态栏
            val decorView = activity.window.decorView as ViewGroup
            val statusBarView = View(activity)
            val lp = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, "状态栏高度")
            statusBarView.setBackgroundColor(color)
            decorView.addView(statusBarView, lp)

        }
    }

效果如下:
在这里插入图片描述
沉浸式在Android4.4 - Android5.0 之间 的状态栏顶部有个渐变,会显示黑色阴影,效果不是很好。

1.2 Android 5.0(API 21)以上版本

在Android 5.0 的时候加入了一个重要的属性和方法,android:statusBarColor (对应方法为 setStatusBarColor),通过设置这个属性或方法,可以实现我们想要的任何状态栏的颜色。

    /**
     * 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_DRAWS_SYSTEM_BAR_BACKGROUNDS ,并且不能设置FLAG_TRANSLUCENT_STATUS(Android 4.4才用这个)
这个比 Android 4.4(API 19)- Android 5.0( API 21) 的阶段省去了自己创建 View 并给该 View 上颜色的步骤,并且去掉了黑色的渐变阴影。当需要让 ContentView 的内容占据状态栏的时候是和原来一样的直接设置 WindowManager 的 FLAG_TRANSLUCENT_STATUS标志
也可以配合 systemUiVisibility(int) 一起使用

1.3 Android 6.0(API 23)以上版本

Android 6.0 以上的的实现方式是和 Android 5.0 一样的,但是从 6.0 开始,提供了状态栏的绘制模式,可以显示白色或黑色的内容和图标,除了部分族开放平台-状态栏变色小米开放平台-MIUI 9 & 10“状态栏黑色字符”实现方法变更通知,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
     * @deprecated Use {@link WindowInsetsController#APPEARANCE_LIGHT_STATUS_BARS} instead.
     */
    @Deprecated
    public static final int SYSTEM_UI_FLAG_LIGHT_STATUS_BAR = 0x00002000;

注释的意思:可以设置 setSystemUiVisibility(int) 进行状态栏绘制模式,可以兼容亮色模式的状态栏。想要这个方法生效,必须还要配合设置FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS ,并且不能设置FLAG_TRANSLUCENT_STATUS

    private fun transparentStatusBar23(activity: Activity) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            activity.window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
            activity.window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
            activity.window.statusBarColor = Color.TRANSPARENT

            val decorView = window.decorView
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
           		 // 状态栏图标为浅色
                val option = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
                decorView.systemUiVisibility = option
            }
        }
    }

设置状态栏字色和图标前:
在这里插入图片描述

设置状态栏字色和图标后:
在这里插入图片描述

综上所述沉浸式状态栏的两种模式,方法如下:

	/**
     * 使 contentView 中的内容占据到状态栏的沉浸式
     *
     *
     * @param activity
     */
    fun setContentAdjustToStatusBar(activity: Activity, isLight: Boolean) {

        val window = activity.window
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
            window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
            window.statusBarColor = Color.TRANSPARENT

            // 部分手机上设置此标识无效
//            window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
//                    or View.SYSTEM_UI_FLAG_LAYOUT_STABLE)

            window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)

            if (isLight) {
                setStatusLight(activity)
            }

        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
        }
    }

    /**
     * 设置状态栏颜色,contentView 的内容在状态栏的下方的沉浸式
     *
     * @param activity
     * @param color
     * @param isLight
     */
    fun setStatusColor(activity: Activity, color: Int, isLight: Boolean) {

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            val window = activity.window
            window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
            window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
            window.statusBarColor = color

            if (isLight) {
                setStatusLight(activity)
            }

        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {

            // 设置 paddingTop
            val rootView = activity.window.decorView.findViewById<View>(android.R.id.content) as ViewGroup
            rootView.setPadding(0, getStatusBarHeight(activity), 0, 0)

            // 增加占位状态栏
            val decorView = activity.window.decorView as ViewGroup
            val statusBarView = View(activity)
            val lp = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, getStatusBarHeight(activity))
            statusBarView.setBackgroundColor(color)
            statusBarView.layoutParams = lp
            decorView.addView(statusBarView)

        }
    }

    /**
     * 状态栏的图标和文字颜色为暗色
     */
    fun setStatusLight(activity: Activity) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            val option = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
            activity.window.decorView.systemUiVisibility = option
        }
    }
		
	/**
     * 获取状态栏高度
     *
     * @param activity
     * @return
     */
    fun getStatusBarHeight(activity: Activity): Int {
        var result = 0
        //获取状态栏高度的资源id
        val resourceId = activity.resources.getIdentifier("status_bar_height", "dimen", "android")
        if (resourceId > 0) {
            result = activity.resources.getDimensionPixelSize(resourceId)
        }
        Log.d("getStatusBarHeight", result.toString() + "")
        return result
    }

二、全屏模式

Android 手机屏幕一般是有状态栏和导航栏的,当需要全屏模式的时候,需要把状态栏和导航栏都隐藏起来。
Android 提供了三个用于将应用设为全屏模式选项:向后倾斜模式、沉浸模式和粘性沉浸模式。在所有三种方法中,系统栏都是隐藏的,您的 Activity 会持续收到所有轻触事件。 它们之间的区别在于用户让系统栏重新显示出来的方式。

2.1 向后倾斜

向后倾斜模式适用于用户不会于屏幕进行大量互动的全屏体验,例如,在观看视频的时候。
当用户希望调出系统栏时,只需要点击屏幕上的任意位置即可调出系统栏。
开启向后倾斜模式,代码如下:

  val window = activity.window
        window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
                or View.SYSTEM_UI_FLAG_FULLSCREEN)

2.2 沉浸模式

沉浸模式使用于用户将与屏幕有大量互动的应用,例如,游戏、查看图库中的图片或分页阅读。
当用户需要调出系统栏时,可以从状态栏或者导航栏的侧边来滑动调起系统栏,其他方式不会退出全面屏。

  val window = activity.window
        window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE
                or View.SYSTEM_UI_FLAG_FULLSCREEN
                or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION)

2.3 粘性沉浸模式

在粘连沉浸模式下,可以从状态栏或者导航栏的侧边来滑动调起系统栏,但他们是半透明的效果浮在应用视图(ContentView内容)之上,当用户点击应用实体后无互动几秒之后,系统栏自动消失,恢复全面屏。这种模式比较适合游戏、绘图类应用。

   window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
                or View.SYSTEM_UI_FLAG_FULLSCREEN
                or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION)

2.4 状态栏和导航栏的隐藏

 	/**
     * 状态栏隐藏
     *
     * @param activity
     */
    fun setStatusBarHide(activity: Activity) {
        // 允许窗口延伸到屏幕短边上的刘海区域
//        supportDisplayCutouts(activity)
        val window = activity.window
        window.decorView.systemUiVisibility = (
                // 预留控制内容到状态栏的距离
                View.SYSTEM_UI_FLAG_LAYOUT_STABLE
                // 隐藏状态栏
                or View.SYSTEM_UI_FLAG_FULLSCREEN
                // 将 contentView的内容延伸到状态栏。
                // 设置此属性时,要考虑是否使用 fitSystemWindows 来填充状态栏的距离;
                // 还要考虑刘海屏的情况,使用 layoutInDisplayCutoutMode 设置
                or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                )

    }

    /**
     * 导航栏隐藏
     *
     * @param activity
     */
    fun setNavigationBarHide(activity: Activity) {
        val window = activity.window

        window.decorView.systemUiVisibility = (
                // 预留控制内容到导航栏的距离
                View.SYSTEM_UI_FLAG_LAYOUT_STABLE
                // 隐藏导航栏
                or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
                // 将 contentView的内容延伸到导航栏。
                // 设置此属性时,要考虑是否使用 fitSystemWindows 来填充导航栏的距离;
                or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                )
    }

2.5 view.setFitsSystemWindows()方法

  • 1.设置系统是否需要考虑System bar(status bar和Navigation bar的统称)占据的区域来显示。如果需要的话就会执行fitSystemWindows(Rect)方法。即设置为true的时候系统会适应System bar 的区域,不内容不被遮住。fitSystemWindows(Rect)(api level 14):用来调整自身的内容来适应System Bar(不让被System Bar遮住)。 这里其实不止Status Bar和Navigation Bar,只是目前只考虑Status Bar、Navigation Bar、IME。
  • 2.onApplyWindowInsets(WindowInsets)(api level 20):同fitSystemWindows(Rect)的作用是一样的,更加方便扩展,对以后增加新的系统控件便于扩展。
  • 3.使用android:fitsSystemWindows="true",系统会自动的调整显示区域来实现详情的控件不会被遮住。

2.6 API 30 及以后的方式

在Android 30 API 上 SystemUIvisibility已弃用,推荐使用 WindowInsetsController类来进行状态栏的显示和隐藏以及其他操作。这种方式更简单、易操作。也可以使用 WindowInsetsControllerCompat 进行兼容。

    private fun hideSystemBars30s(activity: Activity) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            val controller = activity.window.decorView.windowInsetsController
            
            // 隐藏状态栏
            controller?.hide(WindowInsets.Type.statusBars())
            // 显示状态栏
            controller?.show(WindowInsets.Type.statusBars())
            
            // 隐藏导航栏
            controller?.hide(WindowInsets.Type.navigationBars())
            // 显示导航栏
            controller?.show(WindowInsets.Type.navigationBars())
            
            // 同时隐藏状态栏和导航栏
            controller?.hide(WindowInsets.Type.systemBars())
            // 同时显示状态栏和导航栏
            controller?.show(WindowInsets.Type.systemBars())

			// 向后模式
            val behavior1 = WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_TOUCH
            // 沉浸模式
            val behavior2 = WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE
            // 粘性沉浸模式
            val behavior3 = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
            controller?.systemBarsBehavior = behavior1
        }

    }

三、刘海屏

3.1 刘海屏的要求

在Android 9(API 28)及更高版本的设备上正式支持刘海屏。部分制造商也有在低版本上支持刘海屏。
在带刘海屏的设备上为确保一致性和兼容性,有五点强制要求:

  1. 一条边缘最多只能包含一个刘海
  2. 一台设备不能有两个以上的刘海
  3. 设备的两条较长边缘上不能有刘海
  4. 在未设置特殊标志的竖屏模式下,状态栏的高度必须至少与刘海屏的高度一致
  5. 在默认情况下,在全屏模式或横屏模式下,整个刘海屏必须显示黑边

第五条比较重要,这里设置全面屏时必须要考虑是否有刘海屏的这个性质

3.2 刘海模式

Android 允许控制是否在刘海区域显示内容。窗口布局属性 layoutInDisplayCutoutMode 控制内容将如何呈现在刘海区域中。共分为3种模式,可以通过编程或设置 Activity 的样式来控制刘海屏模式:

    <style name="ActivityTheme">
      <item name="android:windowLayoutInDisplayCutoutMode">
        shortEdges <!-- default, shortEdges, never -->
      </item>
    </style>
    fun supportDisplayCutouts(activity: Activity) {
        val window = activity.window
        val lp = window.attributes
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            // 仅当缺口区域完全包含在状态栏之中时,才允许窗口延伸到刘海区域显示
//            lp.layoutInDisplayCutoutMode =
//                WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
            // 永远不允许窗口延伸到刘海区域
//            lp.layoutInDisplayCutoutMode =
//                WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
            // 始终允许窗口延伸到屏幕短边上的刘海区域
            lp.layoutInDisplayCutoutMode =
                WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
            window.attributes = lp
        }

    }

下面就更详细的介绍不同的刘海模式。

3.2.1 默认行为

默认情况下,在未设置特殊标志的竖屏模式下,在带刘海屏的设备上,状态栏的大小会调整为至少与刘海一样高,而您的内容会显示在下方区域。在横屏模式或全屏模式下,您的应用窗口会显示黑边,因此您的任何内容都不会显示在刘海区域中。

3.2.2 将内容呈现在短边刘海屏区域中

在竖屏模式和横屏模式下,内容都会呈现到刘海区域中,而不管系统栏处于隐藏还是可见状态栏。需要注意的是:Android 一般是不允许内容视图与系统栏重叠,如果要强制视图内容延伸到刘海区域,需要和设置上述介绍的沉浸式状态栏全面屏的方式结合使用。

3.2.3 从不将内容呈现在刘海区域中

内容从不呈现到刘海区域中。此模式应该用于暂时设置 View.SYSTEM_UI_FLAG_FULLSCREEN 或 View.SYSTEM_UI_FLAG_HIDE_NAVIGATION 的窗口,以避免在设置或清除了该标志时执行另一种窗口布局。

四、总结

开头说的全屏预览时发现使用全面屏后,图片会闪动一下,这是因为,我当时使用的刘海屏的手机,当使用全屏模式时,刘海屏的手机默认会显示黑边,我们还要设置刘海屏的模式,上面提到的刘海屏的3中模式,可以设置**将内容呈现在短边刘海屏区域中**从而在刘海屏中达到全屏的效果。

沉浸式状态栏主要是分两个模式,一种是控制内容呈现在状态栏中,一种是控制状态栏的颜色。而且沉浸式状态栏还要根据 Android API 的不同使用的方式也不同,这块相对来说在日常开发中用的比较多,概念理清后就比较简单。
全面屏在 Android API 30 之前和之后也有所不同,在 Android API 30 及以后方式简单明了。
刘海屏是在 Android API 28 及以后支持的,要和全面屏配合使用,不然只考虑全面屏,在刘海屏上的效果是不进人不如意的。

Demo 在 系统栏里面

站在巨人的肩膀上:
1.MIUI 9 & 10“状态栏黑色字符”实现方法变更通知
2.魅族开放平台-状态栏变色
3.启用全屏模式
4.支持刘海屏
5.Android关于沉浸式状态栏总结

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值