断更一时爽,一直断更一直爽~ 哈哈哈,就当给自己放了个长假吧。最近的行情太糟了,身边有同学已经被毕业,两个多月总算降薪找到下家 这儿呼吁咱们一定要存好六个月没有工作还能正常日子的银子,以备不时之需!期望疫情能早日平息,经济能够快速康复吧~
自己也没想到这个系列能够到第六篇,断更确实很久了,居然还收到了小伙伴的催更,感谢你们的不离不弃。闲话少说,咱们这非必须介绍的是 Compose 主题,那么 Compose 主题 Theme 到底有什么?用 Compose 完成换肤简略吗?一同来看看吧!
Jetpack Compose 的主题 Theme 便是一套 UI 风格,其间包含字体、字号、色值等等,类比于 Android View 系统中的 Theme.MaterialComponents.DayNight.DarkActionBar等等的主题款式。与 View 系统最大的不同在于,它完全抛弃了 xml 文件的设置,一切款式都是经过代码设置的,主题款式大体能够分为 色值、案牍款式、形状款式 三大类。先来看看主题中的色值。
1. Color 色值
许多组件不仅支持设置它自己的背景色,还能够设置它包含的其他可组合项的默许色值,运用 contentColorFor办法就能够完成。例如下面 code 1:
// code 1
Surface (color = Color.Yellow,contentColor = Color.Red) {
Text(text = "July 2021",style = typography.body2)
}
你会发现,Surface的背景色为黄色,而 Text中案牍为 赤色,假如将 Text换为 Icon,那么 Icon的色彩也会变为赤色,感兴趣的同学能够试试。
相似 Surface的还有 TopAppBar可组合项,下面是它们的完成源码:
// code 2
Surface(
color: Color = MaterialTheme.colors.surface,
contentColor: Color = contentColorFor(color),
...
TopAppBar(
backgroundColor: Color = MaterialTheme.colors.primarySurface,
contentColor: Color = contentColorFor(backgroundColor),
...
Compose 官方引荐运用 Surface来给任何可组合项设置色彩,因为它会设置适当的内容色彩 CompositionLocal值,看 code 2 中 Surface的 color特点就默许设置了 MaterialTheme.colors.surface色值。不引荐直接调用 Modifier.background设置色彩,因为它并没有设置任何的默许色值。在实践开发中,其实咱也没咋用到 MaterialTheme,所以这儿仍是看个人吧~
// code 3
-Row(Modifier.background(MaterialTheme.colors.primary)) { // 不引荐
+Surface(color = MaterialTheme.colors.primary) { // 引荐
+ Row(
...
在可组合项中,一些 UI 的参数是有默许值的,比方 Alpha 透明度、ContentColor 内容色等。咱们能够运用CompositionLocalProvider类去自定义这些特点的默许值。比方:
// code 4
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.disabled) {
Text(text = "Hello, 修之竹~")
}
比照没有加 CompositionLocalProvider的状况,会发现案牍色彩更浅。这是因为,默许状况下 Text案牍的 alpha值为 ContentAlpha.high,这儿设置为 ContentAlpha.disabled,还有一个 ContentAlpha.medium,alpha值的巨细排序为:high > medium > disabled。具体的值能够检查源码,它还分了高比照度和低比照度两种状况。
Compose 在暗夜形式支持方面也做的不错。比方,是否在淡色形式中运转的判别很简略:
// code 5
val isLightTheme = MaterialTheme.colors.isLight
此外,假如在实践中便是运用的 MaterialTheme中的色值来设置,那么需求留意的是,Compose 默许的可组合项中常见的状况是在淡色形式中将容器设为 primary色值,在暗夜形式中将其设为 surface色值,许多组件默许都是运用这种形式,例如TopAppBar(运用栏) 和 BottomNavigation(底部导航栏)。
2. 案牍款式
案牍款式也能够复用 MaterialTheme中已有的字体款式,当然也能够先将已有的款式 copy 一份,然后修正其间的某些特点。比方能够修正字间距:
// code 6
Text(
text = "Hello, 修之竹~",
// style = MaterialTheme.typography.body1 // 复用 MaterialTheme 中的字体款式
style = MaterialTheme.typography.body1.copy( // copy 已有款式并修正字间距特点的值
letterSpacing = 5.sp
),
fontSize = 20.sp // 在Text中设置 fontSize 可重写覆盖 MaterialTheme.typography.body1 TextStyle 中的字体巨细
)
2.1 AnnotatedString 类来设置多种款式
AnnotatedString用来替代 SpannableString最好不过了,因为它真的比 SpannableString好用多了!再也不必忧虑运用 SpannableString引发的数组越界问题了。代码及作用如下,当然还能够完成许多其他的案牍款式,感兴趣的同学能够自行查阅 SpanStyle的官方文档。
// code 7
val annotatedString = buildAnnotatedString {
withStyle(SpanStyle(color = Color.Red, fontWeight = FontWeight.Bold)) {
append("Kotlin ")
}
append("是世上 ")
withStyle(SpanStyle(fontSize = 24.sp)) {
append("最好的语言")
}
}
Text(text = annotatedString)

SpanStyle是设置案牍的款式的,作用于字符单位;而假如要针对案牍的行高、对齐方式等进行设置,则需求运用ParagraphStyle,望文生义它是针对阶段款式的。
3. 形状款式
MaterialTheme主题中也有 Shape形状特点,在许多的官方 Composable 组件中都有这个 Shape特点,比方 Button组件的 Shape特点默许值便是 MaterialTheme.shapes.small。
// code 8
fun Button(
shape: Shape = MaterialTheme.shapes.small,
) {
}
Shapes.kt提供了 small、medium、large3 种不同的特点值,其实都是 RoundedCornerShape的具体完成,只不过圆角的巨细不太相同罢了,具体数值可检查源码。
假如需求在自定义 Composable 组件中运用 Shape,有两种办法:一是运用拥有 Shape特点的官方 Composable 组件;二是运用 Modifier中可设置 shape的办法去接收自定义 Composable 组件传进来的 Shape参数值。先来看看榜首种办法,如 code 9 所示。
// code 9
@Composable
fun RoundedCornerImage(painter: Painter, cornerSize: Int) {
Surface(
shape = RoundedCornerShape(cornerSize.dp)
) {
Image(
painter = painter,
contentDescription = "圆角图片"
)
}
}
这是个能够设置图片圆角巨细的自定义 Composable 组件,因为需求用到 Shape设置圆角,所以运用了 Surface这个组件的 Shape 特点来具体完成。
第二种办法便是凭借 Modifier的办法,比方 Modifier.clip(shape: Shape)、Modifier.background(color: Color, shape: Shape = RectangleShape)、Modifier.border(width: Dp, brush: Brush, shape: Shape)等等。比较简略,感兴趣的同学能够试试。
4. 切换主题
上面说了这么多,其实都是针对单个主题说的,在实践运用中,咱们能够做个切换主题的小功用,如下图 2 所示:
其间包含了色值、字体、形状的切换,用到的思路和原理都是相同的,所以这儿就只拿主题色值的切换来说明。想要完成这一功用,首先需求理解的是,点击事情之后切换主题的回调该怎么做?
总不能给一切设置色值的当地都设置一个监听器吧?那样做想想都觉得“酸爽”。其实,在 Compose 中,咱们能够将当时主题用一个 MutableState目标来保存,然后将主题中的色值调集与这个状况相相关,当用户切换主题改变了这个 MutableState值之后,与之相关的色值调集就会收到回调进行切换,一起通知 Compose 进行重组,这样就运用新的色值调集进行烘托了。
关于 MutableState状况的相关知识,能够查阅我的另一篇文章:Jetpack-Compose 学习笔记(五)—— State 状况是个啥?又是新概念?
OK,全体的思路有了,咱们再具体看看具体是怎么完成的。依照之前的分析,咱们需求在每次烘托页面的时候读取当时主题的值,所以,首先得先获取当时的主题值。我这儿是运用 MMKV存储当时主题值,主题值是 String类型,如下 code 10 所示:
// code 10
//获取选中的主题 id
val chosenThemeId = remember {
mutableStateOf(
MMKV.defaultMMKV().getString(MMKVConstant.ChosenThemeCode, ThemeKinds.DEFAULT.name)
?: ThemeKinds.DEFAULT.name
)
}
enum class ThemeKinds {
DEFAULT, //默许主题
RED, //赤色主题
YELLOW, //黄色主题
BLUE //蓝色主题
}
然后自定义主题,在这儿需求规定主题用到的色值、案牍款式、形状款式等。在每次切换主题后,在这儿还需求依据传入的当时主题值,设置相应的色值组等等。具体如下代码:
// code 11
@Composable
fun CustomTheme(
chosenThemeId: MutableState<String>,
content: @Composable () -> Unit
) {
//自定义主题色值
val colors = when (chosenThemeId.value) {
ThemeKinds.DEFAULT.name -> {
LightColors
}
ThemeKinds.RED.name -> {
RedThemeColors
}
ThemeKinds.YELLOW.name -> {
YellowThemeColors
}
ThemeKinds.BLUE.name -> {
BlueThemeColors
}
else -> {
DarkColors
}
}
MaterialTheme(
colors = colors,
typography = typography,
shapes = shapes
) {
content()
}
}
//赤色主题色值
private val RedThemeColors = lightColors(
primary = Color(0xFFFF4040),
background = Color(0x66FF4040)
)
//黄色主题色值
private val YellowThemeColors = lightColors(
primary = Color(0xFFDAA520),
background = Color(0x66FFD700)
)
//蓝色主题色值
private val BlueThemeColors = lightColors(
primary = Color(0xFF436EEE),
background = Color(0x6600FFFF)
)
private val DarkColors = darkColors(
primary = Color.White,
primaryVariant = Red700,
onPrimary = Color.Black,
secondary = Red300,
onSecondary = Color.Black,
error = Red200
)
private val LightColors = lightColors(
primary = Color.Black,
primaryVariant = Red900,
onPrimary = Color.White,
secondary = Red700,
secondaryVariant = Red900,
onSecondary = Color.White,
error = Red800,
)
能够看到,在咱们自定义的主题 CustomTheme最终,仍是运用的 MaterialTheme,只不过将官方的 MaterialTheme中 colors设置成了咱们自己的 colors,同理,咱们还能够设置案牍 typography和 形状 shapes等参数。
其实,所谓的色值组便是一个 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. 彩蛋 —— 切换主题进阶版
这就完了么?作为主题切换功用来讲,已经完成完了,但,刚刚的切换过程是不是感觉比较僵硬?有没有愈加丝滑的做法?答案当然是有的。

要想完成丝滑的作用,先得认识一位新的朋友: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中的 primaryColorState 值,进而重组一切引用了 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:///post/7070671629713408031
- Android Jetpack Compose 完成主题切换(换肤);九狼 https:///post/7057418707357663246
- Jetpack Compose – animateXxxAsState;乐翁龙 https://blog.csdn.net/u010976213/article/details/114488661
我正在参与技能社区创作者签约方案招募活动,点击链接报名投稿。

