断更一时爽,一直断更一直爽~ 哈哈哈,就当给自己放了个长假吧。最近的行情太糟了,身边有同学已经被毕业,两个多月总算降薪找到下家 这儿呼吁咱们一定要存好六个月没有工作还能正常日子的银子,以备不时之需!期望疫情能早日平息,经济能够快速康复吧~
自己也没想到这个系列能够到第六篇,断更确实很久了,居然还收到了小伙伴的催更,感谢你们的不离不弃。闲话少说,咱们这非必须介绍的是 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
、large
3 种不同的特点值,其实都是 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. 彩蛋 —— 切换主题进阶版
这就完了么?作为主题切换功用来讲,已经完成完了,但,刚刚的切换过程是不是感觉比较僵硬?有没有愈加丝滑的做法?答案当然是有的。 如图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:///post/7070671629713408031
- Android Jetpack Compose 完成主题切换(换肤);九狼 https:///post/7057418707357663246
- Jetpack Compose – animateXxxAsState;乐翁龙 https://blog.csdn.net/u010976213/article/details/114488661
我正在参与技能社区创作者签约方案招募活动,点击链接报名投稿。