Compose 中的主题内的代码大量定义了 CompositionLocal,因此在学习主题之前,先学习 CompositionLocal 铺垫好基础。
1、CompositionLocal
1.1 显式传参与隐式传参
显式传参需要依赖函数的调用,层层传递;隐式传参通过创建全局变量可直接被需要该参数的函数使用。
下面我们通过示例代码对比二者的优劣,先看显式传参:
kotlin
class ExplicitTest {
private fun Layout() {
val color: String = "黑色"
// 假如 Layout 内按照如下顺序摆放组件
Text(color)
Grid(color)
Text(color)
Text(color)
}
private fun Grid(color: String) {
println("other components in Grid")
Text(color)
}
private fun Text(color: String) {
println("Text:$color")
}
@Test
fun test_explicit() {
Layout()
}
}
看 Grid 实际上本身不需要 color 参数,之所以声明它,是为了传递给 Grid 内部的 Text。因此我们能看到显式传参的劣势是需要层层传递参数,比较繁琐。
再来看隐式传参,通过将变量声明为全局变量,函数可直接拿来使用,不用声明参数:
kotlin
class ImplicitTest {
var color: String = "黑色"
private fun Layout() {
// 假如 Layout 内按照如下顺序摆放组件
Text()
Grid()
Text()
Text()
}
private fun Grid() {
println("other components in Grid")
Text()
}
private fun Text() {
println("Text:$color")
}
@Test
fun test_implicit() {
Layout()
}
}
运行结果:
Text:黑色
other components in Grid
Text:黑色
Text:黑色
Text:黑色
假如此时想让 Grid 内的 color 是红色,其余组件不变,你会发现不好处理,因为假如在 Grid 内将 color 修改为"红色"后,其他组件的 color 会受到影响:
kotlin
private fun Grid() {
println("other components in Grid")
color = "红色"
Text()
}
运行结果:
Text:黑色
other components in Grid
Text:红色
Text:红色
Text:红色
因此我们需要一个函数,在 Grid 执行前修改 color 值,在 Grid 执行之后恢复为原来的值:
kotlin
private fun provider(value: String, content: () -> Unit) {
color = value
content()
color = "黑色"
}
要执行 Grid 时,将其传入 provider 内:
kotlin
private fun Layout() {
// 假如 Layout 内按照如下顺序摆放组件
Text()
provider("红色") {
Grid()
}
Text()
Text()
}
这种类似面向切面变成的 AOP 处理,可以优雅的实现隐式传参,运行结果如下:
Text:黑色
other components in Grid
Text:红色
Text:黑色
Text:黑色
可以看到既修改了参数值,又没有影响到其他函数。
看完两个例子可以比较一下两种传参方式:
- 显式传参:优势是数据隔离,劣势是使用繁琐
- 隐式传参:优势是使用方便,劣势是一改全改
1.2 CompositionLocal
将传参方式结合到 Jetpack Compose 的主题,来想像下面这样传递主题是否合适:
答案当然是不合适,主题的颜色值需要经过层层传递才能到比较深层次的组件中,这样很麻烦,所以才引入 CompositionLocal 来解决这个问题:
- 通常情况下,在 Compose 中,数据以参数形式向下流经整个界面树传递给每个可组合函数。但是,对于广泛使用的常用数据(如颜色或类型样式),这可能会很麻烦
- 为了支持无需将颜色作为显式参数依赖项传递给大多数可组合项,Compose 提供了 CompositionLocal,允许创建以树为作用域的具名对象,这可以用作让数据流经界面树的一种隐式方式
- 我们可以通过 MaterialTheme 的 colors、shapes 和 typography 属性访问 LocalColors、LocalShapes 和 LocalTypography 属性
单例的 MaterialTheme 提供了 colors、shapes 和 typography 属性供组件访问,而所有组件的父可组合函数 MaterialTheme 则可以设置单例的 MaterialTheme 的属性。
比如说在设置文字颜色时指定 MaterialTheme 中 colors 属性中的颜色:
kotlin
@Composable
fun CompositionLocalSample1() {
CustomTextLabel(labelText = "Some composable deep in the hierarchy of MaterialTheme")
}
@Composable
fun CustomTextLabel(labelText: String) {
Text(
text = labelText,
color = MaterialTheme.colors.primary
)
}
看一眼源码,colors、typography 和 shapes 是单例类 MaterialTheme 的只读属性:
kotlin
object MaterialTheme {
val colors: Colors
@Composable
@ReadOnlyComposable
get() = LocalColors.current
val typography: Typography
@Composable
@ReadOnlyComposable
get() = LocalTypography.current
val shapes: Shapes
@Composable
@ReadOnlyComposable
get() = LocalShapes.current
}
以颜色为例,LocalColors 是一个 ProvidableCompositionLocal<Colors>
,提供的是一个有众多默认值的颜色属性的 Colors 对象:
kotlin
// Colors.kt:
internal val LocalColors = staticCompositionLocalOf { lightColors() }
fun lightColors(
primary: Color = Color(0xFF6200EE),
primaryVariant: Color = Color(0xFF3700B3),
secondary: Color = Color(0xFF03DAC6),
secondaryVariant: Color = Color(0xFF018786),
background: Color = Color.White,
surface: Color = Color.White,
error: Color = Color(0xFFB00020),
// onXXX 是 XXX 的反色
onPrimary: Color = Color.White,
onSecondary: Color = Color.Black,
onBackground: Color = Color.Black,
onSurface: Color = Color.Black,
onError: Color = Color.White
): Colors = Colors(
primary,
primaryVariant,
secondary,
secondaryVariant,
background,
surface,
error,
onPrimary,
onSecondary,
onBackground,
onSurface,
onError,
true
)
@Stable
class Colors(
primary: Color,
primaryVariant: Color,
secondary: Color,
secondaryVariant: Color,
background: Color,
surface: Color,
error: Color,
onPrimary: Color,
onSecondary: Color,
onBackground: Color,
onSurface: Color,
onError: Color,
isLight: Boolean
) {
var primary by mutableStateOf(primary, structuralEqualityPolicy())
internal set
var primaryVariant by mutableStateOf(primaryVariant, structuralEqualityPolicy())
internal set
var secondary by mutableStateOf(secondary, structuralEqualityPolicy())
internal set
var secondaryVariant by mutableStateOf(secondaryVariant, structuralEqualityPolicy())
internal set
var background by mutableStateOf(background, structuralEqualityPolicy())
internal set
var surface by mutableStateOf(surface, structuralEqualityPolicy())
internal set
var error by mutableStateOf(error, structuralEqualityPolicy())
internal set
var onPrimary by mutableStateOf(onPrimary, structuralEqualityPolicy())
internal set
var onSecondary by mutableStateOf(onSecondary, structuralEqualityPolicy())
internal set
var onBackground by mutableStateOf(onBackground, structuralEqualityPolicy())
internal set
var onSurface by mutableStateOf(onSurface, structuralEqualityPolicy())
internal set
var onError by mutableStateOf(onError, structuralEqualityPolicy())
internal set
var isLight by mutableStateOf(isLight, structuralEqualityPolicy())
internal set
}
上面使用的是默认的颜色,也可以指定主题,使用主题中定义的颜色:
kotlin
@Composable
fun CompositionLocalSample1() {
MaterialTheme {
CustomTextLabel(labelText = "Some composable deep in the hierarchy of MaterialTheme")
}
}
这个 MaterialTheme 主题实际上是一个可组合函数,该函数使用 CompositionLocalProvider 定义了很多 CompositionLocal:
kotlin
@Composable
fun MaterialTheme(
colors: Colors = MaterialTheme.colors,
typography: Typography = MaterialTheme.typography,
shapes: Shapes = MaterialTheme.shapes,
content: @Composable () -> Unit
) {
val rememberedColors = remember {
colors.copy()
}.apply { updateColorsFrom(colors) }
val rippleIndication = rememberRipple()
val selectionColors = rememberTextSelectionColors(rememberedColors)
CompositionLocalProvider(
// LocalColors 就是单例的 MaterialTheme 中使用的 LocalColors
LocalColors provides rememberedColors,
LocalContentAlpha provides ContentAlpha.high,
LocalIndication provides rippleIndication,
LocalRippleTheme provides MaterialRippleTheme,
LocalShapes provides shapes,
LocalTextSelectionColors provides selectionColors,
LocalTypography provides typography
) {
ProvideTextStyle(value = typography.body1) {
PlatformMaterialTheme(content)
}
}
}
除了 MaterialTheme,在项目创建时,也会创建一个项目的主题,比如项目名称是 JetpackCompose,那么在 Theme.kt 中就会创建一个主题名为 JetpackComposeTheme:
kotlin
@Composable
fun JetpackComposeTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
window.statusBarColor = colorScheme.primary.toArgb()
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
}
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
当然,创建的具体内容会随着使用的库的版本不同而变化。
1.3 CompositionLocalProvider 与 current
想要实现文字透明度渐弱的效果:
需要修改 Text 的文字透明度。默认情况下 Text 的文字颜色是通过 LocalContentColor 指定 alpha:
kotlin
@Composable
fun Text(
text: String,
modifier: Modifier = Modifier,
color: Color = Color.Unspecified,
fontSize: TextUnit = TextUnit.Unspecified,
fontStyle: FontStyle? = null,
fontWeight: FontWeight? = null,
fontFamily: FontFamily? = null,
letterSpacing: TextUnit = TextUnit.Unspecified,
textDecoration: TextDecoration? = null,
textAlign: TextAlign? = null,
lineHeight: TextUnit = TextUnit.Unspecified,
overflow: TextOverflow = TextOverflow.Clip,
softWrap: Boolean = true,
maxLines: Int = Int.MAX_VALUE,
onTextLayout: (TextLayoutResult) -> Unit = {},
style: TextStyle = LocalTextStyle.current
) {
val textColor = color.takeOrElse {
style.color.takeOrElse {
LocalContentColor.current.copy(alpha = LocalContentAlpha.current)
}
}
...
}
其中 LocalContentColor 就是一个 ProvidableCompositionLocal:
kotlin
val LocalContentColor = compositionLocalOf { Color.Black }
而透明度 LocalContentAlpha 是 1.0:
kotlin
val LocalContentAlpha = compositionLocalOf { 1f }
这样我们可以通过 CompositionLocalProvider 提供 LocalContentAlpha,对后者进行修改以实现修改文字透明度的需求:
kotlin
@Composable
fun CompositionLocalSample2() {
MaterialTheme {
Column {
// 使用默认透明度
Text(text = "Uses MaterialTheme's provided alpha")
// 使用中等透明度
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
Text(text = "Medium value provided for LocalContentAlpha")
Text(text = "This Text also uses the medium value")
}
// 使用低等透明度
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.disabled) {
DescendantExample()
}
}
}
}
解释一下,CompositionLocalProvider 也是一个可组合项,两个参数分别是可变的 ProvidedValue 以及表示可组合项内容的插槽 API:
kotlin
@Composable
@OptIn(InternalComposeApi::class)
fun CompositionLocalProvider(vararg values: ProvidedValue<*>, content: @Composable () -> Unit) {
currentComposer.startProviders(values)
content()
currentComposer.endProviders()
}
你可以看到函数内容与 1.1 节举例的 provider 函数非常相似,都是在内容函数 content 执行之前,先修改参数的值,在 content 执行之后再将参数值还原。
参数上的 ProvidedValue 由 ProvidableCompositionLocal 的中缀函数 provides 提供:
kotlin
@Stable
abstract class ProvidableCompositionLocal<T> internal constructor(defaultFactory: () -> T) :
CompositionLocal<T> (defaultFactory) {
@Suppress("UNCHECKED_CAST")
infix fun provides(value: T) = ProvidedValue(this, value, true)
@Suppress("UNCHECKED_CAST")
infix fun providesDefault(value: T) = ProvidedValue(this, value, false)
}
因此,可以总结:
- 如需为 CompositionLocal 提供新值,请使用 CompositionLocalProvider 以及 provides 中缀函数
- CompositionLocal 的 current 值对应于该组合部分中的某个祖先提供的最接近的值
第二点实际上说的就是最终生效的属性值是最后一次对该属性赋值的值(即后赋值会覆盖前面赋过的值)。以 CompositionLocalSample2() 的示例代码为例:
-
第一个 Text 最近的祖先是 Column,Column 没有修改透明度,因此再向上找 MaterialTheme,内部修改了 LocalContentAlpha 为 ContentAlpha.high:
kotlin@Composable fun MaterialTheme( colors: Colors = MaterialTheme.colors, typography: Typography = MaterialTheme.typography, shapes: Shapes = MaterialTheme.shapes, content: @Composable () -> Unit ) { val rememberedColors = remember { colors.copy() }.apply { updateColorsFrom(colors) } val rippleIndication = rememberRipple() val selectionColors = rememberTextSelectionColors(rememberedColors) CompositionLocalProvider( LocalColors provides rememberedColors, // LocalContentAlpha 修改为 ContentAlpha.high LocalContentAlpha provides ContentAlpha.high, LocalIndication provides rippleIndication, LocalRippleTheme provides MaterialRippleTheme, LocalShapes provides shapes, LocalTextSelectionColors provides selectionColors, LocalTypography provides typography ) { ProvideTextStyle(value = typography.body1) { PlatformMaterialTheme(content) } } }
-
至于第二到第四个 Text 的最近付父祖先,显而易见是 CompositionLocalProvider,这些 Text 也就使用相应的 LocalContentAlpha 了
再举一个 current 的例子,通过 LocalContext.current 获取 Context 进而获取到 Resources 对象:
kotlin
@Composable
fun FruitText(fruitSize: Int) {
// 获取 Resources 对象
val resources = LocalContext.current.resources
// 通过 Resources 对象获取复数字符串
val fruitText = resources.getQuantityString(R.plurals.fruit_title, fruitSize, fruitSize)
Text(text = fruitText)
}
fruit_title 是一个复数形式的字符串资源:
xml
<resources>
<plurals name="fruit_title">
<item quantity="one" translatable="false">"%d fruit"</item>
<item quantity="other" translatable="false">%d fruits</item>
</plurals>
</resources>
1.4 创建 CompositionLocal
创建两个 Card 布局,效果如下:
通过自定义的 CompositionLocal 来实现该效果:
kotlin
// 阴影的数据
data class Elevations(val card: Dp = 0.dp)
// 创建一个 CompositionLocal
val LocalElevations = compositionLocalOf { Elevations() }
// 定义两个阴影类的只读实例
object CardElevation {
val high: Elevations
get() = Elevations(10.dp)
val low: Elevations
get() = Elevations(5.dp)
}
@Composable
fun MyCard(
// 阴影默认值取 LocalElevations 的默认值 0dp
elevation: Dp = LocalElevations.current.card,
backgroundColor: Color,
content: @Composable () -> Unit
) {
Card(
elevation = elevation,
backgroundColor = backgroundColor,
content = content,
modifier = Modifier.size(200.dp)
)
}
实现预览效果时,上面的 Card 通过 CompositionLocal 指定阴影值为 CardElevation.high,下面的则使用默认值:
kotlin
@Composable
fun CompositionLocalSample3() {
Column {
CompositionLocalProvider(LocalElevations provides CardElevation.high) {
MyCard(backgroundColor = MaterialTheme.colors.onSurface.copy(alpha = 0.05f)) {
}
}
MyCard(backgroundColor = MaterialTheme.colors.onSurface.copy(alpha = 0.05f)) {
}
}
}
1.5 staticCompositionLocalOf
与 compositionLocalOf 不同,更改值会导致提供 CompositionLocal 的整个 content lambda 被重组,而不仅仅是在组合中读取 current 值的组件。
如果为 CompositionLocal 提供的值发生更改的可能性微乎其微或永远不变,使用 staticCompositionLocalOf 可提高性能。因为它不用像 compositionLocalOf 那样去追踪哪些组件用了 current 值,追踪会耗费性能。
来看示例代码:
kotlin
var isStatic = false
var compositionLocalName = ""
// CompositionLocal properties should be prefixed with Local
val LocalColor = if (isStatic) {
compositionLocalName = "StaticCompositionLocal 场景"
staticCompositionLocalOf { Color.Black }
} else {
compositionLocalName = "DynamicCompositionLocal 场景"
compositionLocalOf { Color.Black }
}
// 重组标记,组件初次加载时为 Init,重组(第二次加载)之前将其设置为 Recompose
var recomposeFlag = "Init"
@Composable
fun CompositionLocalSample4() {
val (color, setColor) = remember { mutableStateOf(Color.Green) }
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(text = compositionLocalName)
Spacer(modifier = Modifier.height(20.dp))
// 3 个 TaggedBox 组件,只有中间的使用了 CompositionLocal
CompositionLocalProvider(LocalColor provides color) {
TaggedBox("Wrapper $recomposeFlag", 400.dp, Color.Red) {
TaggedBox("Middle $recomposeFlag", 300.dp, LocalColor.current) {
TaggedBox("Inner $recomposeFlag", 200.dp, Color.Yellow)
}
}
}
Spacer(modifier = Modifier.height(20.dp))
// 点击按钮改变状态,将颜色设置为蓝色观察 3 个 TaggedBox 是否重组
Button(
onClick = {
setColor(Color.Blue)
recomposeFlag = "Recompose"
}
) {
Text(text = "Change Theme")
}
}
}
}
@Composable
fun TaggedBox(tag: String, size: Dp, background: Color, content: @Composable () -> Unit = {}) {
Column(
modifier = Modifier
.size(size)
.background(background),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = tag)
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
content()
}
}
}
当 isStatic 为 false,演示动态的 CompositionLocal 时,由于只对使用了 CompositionLocal 的 current 值的组件进行重组,所以只有中间的 TaggedBox 改变了颜色和标签:
当 isStatic 为 true,演示静态的 CompositionLocal 时,由于不会追踪某个使用 CompositionLocal 的 current 值的组件,而是对 CompositionLocalProvider 的 content lambda 内的所有组件进行重组,因此 3 个 TaggedBox 的标签都会发生变化,且中间的 TaggedBox 因为使用 LocalColor.current 还会变色:
在 Material Design3 的主题中,应该是不用 CompositionLocal 了,相关代码已经被移除了。
2、主题
2.1 什么是 Material Design
Jetpack Compose 的主题是基于 Material Design 的,需要先了解一下。
使用 Material Theme 就可以获取到 Material Design 的设计元素,如想使用其他设计风格需要自己定义主题。
Material Design 定义的内容:
- 颜色:定义了许多语义命名的颜色,可在整个应用程序中使用。如 Color Scheme、Primary、Secondary 和 On Primary
- 排版:定义了许多语义命名的类型样式,H1 ~ H6,Subtitle1、Subtitle2、Body1、Body2、BUTTON、Caption、OVERLINE
- 形状:定义了 3 个类别:小型、中型和大型组件;每个都可定义要使用的形状,自定义角样式(切割或圆角)和大小
2.2 定义主题
定义一个主题涉及到的维度:创建主题、颜色、排版、形状、深色主题。
课程是通过一个案例来讲解,这个案例我猜测是 Google 官方给出的某个 Codelab,由于它会引入一些功能实现的代码,如果网络请求之类的,与主题关系不大,因此我们就不敲完整的案例代码了,只写与主题定义相关的部分。
在创建 Compose 项目时,如果选择创建 Activity,那么 Android Studio 会在 ui.theme 包下生成 Color、Theme 和 Type 三个 kt 源文件用于定义主题内容,需要对这三个文件的内容进行添加与修改。
先是在 Color.kt 中定义主题所需的颜色:
kotlin
val Red200 = Color(0xfff297a2)
val Red300 = Color(0xffea6d7e)
val Red700 = Color(0xffdd0d3c)
val Red800 = Color(0xffd00036)
val Red900 = Color(0xffc20029)
然后在 Type.kt 中定义字体以及排版对象:
kotlin
private val Montserrat = FontFamily(
Font(R.font.montserrat_regular),
Font(R.font.montserrat_medium, FontWeight.W500),
Font(R.font.montserrat_semibold, FontWeight.W600)
)
private val Domine = FontFamily(
Font(R.font.domine_regular),
Font(R.font.domine_bold, FontWeight.Bold)
)
val JetNewsTypography = Typography(
h4 = TextStyle(
fontFamily = Montserrat,
fontWeight = FontWeight.W600,
fontSize = 30.sp
),
h5 = TextStyle(
fontFamily = Montserrat,
fontWeight = FontWeight.W600,
fontSize = 24.sp
),
h6 = TextStyle(
fontFamily = Montserrat,
fontWeight = FontWeight.W600,
fontSize = 20.sp
),
subtitle1 = TextStyle(
fontFamily = Montserrat,
fontWeight = FontWeight.W600,
fontSize = 16.sp
),
subtitle2 = TextStyle(
fontFamily = Montserrat,
fontWeight = FontWeight.W500,
fontSize = 14.sp
),
body1 = TextStyle(
fontFamily = Domine,
fontWeight = FontWeight.Normal,
fontSize = 16.sp
),
body2 = TextStyle(
fontFamily = Montserrat,
fontSize = 14.sp
),
button = TextStyle(
fontFamily = Montserrat,
fontWeight = FontWeight.W500,
fontSize = 14.sp
),
caption = TextStyle(
fontFamily = Montserrat,
fontWeight = FontWeight.Normal,
fontSize = 12.sp
),
overline = TextStyle(
fontFamily = Montserrat,
fontWeight = FontWeight.W500,
fontSize = 12.sp
)
)
再然后新增定义形状的 Shape.kt:
kotlin
val JetNewsShapes = Shapes(
small = CutCornerShape(topStart = 8.dp),
medium = CutCornerShape(topStart = 24.dp),
large = RoundedCornerShape(8.dp)
)
最后在定义主题的 Theme.kt 中使用上述内容:
kotlin
private val LightColors = lightColors(
primary = Red700,
primaryVariant = Red900,
onPrimary = Color.White,
secondary = Red700,
secondaryVariant = Red900,
onSecondary = Color.White,
error = Red800
)
private val DarkColors = darkColors(
primary = Red300,
primaryVariant = Red700,
onPrimary = Color.Black,
secondary = Red300,
onSecondary = Color.Black,
error = Red200
)
@Composable
fun JetNewsTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
MaterialTheme(
colors = if (darkTheme) DarkColors else LightColors,
typography = JetNewsTypography,
shapes = JetNewsShapes,
content = content
)
}
将需要使用该主题的可组合项置于 JetNewsTheme 的 content 参数下即可:
kotlin
@Composable
fun Home() {
JetNewsTheme {
Scaffold {
...
}
}
}
2.3 使用颜色
上面我们创建了自定义的主题,为应用设置颜色、字体样式和形状,所有 Material 组件开箱即可使用这些自定义功能。例如 FloatingActionButton 可组合项中默认使用主题的 secondary 颜色:
kotlin
fun FloatingActionButton(
...
backgroundColor: Color = MaterialTheme.colors.secondary,
...
)
但是,有时我们并不想使用默认设置,你可以通过定义一个 Color 对象指定想要使用的颜色,但是要注意不要使用如下这样的静态声明(即硬编码),因为它会导致更难或者干脆无法支持不同的主题(例如,浅色/深色主题):
kotlin
Surface(color = Color.LightGray) {
Text(
text = "Hard coded colors don't respond to theme changes.",
color = Color(0xffff00ff)
)
}
一种更灵活的方法是从主题中检索颜色:
kotlin
Surface(color = MaterialTheme.colors.primary)
由于主题中的每种颜色都是 Color 实例,可以使用 copy 派生出新的颜色:
kotlin
// 只修改透明度,RGB 不变
val derivedColor = MaterialTheme.colors.onSurface.copy(alpha = 0.1f)
许多组件都接收一对颜色,分别是"颜色"和"内容颜色":
kotlin
@Composable
fun Surface(
...
// Surface 本身的颜色
color: Color = MaterialTheme.colors.surface,
// Surface 内容,即在 Surface 内进行显示的组件颜色
contentColor: Color = contentColorFor(color),
...
)
类似的还有 TopAppBar:
kotlin
@Composable
fun TopAppBar(
...
// TopAppBar 本身的颜色
backgroundColor: Color = MaterialTheme.colors.primarySurface,
// TopAppBar 的内容颜色,根据 backgroundColor 提供一个适合的默认颜色
contentColor: Color = contentColorFor(backgroundColor),
...
)
你可以注意到,TopAppBar 的 contentColor 的默认值是根据 backgroundColor 计算出来的。这是 Compose 在颜色设置方面的一个特点,就是不仅可以设置可组合项的颜色,还能为其"内容"(即包含在其中的可组合项)提供默认颜色,例如 Text 颜色或 Icon 色调。contentColorFor 可以为任何主题颜色检索适当的 "on" 颜色,例如设置 primary 背景,它就会返回 onPrimary 作为内容颜色。如果设置非主题背景颜色,则应自行提供合理的内容颜色:
kotlin
Surface(color = MaterialTheme.colors.primary) {
Text(...) // 默认的字体颜色为 onPrimary
}
Surface(color = MaterialTheme.colors.error) {
Icon(...) // 默认的图标颜色为 onError
}
教程给的 Demo 中,它的 Header 始终具有 Color.LightGray 背景,在浅色主题中看起来没有问题,但是在深色主题中,就会与背景形成高度对比。它们并不指定特定的文本颜色,因此会继承不能与背景形成对比的当前内容颜色:
kotlin
@Composable
fun Header(
text: String,
modifier: Modifier = Modifier
) {
Text(
text = text,
modifier = modifier
.fillMaxWidth()
// 没有使用主题颜色,而是固定使用浅灰色
.background(Color.LightGray)
.padding(vertical = 8.dp, horizontal = 16.dp)
)
}
说白了就是 Text 的背景色写死了,导致默认的文字颜色也是固定的内容颜色,没办法随着主题变化而变化。
解决办法是移除用于硬编码颜色的 background 修饰符,改为将 Text 封装在包含主题派生颜色的 Surface 中,并指定相应内容采用 primary 颜色:
kotlin
@Composable
fun Header(
text: String,
modifier: Modifier = Modifier
) {
Surface(
// 亮色主题下,onSurface 是黑色,透明度设为 0.1 后是灰色;
// 在深色主题下 onSurface 是白色,透明度设为 0.1 像一个透明薄膜
color = MaterialTheme.colors.onSurface.copy(alpha = 0.1f),
contentColor = MaterialTheme.colors.primary,
) {
Text(
text = text,
modifier = modifier
.fillMaxWidth()
.padding(vertical = 8.dp, horizontal = 16.dp)
)
}
}
内容 Alpha 值:
primarySurface:
kotlin
@Composable
fun TopAppBar(
// backgroundColor: Color = MaterialTheme.colors.primary
backgroundColor: Color = MaterialTheme.colors.primarySurface,
}
2.4 处理文本
使用 Text 显示文本,使用 TextField 和 OutlinedTextField 进行文本输入,并使用 TextStyle 对文本应用单一样式。
很多组件往往不会显示文本,而是提供 Slots API 让使用者传入 Text,比如 Button。这些组件会通过 ProvideTextStyle 可组合项(本身就使用了 CompositionLocal)设置主题的排版样式来设置当前的 TextStyle。如果未提供具体的 TextStyle 参数,Text 会默认查询此当前样式。
比如,我们看一下 Button 的源码:
kotlin
@Composable
fun Button(
...
content: @Composable RowScope.() -> Unit
) {
val contentColor by colors.contentColor(enabled)
Surface(
...
) {
CompositionLocalProvider(LocalContentAlpha provides contentColor.alpha) {
ProvideTextStyle(
value = MaterialTheme.typography.button
) {
Row(
Modifier
.defaultMinSize(
minWidth = ButtonDefaults.MinWidth,
minHeight = ButtonDefaults.MinHeight
)
.padding(contentPadding),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
// 将 content 作为 Row 的 content 显示,通常是 Text
content = content
)
}
}
}
}
ProvideTextStyle 会将 content lambda 作为 CompositionLocalProvider 的 content 传入,并且提供 LocalTextStyle 的新值:
kotlin
@Composable
fun ProvideTextStyle(value: TextStyle, content: @Composable () -> Unit) {
val mergedStyle = LocalTextStyle.current.merge(value)
CompositionLocalProvider(LocalTextStyle provides mergedStyle, content = content)
}
因此,Button 组件中的 Text 的 TextStyle 是由 ProvideTextStyle 可组合项提供的。
主题文本样式:
主题样式可以覆盖,比如 Text 的 style 指定了 subtitle2 这个排版样式内部已经定义了 fontSize 属性,但是在 style 之后还可以额外指定 fontSize 覆盖 subtitle2 内的定义。
多种样式:
如上,第一段文字使用默认样式,第二段文字添加了红色背景,第三段文字的大小变为 24sp。直接将 text 赋值给 Text 的 text 属性即可,效果如下:
2.5 处理形状
比如要切图片的左上角:
kotlin
@Composable
fun ShapeTest() {
JetNewsTheme {
Image(
painter = painterResource(id = R.drawable.ic_launcher_background),
contentDescription = null,
modifier = Modifier.clip(shape = MaterialTheme.shapes.small)
)
}
}
clip 指定了 shape 是 MaterialTheme.shapes.small,该属性值在 JetNewsTheme 主题下被定义过:
kotlin
@Composable
fun JetNewsTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
MaterialTheme(
colors = if (darkTheme) DarkColors else LightColors,
typography = JetNewsTypography,
shapes = JetNewsShapes,
content = content
)
}
shapes 使用的是 JetNewsShapes,small 指定为 CutCornerShape(topStart = 8.dp):
kotlin
val JetNewsShapes = Shapes(
small = CutCornerShape(topStart = 8.dp),
medium = CutCornerShape(topStart = 24.dp),
large = RoundedCornerShape(8.dp)
)
Material Design 这部分的内容都是细小知识点,知识点多,细节也多,需要注意一下。