<
Android 适配 深色模式 夜间模式 暗黑模式 黑夜模式 暗夜模式
>
上一篇

Flutter学习笔记 遇到的问题
下一篇

Flutter学习笔记 Navigator 路由 导航 页面跳转

适配深色模式


知识点

先了解必须知道的东西


如何适配

针对以上,接下来需要做的:

做浅色和深色两套资源

浅色 浅色底深色字 value/colors.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="background_purity">#FFFFFFFF</color>
    <color name="primaryText">#333333</color>
    <color name="secondaryText">#8a000000</color>
</resources>

深色 深色底浅色字 valus-night/colors.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="background_purity">#FF000000</color>
    <color name="primaryText">#ffffffff</color>
    <color name="secondaryText">#b3ffffff</color>
</resources>

当然,主题 themes.xml 也可以做两套,而且现在的 MD3 就是用主题实现默认的深色模式

AppCompat 下的主题 都有 Light 和 DayNight 两套对应

value/themes.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <item name="colorPrimary">#FF6200EE</item>
    </style>
</resources>

value-night/themes.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
        <item name="colorPrimary">#FFBB86FC</item>
    </style>
</resources>

要添加其它颜色或主题,直接加就行了

提供开关,并做持久化处理

API

// 当前页面
delegate.localNightMode = AppCompatDelegate.MODE_NIGHT_YES
// App 全局设置 (App 退出后会失效)
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)

持久化的设置

    private fun initView() {
        // set text
        initViewLightModel()
        // set click
        binding.llNightModel.onClick {
            val currentMode = binding.tvNightModel.text
            val nightModeArray = arrayOf("跟随系统", "始终关闭", "始终开启")
            val index = try {
                nightModeArray.toList().indexOf(currentMode)
            } catch (e: Exception) {
                0
            }
            var selectModel = SPUtils.getInstance().getInt("night_model")
            AlertDialog.Builder(this)
                .setTitle("深色模式")
                .setSingleChoiceItems(nightModeArray, index) { dialog, which ->
                    // which 即 index
                    selectModel = when (which) {
                        // 对应 nightModeArray 的索引
                        1 -> {
                            AppCompatDelegate.MODE_NIGHT_NO
                        }

                        2 -> {
                            AppCompatDelegate.MODE_NIGHT_YES
                        }

                        else -> {
                            AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
                        }
                    }
                }
                .setNegativeButton("取消", null)
                .setPositiveButton("确定") { dialog, which ->
                    SPUtils.getInstance().put("night_model", selectModel)
                    // set text
                    initViewLightModel()
                    // set model
                    AppCompatDelegate.setDefaultNightMode(selectModel)
                }
                .show()
        }
    }

    private fun initViewLightModel() {
        binding.tvNightModel.text = when (SPUtils.getInstance().getInt("night_model")) {
            AppCompatDelegate.MODE_NIGHT_NO -> {
                "始终关闭"
            }

            AppCompatDelegate.MODE_NIGHT_YES -> {
                "始终开启"
            }

            else -> {
                "跟随系统"
            }
        }
    }

App 启动时 取出持久化值进行初始化设置

class App : Application() {

    override fun onCreate() {
        super.onCreate()
        initNightModel()
    }

    private fun initNightModel() {
        val lightModel = SPUtils.getInstance().getInt("night_model")
        AppCompatDelegate.setDefaultNightMode(lightModel)
    }

}

完成后,基本实现了深色模式的开关设置

但是当你的页面是不是深色背景浅色字体时,打开深色模式系统开关 VS 关闭深色模式系统开关且打开App深色模式开关, 你会发现两个都是深色模式,但是显示不一致。

是因为你的深色模式对应色值不符合标准(深色底浅色字),系统深色模式打开时, App启用了强制深色模式 forceDarkAllowed

而且,此时在系统深色模式开启时APP设置始终关闭深色模式是无效的

强制深色模式

可以直接在 布局 android:forceDarkAllowed="false" 或 代码 view.isForceDarkAllowed = false 设置, 在主题设置也行

<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
    <item name="colorPrimary">#FFBB86FC</item>
    <!--
    false, 不允许强制深色模式
    系统深色模式 关闭时,APP 深色模式 不会强制适配
    系统深色模式 开启时,APP 普通模式 不会强制适配
    硬编码色值不会被强制改变
    避免自动适配的效果不合预期
    即开启自定义适配 手动适配
    -->
    <item name="android:forceDarkAllowed" tools:targetApi="q">false</item>
</style>

建议: 如果App提供深色主题设置的开关, 可以直接在基类主题禁用强制深色模式, 再在特定页面或View打开 (不是我不适配, 只是设计没有提供对应深色模式的设计图), 因为强制深色模式的效果可能不是你想象中的样子, 只是大致的自动适配

到此就差不多完成了,但是当你拿出你的钉子户小米6时,发现还是有点不对劲。哦!原理是状态栏字体颜色不会跟随改变

适配低版本状态栏字体颜色

一般情况, 10+ 系统的状态栏字体颜色会自动根据状态栏背景色去变化

但是低版本的 只能手动去改变

我们可以根据深色模式设置状态栏字体颜色

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 状态栏导航栏颜色
        immersionBar {
            statusBarColor(initStatusBarColor())
            navigationBarColor(initNavBarColor())
            // 状态栏字体颜色 (系统自动变色优先, 即对低版本系统不会自动变色有效)
            statusBarDarkFont(initStatusBarDarkFont())
        }
        // ...
    }

    protected open fun initStatusBarDarkFont(): Boolean {
        // 非深色模式都默认设置黑色字体
        val lightModel = SPUtils.getInstance().getInt("night_model")
        return lightModel != AppCompatDelegate.MODE_NIGHT_YES
    }

    override fun onConfigurationChanged(newConfig: Configuration) {
        super.onConfigurationChanged(newConfig)
        immersionBar {
            // 低版本系统状态栏字体不会自动变色
            statusBarDarkFont(initStatusBarDarkFont())
        }
    }

作用域是当前的 Window,所以可以在 BaseActivity 和 弹窗的基类去设置


再附上获取当前模式的方法

/**
 * 获取 System 暗色模式 开关状态
 */
val Context.sysIsDarkMode: Boolean
    get() {
        val uiModeManager = ContextCompat.getSystemService(this, UiModeManager::class.java)
            ?: return false
        return uiModeManager.nightMode == UiModeManager.MODE_NIGHT_YES
    }

/**
 * 获取 APP 最终是否 暗色模式
 */
val Context.appIsDarkMode: Boolean
    get() {
        return (this.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
    }

以上用到了一些第三方库 AndroidUtilCode ImmersionBar

打完收工。

Top
Foot