其实,所谓的色值组就是一个 Colors
对象,Compose 中默认就有 lightColors
和 darkColors
两种 Colors
对象,分别用于暗夜模式和白天模式的主题色值的设置,我们这里统一是以白天模式的 lightColors
对象为基准来进行其他主题色值的设置,作为例子这里就重写了 primary
和 background
两个属性,分别用来设置文案色值和背景色的色值。
定义好自定义主题中的各个色值组后,别忘了最后还是要设置到 MaterialTheme
中的 colors
属性中,然后我们才可以通过调用 MaterialTheme colors
来使用自定义主题中的各个色值。下面的代码就是使用样例:
// code 12 CustomTheme(chosenThemeId) { Surface(color = MaterialTheme.colors.background) { ··· } }
所以,如果我们要新增一组色值,我们只需要在 CustomTheme
中新增一组主题色值就可以了,不用去改动设置色值的代码,改动代码量较少。
再来看看切换主题的点击触发事件,显然是在这几个小方块里,而且每个方块代表一种主题,具体的代码如下:
// code 13 @Composable fun ThemeColorCube(themeItem: ThemeItem, chosenThemeId: MutableState<String>, onClick: () -> Unit) { Surface( shape = RoundedCornerShape(10.dp), elevation = 5.dp, color = themeItem.mainColor, modifier = Modifier .size(85.dp) .padding(10.dp) .clickable { onClick() } ) { Row( horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically ) { if (themeItem.id.name == chosenThemeId.value) { Image( modifier = Modifier.size(20.dp), painter = painterResource(id = R.drawable.ic_checkbox_selected_gray), contentScale = ContentScale.FillBounds, contentDescription = "被选中标记图" ) } else { Text( text = themeItem.name, textAlign = TextAlign.Center, style = TextStyle(color = MaterialTheme.colors.primary) ) } } } } data class ThemeItem( val id: ThemeKinds, //主题 id val name: String, //主题 name val mainColor: Color, //主色 )
点击事件的回调在主页面 LazyRow
列表的方法中:
// code 14 LazyRow() { items(themeList) { item: ThemeItem -> ThemeColorCube(themeItem = item, chosenThemeId) { //点击色块选择其中的一种颜色 MMKV.defaultMMKV().putString(MMKVConstant.ChosenThemeCode, item.id.name) chosenThemeId.value = item.id.name } } }
可以看到,点击之后,需要将选中的主题 id
存储在本地,以便下次打开 App 可以获取到选中的主题并设置相应的主题色值组,更为重要的是更新 MutableState
对象,即通过 CustomTheme
传进来的 chosenThemeId
的值。由于 MutableState
的特性,所有引用它的地方,都会触发重组,从而会使得 CustomTheme
重组,重组会根据到更新后的 chosenThemeId
的值来设置色值组,那么 MaterialTheme.colors
的色值组就切换为新选中主题的色值组了。
另外文案字体和大小,以及图片的圆角大小,都是类似的原理,不再赘述,文末见源码获取方法。
5. 彩蛋 —— 切换主题进阶版
这就完了么?作为主题切换功能来讲,已经实现完了,但,刚刚的切换过程是不是感觉比较生硬?有没有更加丝滑的做法?答案当然是有的。
如图3 所示,每次切换时,背景色和字体大小、圆角大小都是渐变的,切换过程丝滑,过渡自然。
要想实现丝滑的效果,先得认识一位新的朋友:animateXxxAsState。
5.1 animateXxxAsState
看前缀就知道是为动画而生的,Xxx 是因为它有许多重载的参数方法,比如 Color、Dp、Float 等,我们这里色值的渐变就是用到的 animateColorAsState
方法。同样地,文案字体大小的动画以及圆角的动画,分别使用的是 animateFloatAsState
和 animateDpAsState
方法。
这一类方法非常好用,官方文档上是这么介绍 animateColorAsState
方法的:
Fire-and-forget animation function for Color.
只需要触发调用它即可,不用管其他的事情。这里只对 animateColorAsState
方法进行举例说明,其他方法以此类推。先来看看它的声明:
// code 15 @Composable fun animateColorAsState( targetValue: Color, animationSpec: AnimationSpec<Color> = colorDefaultSpring, finishedListener: ((Color) -> Unit)? = null ): State<Color>
第一个参数就是设置色值渐变的终值,一旦设置的终值改变,渐变的动画就会自动触发。当动画还未结束终值又有变化时,则动画会调整动画路径到新的终值。
第二个参数可以设置动画的执行规范,实现了 AnimationSpec
接口的有 1)FloatSpringSpec
;2)FloatTweenSpec
;3)InfiniteRepeatableSpec
;4)KeyframesSpec
;5)RepeatableSpec
;6)SnapSpec
;7)SpringSpec
;8)TweenSpec
. 这些都是针对动画进行的设置,例如动画时间,以及动画速度的变化,类似于插值器。
第三个参数就很好理解了,即动画完成后的回调方法。
返回值是一个 State
状态对象,所以它可以不断地去更新值,直至动画完成。
需要注意的是,只要动画所作用的可组合项没有从 Compose 组件树上被移除,那么这个动画方法不会被取消或被停止。
5.2 Color 渐变实现
从上一节可以得知,animateColorAsState
方法返回的是个 State
状态,我们需要这个返回值去重组更新调用了该色值的 Composable 组件,所以,每种需要渐变的色值都需要声明一个 State
状态对象,我这里统一都放在 ViewModel
中管理了:
// code 16 class MainViewModel : ViewModel() { var primaryColor: Color by mutableStateOf(Color(0xFF000000)) // 用于文案色值渐变 var backgroundColor: Color by mutableStateOf(Color(0xFFFFFFFF)) // 用于背景色渐变 ··· val chosenThemeId = mutableStateOf( MMKV.defaultMMKV().getString(MMKVConstant.ChosenThemeCode, ThemeKinds.DEFAULT.name) ?: ThemeKinds.DEFAULT.name ) }
当切换主题后,主题 id 存储的 MutableState
触发重组,然后根据新的主题 id 获取到新的色值组,这时 animateColorAsState
中的 targetValue
就发生了变化,触发渐变动画,从而不断更新 ViewModel
中的 primaryColor
State 值,进而重组所有引用了 primaryColor
值的可组合项,这时渐变效果出现。下面是 CustomTheme
部分代码:
// code 17 val targetColors: AppColors if (isSystemInDarkTheme()) { //如果是深色模式,则只能是深色模式的色值组,无法切换 targetColors = DarkColors } else { targetColors = when (mainViewModel.chosenThemeId.value) { ThemeKinds.RED.name -> { RedThemeColors } ThemeKinds.YELLOW.name -> { YellowThemeColors } ThemeKinds.BLUE.name -> { BlueThemeColors } else -> { DefaultColors } } } //渐变实现 mainViewModel.primaryColor = animateColorAsState(targetColors.primary, TweenSpec(500)).value mainViewModel.backgroundColor = animateColorAsState(targetColors.background, TweenSpec(500)).value
这里设置的渐变时长为 500ms,并且为了方便管理,将所有色值放在 AppColors
类中进行管理,各个不同的主题有着各自不同的 AppColors
类对象,如下所示:
// code 18 @Stable data class AppColors ( val primary: Color, val background: Color ) //红色主题色值 private val RedThemeColors = AppColors( primary = Color(0xFFFF4040), background = Color(0x66FF4040) ) //黄色主题色值 private val YellowThemeColors = AppColors( primary = Color(0xFFDAA520), background = Color(0x66FFD700) )
至于圆角大小以及文字大小的渐变,都是一样的实现方法,就是需要在 ViewModel
中定义需要的 MutableState
状态对象,然后使用相应的 animateXxxAsState
进行渐变动画的实现即可。
碎碎念:其实 Compose 官方教程中的 Theme 主题内容不多,且比较简单,所以就想借着主题切换的功能来巩固和运用这一知识点,希望大家能够学有所得~ 如有问题欢迎留言探讨~
如需文中源码,请在公众号回复:Compose换肤
赞人玫瑰,手留余香!欢迎点赞、转发~ 转发请注明出处~
更多内容,欢迎关注公众号:修之竹
参考文献
- Compose主题切换——让你的APP也能一键换肤;Zhujiang https://juejin.cn/post/7070671629713408031
- Android Jetpack Compose 实现主题切换(换肤);九狼 https://juejin.cn/post/7057418707357663246
- Jetpack Compose - animateXxxAsState;乐翁龙 https://blog.csdn.net/u010976213/article/details/114488661
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。