React Native 暗黑模式适配方案
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了React Native 暗黑模式适配方案相关的知识,希望对你有一定的参考价值。
参考技术AAppearance 提供的 API
考虑到项目都是通过 class 实现,那么我们优先研究非 Hook 方式如何实现。
这里有两个问题需要注意:
暗黑模式和正常模式之间来回切换
根据上面的 API,我们很容易用到 addChangeListener 方法来监听。但是这里有个问题需要考虑,如果 App 的暗黑模式只追随系统变化,那么就简单很多了,接下来只需要考虑,如何优雅的实现即可。实际业务中有很多场景是根据当前 App 的设置而定的。
Appearance 提供的 API 只能读取状态,没法修改。在实践中我们发现在原生中修改暗黑模式的状态 RN 的 Appearance 响应,android 可以做到,而 ios 暂时没有找到方法;
Android 通过获取 RN 当前的环境是可以修改
iOS 原生中修改 RCTRootView 的 overrideUserInterfaceStyle 属性,或者遍历当前 RN 视图进行修改, RN 的 Appearance 是没法响应的。
React Native 内部的实现可以参考 react-native-appearance
小结
如果 App 需要支持自定义切换暗黑模式(不追随系统变化而变化),那么通过 React-Native 中 Appearance 暂时是无法实现的。
既然 RN 的暗黑模式只通过原生读取,那么在 RN 中的状态也只能自定义了,同样上面两个问题也需要解决。
暗黑模式和正常模式之间来回切换
原生通知各个 RN 模板进行变化即可,当然为了避免各个模块的子视图做重复监听,可以通过 Provider 来实现。
以 iOS 为例,原生需要支持
在 RN 模块中,比较自然的想到统一监听原生的暗黑模式状态变化以及通过 Provider 为子视图提供统一的状态
接下来看一下业务的实现
通过上面的方式,业务需求也是可以实现的,只是方式有点难看。
接下来就是如果把实现方式变得优雅一些
每个地方都来写把暗黑模式和正常模式下的颜色很冗余,也不利于统一管理,比较容易想到就是封装一个 DarkColorUtility 来统一管理颜色。这样会遇到一个问题,在 DarkColorUtility 中如何获取 DarkModeContext
根据发现只能通过 hook 的方式才能获取 DarkModeContext ,那么 TestMain 也只能改成 function 的方式,第一步优化之后的效果
业务调整之后的方式
这一步其实已经差不多了,在实际开发用很多同学其实不怎么喜欢用 StyleSheet 来创建 style ,比如
这样在实际调试中比较方法,不需要在 StyleSheet 中业务中来回找,同时在那些特别复杂的界面命名的负担也是很重的。
不过考虑到还是有很多同学喜欢用 StyleSheet ,那么就继续思考,怎样在 StyleSheet 中写 DarkColorUtility 中的工具方法。
为了在 StyleSheet 中直接使用 DarkColorUtility 中的工具方法,发现只能对 StyleSheet 进行重新封装了。这里我们就直接参考 react-native-dynamic 的实现。
在 DynamicStyleSheet 使用的 DynamicValue 相当于 DarkColorUtility 。
业务效果如下
方案一:业务通过 class 实现
方案二:业务通过 function 实现,同时不用 StyleSheet 来创建 style
方案三:业务通过 function 实现,同时也要用 StyleSheet 来创建 style ,借助 react-native-dynamic 来实现
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时代下的各种开发经验,在处理国际化及暗黑模式中着实方便很多。
以上是关于React Native 暗黑模式适配方案的主要内容,如果未能解决你的问题,请参考以下文章
SyntaxError - node_modules/react-native/Libraries/polyfills/error-guard.js:缺少分号。 (14:4) 在 react nati
react-native app 屏幕适配方案(按照设计稿像素大小写就行)
react-native 不同分辨率设备适配(rem、px解决方案)
Kotlin + MVVM + LCE 版玩安卓,暗黑模式横竖屏无网弱网无数据加载失败等等各种情况,协程RoomHiltDataStoreLiveDataRetrofit屏幕适配