Compose中的国际化与本地化、暗黑模式与夜间模式
开篇闲谈
这两年负责的都是面向海外(欧美、中东等)的项目,之前在View的时代下总结了一套国际化与本地化的经验,见《Android 国际化与本地化探索》,文中事无巨细的从 语言翻译 、 UI设计 、 代码规范 三个方面阐述了我的解决方案。
切换到到Compose后,又完全处理了一遍国际化的流程。同时发现在适配暗黑模式中Compose提供了开箱即用的支持,大大简化了我们的开发难度,这篇文章就将经验分享给大家。文中若有纰漏之处,还望大家不吝赐教。
国际化与本地化
关于国际化中的翻译规范以及UI设计规范,这里就不再赘述了,大家可以翻翻开篇提到的文章,我们直接从代码层面入手。
文字处理
如上图所示,我们需要实现简体中文和阿拉伯语的相关页面。
首先我们需要准备相关的语言资源文件,然后消息列表的整体页面只是使用了Row和Column来构建,没有其他多余操作,代码不做过多展示,预览代码如下所示:
@Preview(locale = "zh")
@Composable
fun PreviewMessageListScreen() {
MessageListScreen()
}
@Preview(locale = "ar")
@Composable
fun PreviewMessageListScreenRTL() {
MessageListScreen()
}
可以看到我们只是给Preview注解添加了local参数,预览RTL的预览效果就完全显示出来了,而且运行在手机上的话,是会根据手机系统设置的语言以及布局方向来进行展示的。
再来看这样一种场景,在View体系下,例如阿拉伯语环境下有中文的时候,如果不对TextView的gravity、textAlignment属性进行合适的处理,那么情况可能就会出现下图左侧的样子。
按道理来说,文本内容应该是贴着图标一侧的,可是在切换了RTL语言后,中文文本内容却还是位于屏幕的左侧,这是由于在View体系下,Gravity和TextAlignment都会控制文本的布局方向。而到了Compose中,Text布局方向则是默认听从父布局容器的安排,自己不会做任何多余的处理。所以在Compose中按照正常写法开发下来,效果图就是如上右侧图片所示,完全不用做过多的设置。
所以可能一开始大家在处理Compose中Text居中等情况下都遇到了难题,到底要如何居中呢?其实就是需要在外层嵌套一个Row或者Box等布局,然后将布局方向改为居中就可以了,或者使用其参数TextAlign也可以。
图标处理
上文是基本的布局处理,涉及到一些图片的处理,我们还是使用在View体系下的方案,利用缩放来处理图片的左右翻转,自定义Modifier代码如下所示:
@Composable
fun Modifier.rtl(): Modifier = composed {
val layoutDirection = LocalLayoutDirection.current
scale(
scale = if (layoutDirection == LayoutDirection.Rtl) {
-1f
} else {
1f
}
)
}
如果布局方向是RTL的那么将图片进行左右镜像,我们将下图的返回按钮图标应用上述的自定义Modifier,那么切换到RTL布局后,返回按钮图标的方向就会产生镜像变化。而另一个图标没有应用该Modifier,那么它的方向就不会发生变化。
暗黑模式与夜间模式
关于暗黑模式与夜间模式其实还是有区别的:
-
暗黑模式:如果你的手机是OLED屏幕,那么在黑色像素下屏幕基本不发光,所以会有省电的效果。
-
夜间模式:其使用场景是在夜间,也就是弱光环境下,所以要解决的问题是减少夜间使用情况下屏幕强光对眼睛的刺激效果。
针对上述示例图,我们只能说基本实现了暗黑模式,如果再针对图片等整体进一步的处理,降低亮度、减少对比度,减少对用户眼睛的刺激,那么这才能称的上是一个更加提升用户在晚上使用体验的夜间模式。
文字颜色及背景色的处理
接下来我们主要说下暗黑模式在Compose中的实现,但是并不使用Compose官方提供的Material Theme,而是使用CompositonLocal重新自定义一套类似的颜色主题。如下颜色数据类所示,我们大致需要一个主文本颜色,次文本颜色以及背景颜色,分别设置Light、Dark Model来实现上方图片所示效果。
data class MyColor(
val textPrimary: Color,
val textSecondary: Color,
val background: Color
)
//Light Mode
val myLightColors = MyColor(
textPrimary = Color(0xFF333333),
textSecondary = Color(0xFF666666),
background = Color(0xFFFFFFFF)
)
//Dark Mode
val myDarkColors = MyColor(
textPrimary = Color(0xFFFFFFFF),
textSecondary = Color(0xCCFFFFFF),
background = Color(0xFF333333)
)
因为颜色主题的值是确定的,不会发生更改,所以我们使用staticCompositionLocalOf来创建CompositionLocal,以此来提高性能。
然后创建单例模式的主题,在后续的关于颜色的使用中,我们都需要使用该主题所提供的颜色。
//创建CompositionLocal
val LocalMyColors = staticCompositionLocalOf {
myLightColors
}
//创建自己的主题
object MyTheme {
val colors: MyColor
@Composable
get() = LocalMyColors.current
}
@Composable
fun ComposeSampleTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit,
) {
val myColors = if (darkTheme) {
myDarkColors
} else {
myLightColors
}
CompositionLocalProvider(
LocalMyColors provides myColors
) {
MaterialTheme(
colors = colors,
typography = Typography,
shapes = Shapes,
content = content
)
}
}
最后我们需要使用CompositionLocalProvider来将我们的主题颜色提供出去,默认是Light Mode,这样的话在ComposeSampleTheme下的可组合函数都可以有效的使用我们自定义的颜色信息了。
//使用自定义的主题颜色--MyTheme.colors.textPrimary
Text(
text = title,
fontSize = 16.sp,
color = MyTheme.colors.textPrimary,
fontWeight = FontWeight.Bold,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
预览的时候则传递是否使用Dark Mode的参数即可:
@Preview()
@Composable
fun PreviewMessageListScreenDark() {
ComposeSampleTheme(darkTheme = true) {
MessageListScreen()
}
}
图标颜色处理
如下图所示,在Light及Dark模式下,图标的颜色是需要做区别处理的。如果根据不同的模式,分别使用不同的图片资源,这是一种解决方案,但是使用多套资源文件会造成APP体积的增大。
所以我们可以考虑,当使用Dark模式时,使用ColorFilter来改变图标的颜色,这样既不会增加体积,也方便我们处理各种不同的颜色,代码如下所示:
@Composable
fun ThemeColorFilter(
color: Color? = null
): ColorFilter? {
val isDarkTheme = isSystemInDarkTheme()
if (isDarkTheme) {
return ColorFilter.tint(color = Color(0xCCFFFFFF))
}
return if (color == null) {
null
} else {
ColorFilter.tint(color = color)
}
}
当处于暗黑模式时,我们就给图标使用灰白色的色值,否则就不使用滤色器或者使用自定义的图标色值。
至此基本的暗黑模式框架已经完成了,后续就需要根据自己App的情况添加不同的颜色数据来一步步完善。
总结
Compose作为现代化的UI工具包,也吸取了View时代下的各种开发经验,在处理国际化及暗黑模式中着实方便很多。