Kotlin暗黑模式适配实现方案
一、Kotlin 暗黑模式适配概述
在移动应用开发领域,暗黑模式(Dark Mode)已成为提升用户体验的重要特性。它不仅在夜间或低光环境下减轻眼睛疲劳,还能赋予应用独特的视觉风格。Kotlin 作为一种现代的编程语言,为暗黑模式的适配提供了丰富且便捷的实现方式。
暗黑模式的适配核心在于根据系统设置或用户手动切换,动态调整应用的界面元素颜色、图标样式等视觉属性。在 Kotlin 中,这涉及到资源管理、界面刷新以及对系统设置变化的监听等多方面的操作。
二、资源配置与管理
2.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>
- 在代码中获取颜色资源
在 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 图标资源
- 创建不同模式的图标资源
同样,对于图标资源,我们可以在
res/drawable/
目录下存放亮色模式图标,在res/drawable-night/
目录下存放暗黑模式图标。例如,我们有一个ic_menu
图标,在res/drawable/ic_menu.png
是亮色模式下的图标,而res/drawable-night/ic_menu.png
是暗黑模式下的图标。 - 在视图中使用图标资源
在 Kotlin 代码中,当设置
ImageView
的图标时,系统会根据当前模式自动选择正确的图标。例如:
val imageView: ImageView = findViewById(R.id.image_view)
imageView.setImageResource(R.drawable.ic_menu)
三、监听系统暗黑模式变化
3.1 使用 ConfigurationChange 监听
- 在 AndroidManifest.xml 中配置
首先,在
AndroidManifest.xml
文件中为需要监听暗黑模式变化的Activity
添加android:configChanges="uiMode"
属性:
<activity android:name=".MainActivity"
android:configChanges="uiMode">
</activity>
- 在 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 监听
- 初始化 AppCompatDelegate
在
Application
类或者Activity
的onCreate()
方法中初始化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 在设置界面添加切换开关
- 创建切换开关视图
在设置界面的布局文件(例如
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="切换暗黑模式"/>
- 在 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
来保存设置。
- 保存模式设置
在处理切换事件时,将用户选择保存到
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()
- 读取模式设置并应用
在
Application
类或者Activity
的onCreate()
方法中读取保存的设置并应用:
val sharedPreferences = getSharedPreferences("DarkModePrefs", Context.MODE_PRIVATE)
val nightMode = sharedPreferences.getInt("night_mode", AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
AppCompatDelegate.setDefaultNightMode(nightMode)
五、适配自定义视图
5.1 自定义 View 的颜色适配
- 在自定义 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)
}
}
- 处理模式变化
如果需要在模式变化时更新自定义
View
的颜色,可以在MyView
中添加一个方法来重新获取颜色并刷新视图。例如:
fun updateColor() {
primaryColor = ContextCompat.getColor(context, R.color.primary_color)
invalidate()
}
然后在模式变化监听处调用这个方法。例如,在 Activity
的 onConfigurationChanged()
方法中:
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 的图标适配
- 在自定义 View 中使用图标资源
如果自定义
View
中需要显示图标,同样可以根据模式选择不同的图标。例如,在自定义View
的onDraw()
方法中绘制图标:
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)
}
}
}
- 适配模式变化 与颜色适配类似,为了在模式变化时更新图标,我们可以添加一个方法来重新获取图标并刷新视图。例如:
fun updateIcon() {
iconDrawable = ContextCompat.getDrawable(context, R.drawable.ic_custom_icon)
invalidate()
}
在模式变化监听处调用此方法,以确保图标在不同模式下正确显示。
六、WebView 暗黑模式适配
6.1 WebView 颜色适配
- 设置 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_ON
和 WebSettings.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 性能优化
- 减少资源加载次数 在模式变化时,尽量避免不必要的资源重新加载。例如,如果某个视图在亮色和暗黑模式下的颜色变化不大,可以通过动态计算颜色而不是每次都重新加载资源。例如,对于一个文本视图,我们可以根据背景颜色的亮度动态计算文本颜色,而不是在不同模式下使用不同的颜色资源。
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
}
- 异步加载资源
对于一些较大的资源,如图片,可以采用异步加载的方式。例如,使用
Glide
库加载图片时,在模式变化时可以先显示占位图,然后异步加载新的图片。
Glide.with(this)
.load(R.drawable.dark_mode_image)
.placeholder(R.drawable.placeholder)
.into(imageView)
8.2 注意事项
- 兼容性问题 在适配暗黑模式时,要注意不同 Android 版本和设备的兼容性。例如,某些老设备可能对暗黑模式的支持存在问题,或者某些第三方库可能在暗黑模式下出现显示异常。需要对这些情况进行充分测试,并提供相应的解决方案。
- 用户体验 在手动切换暗黑模式时,要确保切换过程平滑,避免出现界面闪烁等问题。同时,要提供清晰的提示,告知用户当前的模式状态。对于一些特定的内容,如图片、视频等,要确保在暗黑模式下不会影响其可读性和可视性。例如,对于一些带有白色文字的图片,在暗黑模式下可能需要进行特殊处理,以保证文字的清晰显示。
- 资源命名规范
在创建暗黑模式资源时,要保持良好的命名规范。例如,在颜色资源文件中,对于暗黑模式下的颜色,可以在命名上加上
_night
后缀,以便于区分和管理。同样,对于图标资源,也可以采用类似的命名方式,如ic_menu_night.png
。这样可以使项目的资源结构更加清晰,便于维护和扩展。 - 测试覆盖 要进行全面的测试,包括不同模式下的界面布局、颜色显示、交互操作等。可以使用模拟器和实际设备进行测试,覆盖不同的屏幕尺寸、分辨率和 Android 版本。同时,要测试手动切换模式和跟随系统模式切换的各种场景,确保应用在各种情况下都能正常适配暗黑模式。