什么是深色模式?
Android 深色模式(Dark Mode)是 Android 操作系统提供的一种显示模式,它将应用程序的整体配色方案从亮色模式切换为暗色模式。在深色模式下,背景通常为深色或黑色,而文本和其他 UI 元素则使用浅色调。深色模式不仅可以改善视觉体验,还可以在某些场景下减少电量消耗,尤其是在 AMOLED 屏幕上。
Android Compose如何适配深色模式
xml布局时代
在非ComposeUI的情况下,我们可以使用资源文件限定符的形式(drawable-night,values-night)进行深色模式和浅色模式资源进行区分。在上述文件夹下放入深色模式图片,文案及color资源等。上层使用没有变化。即还是按正常获取方式进行获取,当app在深色模式情况下,会自动获取其位于night文件夹下的资源。实现资源自动适配。那么Compose中深色模式适配也会如此简单吗?它的图片和颜色资源等是如何适配深色模式的?
Compose遇到的问题
在Compose中系统给我们提供了两个方法colorResource() 和 painterResource(),我们还是可以通过资源id形式,用这两个方法去分别拿图片资源和颜色资源的。但是经过详细测试后,令人烦恼的事情发生了。我们发现在系统设置深色模式和app不一致时,代码总是选择系统设置的去加载。举个例子:如果app设置深色模式,但是安卓系统设置的是浅色模式。那么Compose中通过 colorResource() 和 painterResource() 拿取到的资源也是浅色模式始终跟随系统,而不是根据app配置的,这和产品需求以及开发预期都是不一致的。下面我们介绍下深色模式切换配置,以及这两个方法的使用。
- 设置跟随系统
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
- 设置浅色模式
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
- 设置深色模式
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
- colorResource的使用
如果在values-night文件夹下有配置app_bar_bg_color,则程序会根据系统的深色或浅色模式自动拿取相应的color资源
colorResource(id = R.color.app_bar_bg_color)
此方法返回的Color可直接赋值给Compose的Text组件
- painterResource的使用
如果在drawable-night文件夹下有配置icon_info_preview,则程序会根据系统的深色或浅色模式自动拿取相应的drawable资源
painterResource(id = R.drawable.icon_info_preview)
此方法返回的painter可直接赋值给Compose的Image组件
解决办法
遇到上述问题后,我们知道不能只是简单使用 colorResource() 和 painterResource() 方法获取color和图片资源资源进行深色模式适配。后面我想到两种方式:
1主题
利用Compose官方主题进行颜色适配是google官方比较推荐的一种方式。
在 Jetpack Compose 中,主题(Theme)可以用来定义应用的整体外观和样式,例如颜色、形状、排版等。通过配置主题,开发者可以轻松应用自定义的风格到整个应用或特定的组件上。
Compose 使用 MaterialTheme 作为主要的主题系统,这与传统的 Android XML 主题不同。通过 MaterialTheme,可以控制应用的配色方案、字体、形状等。
1. 创建主题
Compose 主题的配置通常通过 MaterialTheme 组件来实现。MaterialTheme 提供了三大核心属性:
• colors: 控制颜色方案
• typography: 控制字体排版
• shapes: 控制组件的形状(例如按钮的圆角等)
@Composable
fun MyAppTheme(content: @Composable () -> Unit) {
// 使用 MaterialTheme 来定义应用的主题
MaterialTheme(
colorScheme = myColorScheme(),
typography = myTypography(),
shapes = myShapes(),
content = content
)
}
2. 配置颜色
在 Compose 中,颜色是通过 colorScheme 来定义的,类似于传统的 colors.xml 文件。你可以自定义颜色并应用到主题中。使用 Color 类来定义颜色,然后在 ColorScheme 中设置它们。方式如下
import androidx.compose.ui.graphics.Color
import androidx.compose.material3.lightColorScheme
import androidx.compose.material3.darkColorScheme
// 定义颜色
val Purple200 = Color(0xFFBB86FC)
val Purple500 = Color(0xFF6200EE)
val Purple700 = Color(0xFF3700B3)
val Teal200 = Color(0xFF03DAC5)
// 配置浅色模式的颜色方案
private val LightColors = lightColorScheme(
primary = Purple500,
onPrimary = Color.White,
primaryContainer = Purple200,
onPrimaryContainer = Color.Black,
secondary = Teal200
)
// 配置深色模式的颜色方案
private val DarkColors = darkColorScheme(
primary = Purple200,
onPrimary = Color.Black,
primaryContainer = Purple700,
onPrimaryContainer = Color.White,
secondary = Teal200
)
// 颜色方案选择函数,自动根据模式选择
@Composable
fun myColorScheme(isDarkTheme: Boolean = false) = if (isDarkTheme) DarkColors else LightColors
3. 使用颜色
@Composable
fun MyAppTheme(isDarkTheme: Boolean = false, content: @Composable () -> Unit) {
MaterialTheme(
colorScheme = myColorScheme(isDarkTheme),
typography = myTypography(),
shapes = myShapes(),
content = content
)
}
4. 将主题应用到界面中
@Composable
fun MyApp(content: @Composable () -> Unit) {
MyAppTheme {
// 所有的 UI 都会继承定义的主题样式
Surface {
content()
}
}
}
5. 使用主题中的属性
在 Composable 函数中,使用 MaterialTheme 提供的属性来动态地获取主题中的颜色、排版或形状:
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
@Composable
fun Greeting() {
Text(
text = "Hello, Compose!",
color = MaterialTheme.colorScheme.primary, // 使用主题的主颜色
style = MaterialTheme.typography.titleLarge // 使用主题的排版样式
)
}
主题总结:
上面的主题形式可解决深色模式适配的问题。但同时又暴露出另外的问题。
- colorScheme中的颜色名称都是预置的不能很好的自定义。无法覆盖我们UI的设计师千奇百怪的色值需求,面对五彩斑斓的黑的需求我们无法应对。
- colorScheme中大量内置名称公司APP基本应用不上。定义标准不一样不能强复用。
2自定义适配
根据上面主题的启发,和之前在Flutter中做深色模式适配的经验。我想到了另外一种适配深色模式的方法。介绍如下:
1. 定义开放基类 IeltsColors
open class IeltsColors(
val app_bg_gray_color:Color,
val app_bg_bar_color:Color,
val app_text_primary_color:Color,
val app_text_secondary_color:Color,
val app_line_gray_color:Color,
val app_dialog_bg_color:Color,
//深色浅色模式通用的在此处直接赋值
val C_FFFE2442: Color =Color(0xFFFE2442),
val C_FFFE5B64: Color =Color(0xFFFE5B64),
val C_FF8C91A3: Color =Color(0xFF8C91A3),
val app_iconfont_color:Color,
)
其中未给出默认值的是深色模式和浅色模式有不同的色值,给出默认值的则为深浅两种模式相同色值。
2. 定义浅色模式,色值类(继承自IeltsColors)并给其中基类没有默认值的属性赋值
/**
* lightColor 浅色模式
*/
class LightColor : IeltsColors(
app_bg_gray_color = Color(0xFFF5F6F9),
app_bg_bar_color = Color(0xFFFFFFFF),
app_text_primary_color = Color(0xFF32374E),
app_text_secondary_color = Color(0xFF989DA9),
app_line_gray_color = Color(0xFFECECEC),
app_dialog_bg_color = Color(0xFFFFFFFF),
app_iconfont_color = Color(0xFF32374E),
)
3. 定义深色模式,色值类(继承自IeltsColors)并给其中基类没有默认值的属性赋值
/**
* darkColor 深色模式
*/
class DarkColor : IeltsColors(
app_bg_gray_color = Color(0xFF000000),
app_bg_bar_color = Color(0xFF1D1B1D),
app_text_primary_color = Color(0xFFF2F4FF),
app_text_secondary_color = Color(0xFF959BA0),
app_line_gray_color = Color(0xFF3F4044),
app_dialog_bg_color = Color(0xFF4F4A4F),
app_iconfont_color = Color(0xFFF2F4FF),
)
4. 创建IeltsTheme
根据上面的物料,我们可以定义自己的主题对象啦。代码如下:
object IeltsTheme{
//定义light和dark颜色
private val lightColors = LightColor()
private val darkColors = DarkColor()
//定义外接拿取的theme。
private var darkTheme = DarkModeUtils.isNightMode(IeltsApp.getIeltsApp())
var currentMode: IeltsColors = if(darkTheme){
darkColors
}else{
lightColors
}
/**
* Activity启动或暗黑切换时,重置theme
*/
fun resetDarkTheme():Boolean{
darkTheme = DarkModeUtils.isNightMode(IeltsApp.getIeltsApp())
currentMode = if(darkTheme){
darkColors
}else{
lightColors
}
return darkTheme
}
fun getIsDarkTheme():Boolean{
return darkTheme
}
}
我们将LightColor和DarkColor在此单例类中实例化,并根据我们自己拿取的主题模式(是深色还是浅色)来进行currentMode属性的初始化。程序会根据app设置的模式,来自动调节主题颜色。此时外界只需要做如下使用即可:
color = IeltsTheme.currentMode.app_line_gray_color
- 优点
操作简单,利用此套逻辑。如果定义深浅一致色值我们只需要在IeltsColors直接定义及初始化即可。定义深浅不一致色值,我们只需要在IeltsColors定义好属性,然后在相应的LightColor和DarkColor中初始化即可。使用起来也极其方便。
- 缺点
此方式跟主题方案相比,不能定义公共样式。做不到主题里配置后,其他地方默认都应用了。得需要在相关容器中各自使用定义颜色。
- 坑
因为IeltsTheme单例类中darkTheme属性是初始化后不再改变了,所以在改变app或系统主题后Compose UI 可能不会及时刷新。因为darkTheme值没有变从而currentMode也没有变化。解决方式是在使用类的
解决方式1
onCreate中增加如下代码:
override fun onCreate(savedInstanceState: Bundle?) {
IeltsTheme.resetDarkTheme()
super.onCreate(savedInstanceState)
}
当然,我们可以抽取公共基类,替代上面重复的操作
/**
* 此类将用作ComposeActivity基类,
* 1. 目前主要作用是在所有ComposeActivity onCreate前重置一下主题Theme,以应对暗黑 跟随系统切换的场景
*/
open class BaseComposeActivity: ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
IeltsTheme.resetDarkTheme()
super.onCreate(savedInstanceState)
}
}
解决方式2
在后续进行技术探讨时,觉得上面的解决方式会有IeltsTheme.resetDarkTheme()调用过于频繁
的问题。经后续搜索资料发现,可以在应用的Application中注册深色模式改变的监听,从而进行resetDarkTheme()方法的调用。当你手动切换系统深色模式或浅色模式时,系统会先调用此监听器
,再执行目标Activity的onCreate
。此方式精简调用次数
。也更便于理解
。下面是相关java
代码,kotlin
请自行修改。
registerComponentCallbacks(new ComponentCallbacks() {
@Override
public void onConfigurationChanged(@NonNull android.content.res.Configuration newConfig) {
IeltsTheme.INSTANCE.resetDarkTheme();
}
@Override
public void onLowMemory() {
}
});
注意,按照解决方式2修改后,当在设置成不跟随系统(app自己设置固定深色或浅色模式
)时,你需要自行在设置模式前调用resetDarkTheme
,因为上面的监听检测不到。
适配过程中遇到的问题。
- 开始用的
colorResource
获取颜色的方案在经过测试后居然不能跟随应用暗黑切换,只跟系统暗黑切换,产生了巨大的困惑。为什么这么设计??? - 一开始不知道
ColorScheme
可以扩展,从而探寻出了上面第二条深色适配的路子。
现在该讲一讲写这篇文章时才发现的最新的收获了。
原来ColorScheme可以扩展,它不局限于系统定义的那几个名称,开发者可以任意定义属性扩展,从而满足各种app奇葩需求。
在Android Developer官网自定义主题章节介绍到,我们可以通过给其添加扩展属性的方式进行扩展。代码如下:
// Use with MaterialTheme.colorScheme.snackbarAction
val ColorScheme.snackbarAction: Color
@Composable
get() = if (isSystemInDarkTheme()) Red300 else Red700
Compose 深色模式和主题,现在可以好好玩耍了吧!
补充
- isSystemInDarkTheme() 方法返回的主题模式也是跟系统绑定的,而非App配置的。
- 系统主题是根据切换 colorScheme 来切换颜色系统的。
- 如何让 isSystemInDarkTheme() 方法跟随你自己配置,而非系统配置 ???
resources.configuration.uiMode= Configuration.UI_MODE_NIGHT_NO
- 正在探索可以让colorResource和painterResource方法跟随应用配置,而非系统配置的方法。
接下来你就可以按照你的需求来随意配置app啦。