返回

Android 15进入ActionMode状态栏变黑?3种方案解决

Android

修复 Android 15 进入 ActionMode 后状态栏变黑的问题

用 Material Components 1.12.0 和 AppCompat 1.7.0 开发时,你可能碰到了这么个怪事儿:在 Android 15 设备上,当你的 Fragment 里的 RecyclerView 进入 ActionMode(比如长按列表项后),那个顶部状态栏的颜色突然就从你精心设置的颜色(比如蓝色)变成了黑色。但在 Android 14 或者更早的版本上,一切正常。

你可能已经尝试过网上的各种方案,特别是针对 Android 15 WindowInsets 的改动,写了类似下面的代码去尝试在 onCreateActionModeonDestroyActionMode 里控制状态栏颜色:

// ... 在 Fragment 或 Activity 中 ...

override fun onCreateActionMode(
    mode: ActionMode?, menu: Menu?
): Boolean {
    // ... 省略菜单加载、按钮隐藏等逻辑 ...
    val color = ContextCompat.getColor(requireContext(), R.color.bar_background) // 你的目标蓝色
    setStatusBarColor(color, true)
    return true
}

override fun onDestroyActionMode(mode: ActionMode?) {
    // ... 省略恢复按钮、多选状态清理等逻辑 ...
    val color = ContextCompat.getColor(requireContext(), R.color.bar_background) // 恢复为你想要的颜色
    setStatusBarColor(color, false)
}

private fun setStatusBarColor(color: Int, isActionMode: Boolean) {
    val window = requireActivity().window
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { // Android 15+
        // 尝试用 OnApplyWindowInsetsListener (这可能是问题所在)
        window.decorView.setOnApplyWindowInsetsListener { view, insets ->
            // 这行试图改变整个 DecorView 背景,通常不是期望的行为
            // view.setBackgroundColor(color)

            // 处理 padding 和 consumeInsets 的各种尝试... 结果都不理想
            // 比如重叠的工具栏,或者依旧是黑色状态栏
            insets // 返回原始 insets,避免消费它们导致布局问题
        }
        // 你可能还需要直接设置 window.statusBarColor 或使用 WindowInsetsController
         window.statusBarColor = color // 尝试直接设置
         val controller = WindowCompat.getInsetsController(window, window.decorView)
         // 根据背景色决定状态栏图标是亮色还是暗色 (假设你的蓝色背景需要亮色图标)
         controller.isAppearanceLightStatusBars = !isColorDark(color) // 你需要一个 isColorDark 函数判断颜色亮度
    } else {
        // 旧版本的处理方式
        window.statusBarColor = color
        // 可能还需要根据颜色调整SYSTEM_UI_FLAG_LIGHT_STATUS_BAR (API 23+)
        val decorView = window.decorView
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            val isDark = isColorDark(color) // 同样需要 isColorDark
            var flags = decorView.systemUiVisibility
            if (isDark) { // 深色背景,需要亮色图标
                 flags = flags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
             } else { // 浅色背景,需要深色图标
                 flags = flags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
             }
            decorView.systemUiVisibility = flags
        }
    }
}

// 辅助函数,判断颜色是否为深色 (简单示例)
private fun isColorDark(color: Int): Boolean {
    val darkness = 1 - (0.299 * Color.red(color) + 0.587 * Color.green(color) + 0.114 * Color.blue(color)) / 255
    return darkness >= 0.5 // 亮度阈值,可调整
}

上面这种尝试,特别是针对 Android 15 使用 setOnApplyWindowInsetsListener 来控制 ActionMode 下状态栏颜色的方式,往往会掉进坑里。不是状态栏依然黑屏,就是搞出两个工具栏叠在一起,或者状态栏和 ActionMode 工具栏抢夺触摸事件。文档对这块儿确实讲得不够细,让人头疼。

为啥 Android 15 上会这样?

这很可能跟 Android 15 对 Edge-to-Edge 显示模式和 WindowInsets 处理的进一步规范或内部实现调整有关。

  1. Edge-to-Edge 行为变化: Android 系统越来越推荐应用采取 Edge-to-Edge 设计,让内容能够绘制到状态栏和导航栏后面。在 Android 15 上,系统对窗口如何处理 इनसेट(insets,即系统 UI 占据的空间)可能有了更严格的执行逻辑。当 ActionMode 启动时,它会在现有 UI 上层叠加一个新的 Toolbar。这个叠加过程与 WindowInsets 的分发、消费,以及底层背景的绘制方式交互,可能在 Android 15 上产生了预期外的副作用。

  2. ActionMode 绘制机制: ActionMode 的工具栏(通常是一个 ActionBarContextView)插入到视图层级中。它的背景绘制、布局方式,以及它如何影响其下层视图对 WindowInsets 的响应,在 Android 15 上可能微调了。如果 ActionMode 的背景没有正确处理,或者它错误地“遮挡”了本该显示颜色的区域,而系统默认给状态栏区域绘制了一个黑色底色(在某些 edge-to-edge 配置下可能发生),就看到了黑条。

  3. OnApplyWindowInsetsListener 的误用: 像上面示例代码那样,在 window.decorView 上设置 OnApplyWindowInsetsListener 并试图在里面直接修改背景色或者粗暴地消费 insets,通常不是正确做法。DecorView 是整个窗口的根视图,在这里处理特定模式(如 ActionMode)下的状态栏颜色过于“全局”,容易干扰其他部分的布局和绘制。而且,这个 Listener 的主要目的是让你根据 insets 调整自己视图的 padding 或 margin ,让内容避开系统栏,而不是用它来控制系统栏本身的颜色。

解决方案

别灰心,咱们换几种思路试试。

方案一:直接设置 Window 属性(优先尝试)

有时候,最直接的方法反而有效。虽然 Edge-to-Edge 推荐通过主题或让内容延伸来着色状态栏区域,但对于 ActionMode 这种临时性的覆盖 UI,直接修改 WindowstatusBarColor 属性可能仍然是管用的,尤其是在 ActionMode 生命周期内精确控制。

原理:

直接调用 window.statusBarColor 来设置颜色。同时,为了保证状态栏上的图标(时间、电量、信号等)颜色与你的背景色形成对比,需要使用 WindowInsetsController 来控制图标的明暗 (isAppearanceLightStatusBars)。

操作步骤:

  1. 确保你的项目中已经添加了 androidx.core:core-ktx 依赖,方便使用 ContextCompatWindowCompat
  2. 修改你的 setStatusBarColor 函数(或者创建一个新的专用函数),只包含直接设置颜色的逻辑,并正确设置图标明暗。

代码示例:

import android.graphics.Color
import android.os.Build
import android.view.Menu
import android.view.MenuInflater
import android.view.View
import android.view.Window
import androidx.appcompat.view.ActionMode
import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils
import androidx.core.view.WindowCompat
import androidx.fragment.app.Fragment

// ... 在你的 Fragment 或包含 RecyclerView 的地方 ...

class MyFragment : Fragment(), ActionMode.Callback {

    private var currentActionMode: ActionMode? = null
    // ... 其他成员变量和方法 ...

    override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
        currentActionMode = mode
        val inflater: MenuInflater = mode.menuInflater
        inflater.inflate(R.menu.multi_selection_menu, menu)
        // ... 其他初始化 ...

        val actionModeColor = ContextCompat.getColor(requireContext(), R.color.action_mode_status_bar_blue) // 定义一个 ActionMode 专用的蓝色
        setStatusBarColorForMode(requireActivity().window, actionModeColor)
        return true
    }

    override fun onDestroyActionMode(mode: ActionMode) {
        // 恢复到 Fragment 或 Activity 的常规状态栏颜色
        val defaultColor = ContextCompat.getColor(requireContext(), R.color.default_status_bar_blue) // 你的常规状态栏颜色
        setStatusBarColorForMode(requireActivity().window, defaultColor)
        currentActionMode = null
        // ... 其他清理工作 ...
    }

    // ... 其他 ActionMode.Callback 方法 (onPrepareActionMode, onActionItemClicked) ...

    private fun setStatusBarColorForMode(window: Window, color: Int) {
        // 1. 直接设置状态栏背景色
        window.statusBarColor = color

        // 2. 控制状态栏图标颜色 (浅色/深色)
        val controller = WindowCompat.getInsetsController(window, window.decorView)
        // 使用 ColorUtils 判断颜色亮度更可靠
        val isLightColor = ColorUtils.calculateLuminance(color) >= 0.5
        controller.isAppearanceLightStatusBars = !isLightColor // 浅色背景配深色图标,深色背景配浅色图标

        // 对于 API 23-29,还需要用旧方法设置图标颜色(如果你的 minSdk < 30)
        // WindowInsetsController 会自动处理,但在旧 API 上需要 fallback
        // 这部分逻辑可以封装得更完善,但基本思路是这样
         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
             val decorView = window.decorView
             var flags = decorView.systemUiVisibility
             if (!isLightColor) { // 深色背景 -> 亮色图标 (清除 LIGHT_STATUS_BAR flag)
                 flags = flags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
             } else { // 浅色背景 -> 深色图标 (设置 LIGHT_STATUS_BAR flag)
                 flags = flags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
             }
             decorView.systemUiVisibility = flags
         }
    }

    // --- RecyclerView 和 ActionMode 触发逻辑 ---
    // (省略具体实现,假设你已经有 RecyclerView Adapter 和长按触发 ActionMode 的代码)
}

// 在 res/values/colors.xml 中定义颜色:
// <color name="action_mode_status_bar_blue">#FF1976D2</color> <!-- 比如 Material Blue 700 -->
// <color name="default_status_bar_blue">#FF2196F3</color> <!-- 比如 Material Blue 500 -->

说明:

  • 这种方法相对简单直接,专门针对 ActionMode 的进入和退出时机设置颜色。
  • 使用了 ColorUtils.calculateLuminance (来自 androidx.core:core-ktx) 来判断颜色亮度,这比简单的 RGB 加权平均更准确。
  • 记得在你的 colors.xml 文件里定义好 action_mode_status_bar_bluedefault_status_bar_blue 这两个颜色值。
  • 这种方式在大部分情况下应该能解决问题,因为它不涉及复杂的 WindowInsets 监听和消费逻辑,只是在特定时刻强制设定状态栏颜色。

方案二:检查并调整主题(Theme)

Android 的 ActionMode 外观很大程度上是由当前 Activity 的主题(Theme)以及可能的 actionModeStyleactionModeTheme 属性控制的。Android 15 可能更严格地依据主题来渲染 ActionMode,包括状态栏区域。

原理:

通过在你的应用主题或者为 ActionMode 指定的特定主题(Theme Overlay)中,定义状态栏相关的颜色属性,让系统在进入 ActionMode 时自动应用这些颜色。

操作步骤:

  1. 打开你的 res/values/styles.xml (或者 themes.xml) 文件。
  2. 找到你的应用主主题(通常继承自 Theme.MaterialComponents.*Theme.AppCompat.*)。
  3. 检查或添加以下属性:
    • android:statusBarColorcolorPrimaryDark (旧属性,但有时仍影响状态栏):设置常规状态下的状态栏颜色。
    • actionModeStyle:可以指向一个自定义的 Style,用于配置 ActionMode 的外观,比如背景色 (android:background)。
    • windowActionModeOverlay:通常设置为 true,让 ActionMode 工具栏覆盖在内容区域之上,而不是推开内容。这对于 Material Design 风格很重要。
    • 尝试在主主题或者通过 actionModeTheme 属性指向的 Theme Overlay 中,直接设置 android:statusBarColor 为你 ActionMode 时想要的颜色。

代码示例 (styles.xml/themes.xml):

<resources>
    <!-- 应用主主题 -->
    <style name="AppTheme" parent="Theme.Material3.DayNight.NoActionBar">
        <!-- 常规状态栏颜色 -->
        <item name="android:statusBarColor">@color/default_status_bar_blue</item>
        <!-- 让 ActionMode 覆盖内容 -->
        <item name="windowActionModeOverlay">true</item>
        <!-- 指向自定义的 ActionMode Style -->
        <item name="actionModeStyle">@style/CustomActionModeStyle</item>
        <!-- (可选)指向一个 ActionMode 专用的 Theme Overlay -->
        <!-- <item name="actionModeTheme">@style/ActionModeThemeOverlay</item> -->
         <!-- ... 其他主题属性 ... -->
    </style>

    <!-- 自定义 ActionMode Style -->
    <style name="CustomActionModeStyle" parent="Widget.AppCompat.ActionMode">
        <!-- ActionMode 工具栏本身的背景色 -->
        <item name="background">@color/action_mode_toolbar_background</item>
        <!-- 尝试在这里或者 Theme Overlay 里设置状态栏颜色 -->
        <!-- <item name="android:statusBarColor" tools:targetApi="lollipop">@color/action_mode_status_bar_blue</item> -->
        <!-- ... 其他 ActionMode 样式,如标题文字颜色等 ... -->
    </style>

    <!-- (可选)ActionMode 专用的 Theme Overlay -->
    <!-- <style name="ActionModeThemeOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar"> -->
        <!-- 在这里强制指定 ActionMode 期间的状态栏颜色 -->
        <!-- <item name="android:statusBarColor" tools:targetApi="lollipop">@color/action_mode_status_bar_blue</item> -->
    <!-- </style> -->

    <!-- 在 colors.xml 中定义颜色 -->
    <!-- <color name="action_mode_toolbar_background">#FF1976D2</color> -->
    <!-- <color name="action_mode_status_bar_blue">#FF1565C0</color> --><!-- 略深一点的蓝 -->
    <!-- <color name="default_status_bar_blue">#FF2196F3</color> -->
</resources>

说明:

  • 通过主题设置是更“系统级”的方法,理论上更符合 Android 的设计范式。
  • 你需要仔细调整主题中的各个颜色属性,特别是 android:statusBarColor。注意它可能需要 API level (用 tools:targetApi 标记)。
  • 测试时注意区分 ActionMode 工具栏自身的背景色 (background in actionModeStyle) 和状态栏区域的背景色 (android:statusBarColor in theme)。
  • 如果你的应用已经启用了 Edge-to-Edge(通过 WindowCompat.setDecorFitsSystemWindows(window, false)),主题中的 android:statusBarColor 可能被设为透明,依靠内容视图的背景来给状态栏区域上色。这种情况下,你需要确保 ActionMode 启动时,合适的视图(可能是 ActionMode 工具栏自身,如果它足够高并延伸到状态栏区域)具有你想要的背景色。这让问题变得复杂,也是方案一为何可能更直接的原因。

方案三:精细化处理 WindowInsets(终极手段)

如果以上方法都不行,那问题可能确实出在 Edge-to-Edge 和 WindowInsets 的复杂交互上。这时就需要更小心地处理 WindowInsets,但要避免在 DecorView 的 Listener 里做错误的操作

原理:

正确配置 Edge-to-Edge。状态栏颜色要么由主题设定(并让系统处理绘制),要么由延伸到状态栏区域下的内容视图(比如你的 Toolbar 或 Fragment 的背景)的背景色决定。OnApplyWindowInsetsListener 的正确用法是:监听 Insets 变化,然后只调整需要避让系统栏的视图的 Padding 或 Margin ,而不是用它来设置颜色或粗暴地消费 Insets。

操作步骤:

  1. 确保 Edge-to-Edge 正确开启: 在 Activity 的 onCreate 中调用 WindowCompat.setDecorFitsSystemWindows(window, false)
  2. 移除错误的 Listener: 把之前在 DecorView 上设置的、尝试修改背景色或消费 Insets 的 OnApplyWindowInsetsListener 代码删掉。
  3. 在合适的视图上应用 Insets: 在你的 Fragment 的根视图,或者包含 RecyclerView 和 Toolbar 的那个布局容器(比如 CoordinatorLayout, ConstraintLayout)上,设置 OnApplyWindowInsetsListener 或使用 ViewCompat.setOnApplyWindowInsetsListener
  4. Listener 只做 Padding/Margin 调整: 在 Listener 回调中,获取 WindowInsetsCompat.Type.statusBars()systemBars() 的 insets 值,然后只用它们来更新视图的 paddingTopmarginTop,确保内容不被状态栏遮挡。绝对不要 在 Listener 里调用 setBackgroundColorconsumeSystemWindowInsets()/consumeStableInsets() (除非你非常清楚为什么要这样做,通常不需要)。
  5. 颜色来源:
    • 状态栏区域的颜色现在应该由延伸到该区域的那个视图的背景决定。如果是 Toolbar,设置 Toolbar 的背景色。如果是 Fragment 背景,设置 Fragment 根视图的背景色。
    • 对于 ActionMode,其工具栏应该有自己的背景色(通过主题的 actionModeStyle 设置 background)。如果它设计为覆盖整个顶部区域(包括状态栏),那么它的背景色就是你在 ActionMode 期间看到的颜色。确保这个颜色是你想要的蓝色。

代码示例 (Fragment 中应用 Insets 到根视图):

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    // 假设 view 是 Fragment 的根布局 (例如 CoordinatorLayout 或 ConstraintLayout)
    ViewCompat.setOnApplyWindowInsetsListener(view) { v, windowInsets ->
        val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())

        // 只更新 Padding,让内容避开系统栏
        v.updatePadding(top = insets.top, bottom = insets.bottom)
        // 可能还需要调整左右 padding,如果导航栏在侧边等

        // 返回原始 insets 或者 WindowInsetsCompat.CONSUMED (取决于你的布局是否需要进一步传递insets)
        // 通常对于根布局,消费掉可能是对的,防止重复应用
        // 但为了安全起见,先返回原始的试试
        windowInsets // 或者 WindowInsetsCompat.CONSUMED
    }

    // --- ActionMode 相关代码保持不变,依赖方案一或方案二来控制颜色 ---
    // 你仍然需要在 onCreateActionMode/onDestroyActionMode 中调用类似方案一的 setStatusBarColorForMode
    // 或者,完全依赖方案二的主题设置,此时就不需要那两个函数了。
}

关于 consumeSystemWindowInsetsconsumeStableInsets

  • 你在尝试中发现 consumeSystemWindowInsets() 会导致两个工具栏都显示,状态栏覆盖 ActionBar;而 consumeStableInsets() 留下黑条。这都说明消费 Insets 的方式和地点不对。
  • consumeSystemWindowInsets() 会阻止 Insets 向下传递给子 View,导致像 AppBarLayoutToolbar 这样的控件收不到它们需要的 Insets 信息来调整自身布局,可能会引发重叠问题。
  • DecorView 层面消费 Insets 通常是错误的,因为它影响了整个窗口的内容布局。

安全建议:

  • 修改 WindowInsets 处理逻辑时,务必在不同 Android 版本(尤其 11+ 和 15)以及不同设备(有无刘海、挖孔屏、不同导航栏模式)上充分测试。
  • 优先尝试最简单的方案(方案一)。如果无效,再考虑主题(方案二),最后才深入研究 Insets 的精细化处理(方案三)。

希望以上某个方案能帮你搞定 Android 15 上 ActionMode 状态栏变黑的这个“小”麻烦!