断更一时爽,一直断更一直爽~ 哈哈哈,就当给自己放了个长假吧。最近的行情太糟了,身边有同学已经被毕业,两个多月总算降薪找到下家 这儿呼吁咱们一定要存好六个月没有工作还能正常日子的银子,以备不时之需!期望疫情能早日平息,经济能够快速康复吧~

自己也没想到这个系列能够到第六篇,断更确实很久了,居然还收到了小伙伴的催更,感谢你们的不离不弃。闲话少说,咱们这非必须介绍的是 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 中 Surfacecolor特点就默许设置了 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.mediumalpha值的巨细排序为: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)

Jetpack-Compose 学习笔记(六)——Compose 主题 Theme 一探究竟,换肤还能如此 Easy & Silky?
SpanStyle是设置案牍的款式的,作用于字符单位;而假如要针对案牍的行高、对齐方式等进行设置,则需求运用ParagraphStyle,望文生义它是针对阶段款式的。

3. 形状款式

MaterialTheme主题中也有 Shape形状特点,在许多的官方 Composable 组件中都有这个 Shape特点,比方 Button组件的 Shape特点默许值便是 MaterialTheme.shapes.small

// code 8
fun Button(
    
    shape: Shape = MaterialTheme.shapes.small,
    
) {
}

Shapes.kt提供了 smallmediumlarge3 种不同的特点值,其实都是 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 所示:

Jetpack-Compose 学习笔记(六)——Compose 主题 Theme 一探究竟,换肤还能如此 Easy & Silky?

其间包含了色值、字体、形状的切换,用到的思路和原理都是相同的,所以这儿就只拿主题色值的切换来说明。想要完成这一功用,首先需求理解的是,点击事情之后切换主题的回调该怎么做?

总不能给一切设置色值的当地都设置一个监听器吧?那样做想想都觉得“酸爽”。其实,在 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,只不过将官方的 MaterialThemecolors设置成了咱们自己的 colors,同理,咱们还能够设置案牍 typography和 形状 shapes等参数。

其实,所谓的色值组便是一个 Colors目标,Compose 中默许就有 lightColorsdarkColors两种 Colors目标,别离用于暗夜形式和白天形式的主题色值的设置,咱们这儿一致是以白天形式的 lightColors目标为基准来进行其他主题色值的设置,作为例子这儿就重写了 primarybackground两个特点,别离用来设置案牍色值和背景色的色值。

定义好自定义主题中的各个色值组后,别忘了最终仍是要设置到 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. 彩蛋 —— 切换主题进阶版

这就完了么?作为主题切换功用来讲,已经完成完了,但,刚刚的切换过程是不是感觉比较僵硬?有没有愈加丝滑的做法?答案当然是有的。

Jetpack-Compose 学习笔记(六)——Compose 主题 Theme 一探究竟,换肤还能如此 Easy & Silky?
如图3 所示,每次切换时,背景色和字体巨细、圆角巨细都是突变的,切换过程丝滑,过渡自然。

要想完成丝滑的作用,先得认识一位新的朋友:animateXxxAsState。

5.1 animateXxxAsState

看前缀就知道是为动画而生的,Xxx 是因为它有许多重载的参数办法,比方 Color、Dp、Float 等,咱们这儿色值的突变便是用到的 animateColorAsState办法。同样地,案牍字体巨细的动画以及圆角的动画,别离运用的是 animateFloatAsStateanimateDpAsState办法。

这一类办法十分好用,官方文档上是这么介绍 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换肤

赞人玫瑰,手留余香!欢迎点赞、转发~ 转发请注明出处~

更多内容,欢迎关注大众号:修之竹

参考文献

  1. Compose主题切换——让你的APP也能一键换肤;Zhujiang https:///post/7070671629713408031
  2. Android Jetpack Compose 完成主题切换(换肤);九狼 https:///post/7057418707357663246
  3. Jetpack Compose – animateXxxAsState;乐翁龙 https://blog.csdn.net/u010976213/article/details/114488661

我正在参与技能社区创作者签约方案招募活动,点击链接报名投稿。