引言
JetPack Compose
正式版已经发布好几个月了,在这段时间里,除了业务相关需求之外,我也开始了 Compose
在实际项目中的落地实验,因为一旦要接入当前项目,那么遇到的问题其实远远大于新创建一个项目所需要的问题。
本篇要解决的就是 Compose
默认 Material
主题颜色太少,如何配置自己的业务颜色板,或者说,如何自定义自己的颜色系统,并由点入深,系统的分析相关实现方法与原理。
问题
在开始之前,我们先看看目前创建一个 Compose
项目,默认的 Material
主题为我们提供的颜色有哪些:
对于一个普通的应用而言,默认的已基本满足开发使用,基本的主题配色已经足够。但是此时一个问题出现了,如果我存在其他的主题配色呢?
传统做法
在传统的 View
体系中,我们一般都会将颜色定义在 color.xml 文件中,在使用的时候直接读取即可,getColor(R.xx) ,这个大家都已经很熟悉了,那么在 Compose
中呢?
Compose
在 Compose
中,google
将颜色数值统一放在了 theme
下的 color.kt 中,这其实也就是全局静态变量,乍一看好像没什么问题,那我的业务颜色放在那里呢,总不能都全局暴露吧?
但是聪明的你肯定知道,我按照老办法放到 color.xml 里不就行哈,这样也不是不可以,但是随之而来的问题如下:
- 切换主题时候,颜色怎么统一解决?
- 在
Google
的 simple 里,color.xml 里往往不会写任何配置,即 Google 本身不建议在compose
里这样用
那么我该怎么办,我去看看google的simple,看看他们怎么解决:
simple果然是simple 😑 ,Google 完全按照 Material 的标准,即不会出现其他的非主题配色,那实际呢,我们开发怎么办。然后我搜了下目前github上大佬们写的一些开源项目,发现他们也都是按照 Material 去实现,但是很明显这样很不符合实际。🙃
解决思路
随心所欲写法(不推荐)
既然官方没写怎么解决,那就自己想办法解决喽。
compose 中,对于数据的改变监听是使用 MutableState ,那么我自己自定义一个单例持有类,持有现有的主题配置,然后定义一个业务颜色类,并且定义相应的主题颜色类对象,最终根据当前单例的主题配置,去判断最终使用什么颜色板即可,更改业务主题时只需要更改这个单例主题配置字段即可。一想到如此简单,我可真是个抖机灵,说干就干 👨🔧
创建主题枚举
enum class ColorPallet { // 默认就给两个颜色,根据需求可以定义多个 DARK, LIGHT }
增加主题配置单例
object ColorsManager { /** 使用一个变量维护当前主题 */ var pallet by mutableStateOf(ColorPallet.LIGHT) }
增加颜色板
/** 共用的颜色 */ val Purple200 = Color(0xFFBB86FC) val Purple500 = Color(0xFF6200EE) val Purple700 = Color(0xFF3700B3) val Teal200 = Color(0xFF03DAC5) /** 业务颜色配置,如果需要增加其他主题业务色,直接定义相应的下述对象即可,如果某个颜色共用,则增加默认值 */ open class CkColor(val homeBackColor: Color, val homeTitleTvColor: Color = Color.Gray) /** 提前定义好的业务颜色模板对象 */ private val CkDarkColor = CkColor( homeBackColor = Color.Black ) private val CkLightColor = CkColor( homeBackColor = Color.White ) /** 默认的颜色配置,即Md的默认配置颜色 */ private val DarkColorPalette = darkColors( primary = Purple200, primaryVariant = Purple700, secondary = Teal200 ) private val LightColorPalette = lightColors( primary = Purple500, primaryVariant = Purple700, secondary = Teal200 )
增加统一调用入口
为了便于实际使用,我们增加一个 MaterialTheme.ckColor的扩展函数,以便使用我们自定义的颜色组:
/** 增加扩展 */ val MaterialTheme.ckColor: CkColor get() = when (ColorsManager.pallet) { ColorPallet.DARK -> CkDarkColor ColorPallet.LIGHT -> CkLightColor }
最终的主题如下
@Composable fun CkTheme( pallet: ColorPallet = ColorsManager.pallet, content: @Composable() () -> Unit ) { val colors = when (pallet) { ColorPallet.DARK -> DarkColorPalette ColorPallet.LIGHT -> LightColorPalette } MaterialTheme( colors = colors, typography = Typography, shapes = Shapes, content = content ) }
效果图
看效果也还成,简单粗暴,[看着] 也没什么问题,那有没有什么其他方式呢?我还是不相信官方没有写,可能是我疏忽了。
自定义颜色系统(官方)
就在我翻官方文档时,突然看见了这样几个小字,它实现了自定义颜色系统。
增加颜色模板
// 示例,正确做法是放到color.kt下 val Blue50 = Color(0xFFE3F2FD) val Blue200 = Color(0xFF90CAF9) val A900 = Color(0xFF0D47A1) /** * 实际主题的颜色集,所有颜色都需要添加到其中,并使用相应的子类覆盖颜色。 * 每一次的更改都需要将颜色配置在下方 [CkColors] 中,并同步 [CkDarkColor] 与 [CkLightColor] * */ @Stable class CkColors( homeBackColor: Color, homeTitleTvColor: Color ) { var homeBackColor by mutableStateOf(homeBackColor) private set var homeTitleTvColor by mutableStateOf(homeTitleTvColor) private set fun update(colors: CkColors) { this.homeBackColor = colors.homeBackColor this.homeTitleTvColor = colors.homeTitleTvColor } fun copy() = CkColors(homeBackColor, homeTitleTvColor) } /** 提前定义好的颜色模板对象 */ private val CkDarkColors = CkColors( homeBackColor = A900, homeTitleTvColor = Blue50, ) private val CkLightColors = CkColors( homeBackColor = Blue200, homeTitleTvColor = Color.White, )
增加 xxLocalProvider
@Composable fun ProvideLcColors(colors: CkColors, content: @Composable () -> Unit) { val colorPalette = remember { colors.copy() } colorPalette.update(colors) CompositionLocalProvider(LocalLcColors provides colorPalette, content = content) }
增加 LocalLcColors 静态变量
// 创建静态 CompositionLocal ,通常情况下主题改动不会很频繁 private val LocalLcColors = staticCompositionLocalOf { CkLightColors }
增加主题配置单例
enum class StylePallet { // 默认就给两个颜色,根据需求可以定义多个 DARK, LIGHT } /* 针对当前主题配置颜色板扩展属性 */ private val StylePallet.colors: Pair<Colors, CkColors> get() = when (this) { StylePallet.DARK -> DarkColorPalette to CkDarkColors StylePallet.LIGHT -> LightColorPalette to CkLightColors } /** CkX-Compose主题管理者 */ object CkXTheme { /** 从CompositionLocal中取出相应的Local */ val colors: CkColors @Composable get() = LocalLcColors.current /** 使用一个state维护当前主题配置,这里的写法取决于具体业务,如果你使用了深色模式默认配置,则无需这个变量,即app只支持深色与亮色,那么只需要每次读系统配置即可。但是compose本身可以做到快速切换主题,那么维护一个变量是肯定没法避免的 */ var pallet by mutableStateOf(StylePallet.LIGHT) }
最终主题代码
@Composable fun CkXTheme( pallet: StylePallet = CkXTheme.pallet, content: @Composable () -> Unit ) { val (colorPalette, lcColors) = pallet.colors ProvideLcColors(colors = lcColors) { MaterialTheme( colors = colorPalette, typography = Typography, shapes = Shapes, content = content ) } }
分析
最终的效果和上述的一致,也就不具体赘述了,我们主要来分析一下,为什么Google要这么写:
我们可以看到上述的示例里主要是使用了 CompositionLocalProvider
去保存当前的主题配置 ,而 CompositionLocalProvider
又继承自 CompositionLocal
,比如我们常用的 MaterialTheme
主题中的 Shapes
,typography
都是由此来管理。
CkColors
这个类上增加了 @Stable ,其代表着对于 Compose
而言,这个类是一个稳定的类,即每次更改不会引发重组,内部的颜色字段使用了 mustbaleStateOf
包装,以便当颜色更改时触发重组,内部还增加了 update() 与 copy() 方法,以便于管理与单向数据的更改。
其实如果我们去看的 Colors
类。就会发现上述示例中的 CkColors
和其是完全一样的设计方式。
所以在Compose中自定义主题颜色,其实就是我们在 Colors
的基础上自己又写了一套自己的配色。😂
既然这样,那为什么我们不直接继承Colors去增加配色呢?使用的时候我强制一下不就行,这样不就不用再自己造什么 CompositionLocal 了?
其实很好理解,因为 Colors
中的 copy() 以及 update() 无法被重写,即没加 open
,而且其内部变量使用了 internal
修饰 set
方法更重要的原因是这样 不符合Md的设计 ,所以这也就是为什么 需要我们去自定义自己的颜色系统,甚至于可以完全自定义自己的主题系统。前提是你觉得自定义的主题里面再包装一层 MaterialTheme
主题比较丑陋的话,当然相应的,你也需要考虑如何解决其他附带的问题。