MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Kotlin暗黑模式适配实现方案

2024-10-025.1k 阅读

一、Kotlin 暗黑模式适配概述

在移动应用开发领域,暗黑模式(Dark Mode)已成为提升用户体验的重要特性。它不仅在夜间或低光环境下减轻眼睛疲劳,还能赋予应用独特的视觉风格。Kotlin 作为一种现代的编程语言,为暗黑模式的适配提供了丰富且便捷的实现方式。

暗黑模式的适配核心在于根据系统设置或用户手动切换,动态调整应用的界面元素颜色、图标样式等视觉属性。在 Kotlin 中,这涉及到资源管理、界面刷新以及对系统设置变化的监听等多方面的操作。

二、资源配置与管理

2.1 颜色资源

  1. 定义不同模式下的颜色资源 在 Kotlin 项目中,我们通过资源文件来管理颜色。在 res/values/colors.xml 文件中定义亮色模式的颜色,例如:
<resources>
    <color name="primary_color">#FF0000</color>
    <color name="background_color">#FFFFFF</color>
</resources>

为暗黑模式创建 res/values-night/colors.xml 文件,这里的颜色定义会在暗黑模式下生效,例如:

<resources>
    <color name="primary_color">#00FF00</color>
    <color name="background_color">#000000</color>
</resources>
  1. 在代码中获取颜色资源 在 Kotlin 代码中,我们可以使用 ContextCompat.getColor() 方法来获取颜色。例如,在一个 Activity 中设置背景颜色:
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val backgroundColor = ContextCompat.getColor(this, R.color.background_color)
        window.decorView.setBackgroundColor(backgroundColor)
    }
}

2.2 图标资源

  1. 创建不同模式的图标资源 同样,对于图标资源,我们可以在 res/drawable/ 目录下存放亮色模式图标,在 res/drawable-night/ 目录下存放暗黑模式图标。例如,我们有一个 ic_menu 图标,在 res/drawable/ic_menu.png 是亮色模式下的图标,而 res/drawable-night/ic_menu.png 是暗黑模式下的图标。
  2. 在视图中使用图标资源 在 Kotlin 代码中,当设置 ImageView 的图标时,系统会根据当前模式自动选择正确的图标。例如:
val imageView: ImageView = findViewById(R.id.image_view)
imageView.setImageResource(R.drawable.ic_menu)

三、监听系统暗黑模式变化

3.1 使用 ConfigurationChange 监听

  1. 在 AndroidManifest.xml 中配置 首先,在 AndroidManifest.xml 文件中为需要监听暗黑模式变化的 Activity 添加 android:configChanges="uiMode" 属性:
<activity android:name=".MainActivity"
          android:configChanges="uiMode">
</activity>
  1. 在 Activity 中重写方法 然后,在对应的 Activity 中重写 onConfigurationChanged() 方法。在这个方法中,我们可以重新加载资源以适配新的模式。例如:
class MainActivity : AppCompatActivity() {
    override fun onConfigurationChanged(newConfig: Configuration) {
        super.onConfigurationChanged(newConfig)
        if (newConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES) {
            // 暗黑模式
            recreate()
        } else {
            // 亮色模式
            recreate()
        }
    }
}

这里使用 recreate() 方法重新创建 Activity,以便重新加载资源。在实际应用中,也可以选择更细粒度的刷新,比如只更新特定的视图。

3.2 使用 AppCompatDelegate 监听

  1. 初始化 AppCompatDelegateApplication 类或者 ActivityonCreate() 方法中初始化 AppCompatDelegate
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)

这里设置为跟随系统模式。AppCompatDelegate 提供了多种模式选项,如 MODE_NIGHT_YES(强制暗黑模式)、MODE_NIGHT_NO(强制亮色模式)。 2. 监听模式变化 通过 AppCompatDelegate.setNightModeChangeListener() 方法监听模式变化。例如:

AppCompatDelegate.setNightModeChangeListener { mode ->
    when (mode) {
        AppCompatDelegate.MODE_NIGHT_YES -> {
            // 暗黑模式
            updateUIForDarkMode()
        }
        AppCompatDelegate.MODE_NIGHT_NO -> {
            // 亮色模式
            updateUIForLightMode()
        }
        else -> {
            // 跟随系统模式
            if (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES) {
                updateUIForDarkMode()
            } else {
                updateUIForLightMode()
            }
        }
    }
}

其中 updateUIForDarkMode()updateUIForLightMode() 是自定义的方法,用于更新界面以适配相应模式。例如:

private fun updateUIForDarkMode() {
    val backgroundColor = ContextCompat.getColor(this, R.color.background_color)
    window.decorView.setBackgroundColor(backgroundColor)
    // 更新其他视图元素
}

private fun updateUIForLightMode() {
    val backgroundColor = ContextCompat.getColor(this, R.color.background_color)
    window.decorView.setBackgroundColor(backgroundColor)
    // 更新其他视图元素
}

四、手动切换暗黑模式

4.1 在设置界面添加切换开关

  1. 创建切换开关视图 在设置界面的布局文件(例如 res/layout/settings_layout.xml)中添加一个 Switch 视图:
<Switch
    android:id="@+id/dark_mode_switch"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="切换暗黑模式"/>
  1. 在 Kotlin 代码中处理切换事件 在对应的 SettingsActivity 中获取 Switch 并处理其切换事件。例如:
class SettingsActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.settings_layout)
        val darkModeSwitch: Switch = findViewById(R.id.dark_mode_switch)
        val currentMode = AppCompatDelegate.getDefaultNightMode()
        darkModeSwitch.isChecked = currentMode == AppCompatDelegate.MODE_NIGHT_YES
        darkModeSwitch.setOnCheckedChangeListener { _, isChecked ->
            if (isChecked) {
                AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
            } else {
                AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
            }
        }
    }
}

4.2 保存用户选择

为了在应用重启后保持用户选择的模式,我们可以使用 SharedPreferences 来保存设置。

  1. 保存模式设置 在处理切换事件时,将用户选择保存到 SharedPreferences 中:
val sharedPreferences = getSharedPreferences("DarkModePrefs", Context.MODE_PRIVATE)
val editor = sharedPreferences.edit()
if (isChecked) {
    editor.putInt("night_mode", AppCompatDelegate.MODE_NIGHT_YES)
} else {
    editor.putInt("night_mode", AppCompatDelegate.MODE_NIGHT_NO)
}
editor.apply()
  1. 读取模式设置并应用Application 类或者 ActivityonCreate() 方法中读取保存的设置并应用:
val sharedPreferences = getSharedPreferences("DarkModePrefs", Context.MODE_PRIVATE)
val nightMode = sharedPreferences.getInt("night_mode", AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
AppCompatDelegate.setDefaultNightMode(nightMode)

五、适配自定义视图

5.1 自定义 View 的颜色适配

  1. 在自定义 View 中获取颜色资源 假设我们有一个自定义的 MyView,需要根据暗黑模式适配颜色。在 MyView 的构造函数或者 onDraw() 方法中获取颜色资源。例如:
class MyView(context: Context, attrs: AttributeSet?) : View(context, attrs) {
    private var primaryColor: Int = 0
    init {
        primaryColor = ContextCompat.getColor(context, R.color.primary_color)
    }
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        val paint = Paint()
        paint.color = primaryColor
        canvas.drawCircle(100f, 100f, 50f, paint)
    }
}
  1. 处理模式变化 如果需要在模式变化时更新自定义 View 的颜色,可以在 MyView 中添加一个方法来重新获取颜色并刷新视图。例如:
fun updateColor() {
    primaryColor = ContextCompat.getColor(context, R.color.primary_color)
    invalidate()
}

然后在模式变化监听处调用这个方法。例如,在 ActivityonConfigurationChanged() 方法中:

override fun onConfigurationChanged(newConfig: Configuration) {
    super.onConfigurationChanged(newConfig)
    if (newConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES) {
        myView.updateColor()
    } else {
        myView.updateColor()
    }
}

5.2 自定义 View 的图标适配

  1. 在自定义 View 中使用图标资源 如果自定义 View 中需要显示图标,同样可以根据模式选择不同的图标。例如,在自定义 ViewonDraw() 方法中绘制图标:
class IconView(context: Context, attrs: AttributeSet?) : View(context, attrs) {
    private var iconDrawable: Drawable? = null
    init {
        iconDrawable = ContextCompat.getDrawable(context, R.drawable.ic_custom_icon)
    }
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        iconDrawable?.let {
            it.setBounds(0, 0, width, height)
            it.draw(canvas)
        }
    }
}
  1. 适配模式变化 与颜色适配类似,为了在模式变化时更新图标,我们可以添加一个方法来重新获取图标并刷新视图。例如:
fun updateIcon() {
    iconDrawable = ContextCompat.getDrawable(context, R.drawable.ic_custom_icon)
    invalidate()
}

在模式变化监听处调用此方法,以确保图标在不同模式下正确显示。

六、WebView 暗黑模式适配

6.1 WebView 颜色适配

  1. 设置 WebView 的颜色模式 在 Kotlin 中,我们可以通过设置 WebSettings 来适配暗黑模式。例如,在 Activity 中:
class WebViewActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_web_view)
        val webView: WebView = findViewById(R.id.web_view)
        val webSettings = webView.settings
        if (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES) {
            webSettings.forceDark = WebSettings.FORCE_DARK_ON
        } else {
            webSettings.forceDark = WebSettings.FORCE_DARK_OFF
        }
        webView.loadUrl("https://example.com")
    }
}

这里通过 WebSettings.FORCE_DARK_ONWebSettings.FORCE_DARK_OFF 来分别开启和关闭暗黑模式。WebSettings 还提供了一些其他的配置选项,如 FORCE_DARK_AUTO,可以根据页面的颜色自动适配暗黑模式。 2. 处理页面内的样式 有些情况下,WebView 加载的页面可能需要额外的样式调整。我们可以通过注入 CSS 样式来进一步优化暗黑模式的显示。例如:

val css = """
    body {
        background-color: #000000;
        color: #FFFFFF;
    }
""".trimIndent()
webView.loadUrl("javascript:(function() { " +
        "var parent = document.getElementsByTagName('head').item(0); " +
        "var style = document.createElement('style'); " +
        "style.type = 'text/css'; " +
        "style.innerHTML = '$css'; " +
        "parent.appendChild(style); " +
        "})()")

6.2 WebView 图标适配

如果 WebView 中显示的图标需要适配暗黑模式,可以通过修改页面的 HTML 来加载不同模式的图标。例如,在页面的 HTML 中:

<img id="icon" src="light_mode_icon.png" />
<script>
    if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
        document.getElementById('icon').src = 'dark_mode_icon.png';
    }
</script>

在 Kotlin 代码中加载这个 HTML 页面即可实现图标在暗黑模式下的适配。

七、适配不同 Android 版本

7.1 Android 10 及以上

从 Android 10(API 级别 29)开始,系统原生支持暗黑模式。我们可以直接使用系统提供的 API 来监听模式变化和获取当前模式。例如,在监听模式变化时:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    val contentResolver = this.contentResolver
    val mode = Settings.System.getInt(contentResolver, Settings.System.ACCENT_COLOR_MODE, -1)
    if (mode == Settings.System.ACCENT_COLOR_MODE_NIGHT_YES) {
        // 暗黑模式
    } else {
        // 亮色模式
    }
}

7.2 Android 10 以下

对于 Android 10 以下的版本,我们可以使用 AppCompatDelegate 来模拟暗黑模式的适配。如前文所述,通过设置 AppCompatDelegate.setDefaultNightMode() 来控制模式,并且通过 AppCompatDelegate.setNightModeChangeListener() 来监听模式变化。在资源管理方面,同样可以通过 res/values-night/ 目录来存放暗黑模式资源,系统会自动根据模式加载相应资源。

八、性能优化与注意事项

8.1 性能优化

  1. 减少资源加载次数 在模式变化时,尽量避免不必要的资源重新加载。例如,如果某个视图在亮色和暗黑模式下的颜色变化不大,可以通过动态计算颜色而不是每次都重新加载资源。例如,对于一个文本视图,我们可以根据背景颜色的亮度动态计算文本颜色,而不是在不同模式下使用不同的颜色资源。
private fun calculateTextColor(backgroundColor: Int): Int {
    val red = Color.red(backgroundColor)
    val green = Color.green(backgroundColor)
    val blue = Color.blue(backgroundColor)
    val brightness = (red * 0.299 + green * 0.587 + blue * 0.114).toInt()
    return if (brightness > 127) Color.BLACK else Color.WHITE
}
  1. 异步加载资源 对于一些较大的资源,如图片,可以采用异步加载的方式。例如,使用 Glide 库加载图片时,在模式变化时可以先显示占位图,然后异步加载新的图片。
Glide.with(this)
   .load(R.drawable.dark_mode_image)
   .placeholder(R.drawable.placeholder)
   .into(imageView)

8.2 注意事项

  1. 兼容性问题 在适配暗黑模式时,要注意不同 Android 版本和设备的兼容性。例如,某些老设备可能对暗黑模式的支持存在问题,或者某些第三方库可能在暗黑模式下出现显示异常。需要对这些情况进行充分测试,并提供相应的解决方案。
  2. 用户体验 在手动切换暗黑模式时,要确保切换过程平滑,避免出现界面闪烁等问题。同时,要提供清晰的提示,告知用户当前的模式状态。对于一些特定的内容,如图片、视频等,要确保在暗黑模式下不会影响其可读性和可视性。例如,对于一些带有白色文字的图片,在暗黑模式下可能需要进行特殊处理,以保证文字的清晰显示。
  3. 资源命名规范 在创建暗黑模式资源时,要保持良好的命名规范。例如,在颜色资源文件中,对于暗黑模式下的颜色,可以在命名上加上 _night 后缀,以便于区分和管理。同样,对于图标资源,也可以采用类似的命名方式,如 ic_menu_night.png。这样可以使项目的资源结构更加清晰,便于维护和扩展。
  4. 测试覆盖 要进行全面的测试,包括不同模式下的界面布局、颜色显示、交互操作等。可以使用模拟器和实际设备进行测试,覆盖不同的屏幕尺寸、分辨率和 Android 版本。同时,要测试手动切换模式和跟随系统模式切换的各种场景,确保应用在各种情况下都能正常适配暗黑模式。