主题
主要学习内容
- Material Design 入门指南以及如何针对您的品牌对其进行自定义
- Compose 如何实现 Material Design 系统
- 如何在应用中定义和使用颜色、排版和形状
- 如何设置组件的样式
- 如何支持浅色主题和深色主题
在本次学习中我们将设置新闻阅读应用的样式,从未设置样式的应用入手,应用所学的内容来设置应用的主题,并为设置深色主题提供支持
准备工作
因为之后的代码都是基于其中的项目进行的,所以还是推荐下载。同时也可以看一下Google人员对于的Compose的代码编写风格
因为代码过多且需要添加drawable资源文件,此处就不将代码写出来了
在解压文件中的ThemingCodelab
目录中存放本次学习的案例代码
此项目包含 3 个主要软件包:
-
com.codelab.theming.data
- 该软件包包含模型类和示例数据,无需修改该软件包 -
com.codelab.theming.ui.start
- 该 示例 的起点,您应该在该软件包中完成此 示例 中要求的所有更改 -
com.codelab.theming.ui.finish
- 该软件包是此 示例 的最终状态,供参考
我们可以选择Import Project
方式进行学习,也可以通过拷贝代码到自己项目中的方式
我使用的是拷贝代码的方式,可能之后跟
Import Project
方式有些区别请谅解如果是选择拷贝代码的方式请注意:
Empty Activity
默认Activity
继承androidx.appcompat.app.AppCompatActivity
,而要使用 Compose 则需要Activity
继承androidx.activity.ComponentActivity
,否则会产生如下异常Caused by: java.lang.IllegalStateException: You need to use a Theme.AppCompat theme (or descendant) with this activity.
Material主题设置
Jetpack Compose 提供了 Material Design 的实现,Material Design 是一个用于创建数字化界面的综合设计体系。Material Design 组件(按钮、卡片、开关等)在Material Theme 设置的基础上构建而成,一个 Material Theme由颜色、排版和形状属性组成
颜色
Material Theme 定义了一些从语义上命名的颜色,我们可以在应用中使用:
其中 primary
是应用的主要颜色,secondary
用于提供强调色,我们可以通过这两种颜色的设置凸显出需要对比区域
background
和surface
两种颜色用于在概念上驻留在"应用表面"的组件的容器,也就是背景颜色
Material Design 中还定义了on
颜色,是与具名颜色产生明显对比的颜色
例如:采用
surface
作为背景颜色的容器中文本应该采用onSurface
颜色
Material 组件已配置并使用这些主题颜色。例如,FloatingActionButton
的默认颜色为 secondary
,Card
的默认颜色为 surface
,诸如此类。
排版
同样,Material Theme还定义了一些从语义上命名的字体样式:
虽然我们可能不会按主题来更改字体样式,但使用 Material Design 中的字体样式可提升应用内部的一致性
Material 组件已配置并使字体样式。例如,TopAppBar
默认使用 h6 样式,Button
默认使用 button
样式,诸如此类。
形状
Material Theme 中定义了 3 个类别:小型、中型和大型组件;每种组件都可以定义要使用的形状,从而自定义角的样式(切角和圆角)和大小
默认情况下,Button
和TextField
使用小型形状主题,Card
和Dialog
使用中型形状主题,Sheet
使用大型形状主题
如需查看组件和形状主题的完整对应关系,请点击此处
基准
Material 默认采用“基准”主题,即紫色的配色方案、Roboto 字体比例,以及以上图片所示的略呈圆形的形状。如果您未指定或自定义主题,组件就会使用基准主题
定义主题
MaterialTheme
在 Jetpack Compose 中实现主题设置的核心元素是 MaterialTheme
可组合项。如果将此可组合项放在 Compose 层次结构中,您就可以为其中的所有组件指定颜色、字体和形状的自定义设置
@Composable
fun MaterialTheme(
colors: Colors,
typography: Typography,
shapes: Shapes,
content: @Composable () -> Unit
) { ... }
我们可以使用
MaterialTheme
object
检索传递到此可组合项的参数,以公开colors
、typography
和shapes
属性。在之后,我们将逐一进行深入介绍
找到 Home
可组合函数 - 这是应用的主入口点。请注意,虽然我们声明了 MaterialTheme
,但并未指定任何参数,因此会获得默认的“基准”样式:
@Composable
fun Home() {
...
MaterialTheme {
Scaffold(...){...}
}
}
创建主题
如果需集中设置样式,官方建议创建自己的可组合项,用于封装和配置 MaterialTheme
这样做,我们就可以在指定自己的主题自定义设置,并在多个位置(例如跨多个屏幕或
@Preview
)轻松地重复使用这些自定义设置可以根据需要创建多个主题可组合项。例如,如果您想针对应用的不同部分支持不同的样式
我们可以在Theme.kt
中添加一个名为StudyTheme
新可组合函数
@Composable
fun StudyTheme(content: @Composable () -> Unit) {
MaterialTheme(content = content)
}
如果使用
Import Project
,在start
中没有Theme.kt
文件,需要新建Theme.kt
文件
我们回到 Home
可组合函数,并将 MaterialTheme
替换为 StudyTheme
:
@Composable
fun Home() {
...
StudyTheme {
Scaffold(...){...}
}
}
同样在
PostItemPreview
和FeaturedPostPreview
以使用新的StudyTheme
可组合项来封装其内容,以便预览使用新的主题@Preview("Post Item") @Composable private fun PostItemPreview() { val post = remember { PostRepo.getFeaturedPost() } StudyTheme{ Surface { PostItem(post = post) } } } @Preview("Featured Post") @Composable private fun FeaturedPostPreview() { val post = remember { PostRepo.getFeaturedPost() } StudyTheme{ FeaturedPost(post = post) } }
颜色
我们要在应用中实现的调色板如下所示:
Compose 中的颜色是使用 Color
类定义的。借助多个构造函数,您可以将颜色指定为 ULong
,也可以按单独的颜色通道来指定颜色
若要从用于指定颜色的常用“#dd0d3c”格式进行转换,请将“#”替换为“0xff”,即
Color(0xffdd0d3c)
,其中“ff”表示完整的 Alpha 值
在Color.kt
中添加以下颜色:
val Red700 = Color(0xffdd0d3c)
val Red800 = Color(0xffd00036)
val Red900 = Color(0xffc20029)
在定义颜色时,我们要根据颜色值“字面意义”命名颜色,而不要“从语义上”命名颜色
例如,命名为
Red500
而不是primary
。这样一来,我们就可以定义多个主题。例如,在深色主题中或样式设置不同的屏幕上,系统可能会将另一种颜色视为primary
注意导入 Compose 的 Color 类型是
androidx.compose.ui.graphics.Color
,而不是android.graphics.Color
如果使用
Import Project
,在start
中没有Color.kt
文件,需要新建Color.kt
文件
现在,我们已经定义了应用的颜色。接下来,我们将其合并到 MaterialTheme
所需的 Colors
对象中,从而将特定颜色分配到 Material 的具名颜色。切换回 Theme.kt
,然后添加以下代码:
//为外部属性,不是StudyTheme中的临时变量
private val LightColors = lightColors(
primary = Red700,
primaryVariant = Red900,
onPrimary = Color.White,
secondary = Red700,
secondaryVariant = Red900,
onSecondary = Color.White,
error = Red800
)
下面,我们要使用 lightColors
函数来构建 Colors
,这样即可提供合理的默认值,让我们不必将构成 Material 调色板的所有颜色全都指定出来
例如,我们尚未指定
background
颜色或许多“on”颜色,我们将会使用lightColors
中的默认值
我们在StudyTheme
中使用这些颜色:
@Composable
fun StudyTheme(content: @Composable () -> Unit) {
MaterialTheme(
content = content,
/*+*/ colors = LightColors
)
}
此时刷新预览,就会发现新的配色方案会反映在
TopAppBar
等组件中
排版
我们要在应用中实现的字体样式如下所示:
在 Compose 中,我们可以定义 TextStyle
对象,以定义设置一些文本的样式所需的信息。下面是其属性的示例:
@Immutable
class TextStyle(
val color: Color = Color.Unspecified,
val fontSize: TextUnit = TextUnit.Unspecified,
val fontWeight: FontWeight? = null,
val fontStyle: FontStyle? = null,
val fontSynthesis: FontSynthesis? = null,
val fontFamily: FontFamily? = null,
val fontFeatureSettings: String? = null,
val letterSpacing: TextUnit = TextUnit.Unspecified,
val baselineShift: BaselineShift? = null,
val textGeometricTransform: TextGeometricTransform? = null,
val localeList: LocaleList? = null,
val background: Color = Color.Unspecified,
val textDecoration: TextDecoration? = null,
val shadow: Shadow? = null,
val textAlign: TextAlign? = null,
val textDirection: TextDirection? = null,
val lineHeight: TextUnit = TextUnit.Unspecified,
val textIndent: TextIndent? = null
){ ... }
我们所需的字体比例要针对标题使用 Montserrat
,并针对正文文本使用 Domine
相关字体文件在 示例 的
res/font
文件夹中
在Type.kt
文件中定义 FontFamily
(结合了每个 Font
的不同粗细):
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)
)
然后创建一个 MaterialTheme
接受的 Typography
对象,为比例中的每个语义样式指定 TextStyle
:
val StudyTypography = 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
)
)
我们在StudyTheme
中使用新的 Typography
:
@Composable
fun StudyTheme(content: @Composable () -> Unit) {
MaterialTheme(
content = content,
colors = LightColors,
/*+*/ typography = StudyTypography
)
}
形状
Compose 提供了 RoundedCornerShape
类和 CutCornerShape
类,可用于定义形状主题
在 Shape.kt
,并添加以下代码:
val StudyShapes = Shapes(
small = CutCornerShape(topStart = 8.dp),
medium = CutCornerShape(topStart = 24.dp),
large = RoundedCornerShape(8.dp)
)
我们在StudyTheme
中使用新的 Shapes
:
@Composable
fun StudyTheme(content: @Composable () -> Unit) {
MaterialTheme(
content = content,
colors = LightColors,
typography = StudyTypography,
/*+*/ shapes = StudyShapes
)
}
刷新预览,可以看见精选博文的 Card
变为左上切角形状
深色主题
在应用中支持深色主题不仅有助于您的应用在用户设备上更好地集成(从 Android 10 开始,设备上已提供全局深色主题切换开关),还有助于降低能耗以及为满足无障碍功能需求提供支持。Material 提供了关于如何创建深色主题的接口
以下是我们想为深色主题实现的调色板:
打开 Color.kt
并添加以下颜色:
val Red200 = Color(0xfff297a2)
val Red300 = Color(0xffea6d7e)
打开 Theme.kt
并添加以下代码:
private val DarkColors = darkColors(
primary = Red300,
primaryVariant = Red700,
onPrimary = Color.Black,
secondary = Red300,
onSecondary = Color.Black,
error = Red200
)
darkColors
和lightColors
一样,当我们没有提供调色板颜色时提供默认值
然后,更新 StudyTheme
:
@Composable
fun StudyTheme(
/*+*/darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
MaterialTheme(
content = content,
/*+*/ colors = if (darkTheme) DarkColors else LightColors,
typography = StudyTypography,
shapes = StudyShapes
)
}
此时,我们添加了用于判断是否使用深色主题的新参数,并将其默认设为查询设备的全局设置
为 FeaturedPost
可组合项创建新的预览,此预览能够以深色主题显示该可组合项:
@Preview("Featured Post • Dark")
@Composable
private fun FeaturedPostDarkPreview() {
val post = remember { PostRepo.getFeaturedPost() }
StudyTheme(darkTheme = true) {
FeaturedPost(post = post)
}
}
预览效果对比
处理颜色
我们现在可以创建自己的Theme,设置应用的颜色、字体样式、形状。而所有的Material 组件默认支持这些自定义属性
例如,FloatingActionButton
可组合项默认使用主题中的 secondary
颜色,当然我们可以通过为此参数指定不同的值来设置颜色:
@Composable
fun FloatingActionButton(
...
backgroundColor: Color = MaterialTheme.colors.secondary,
...
){ ... }
原色
Compose 提供了一个 Color
类。您可以在本地创建这些类,并将其保留在 object
等元素中:
Surface(color = Color.LightGray) {
Text(
text = "Hard coded colors don't respond to theme changes :(",
textColor = Color(0xffff00ff)
)
}
注意:在静态声明颜色定义时,请务必小心,因为这些定义会导致更难/无法支持不同的主题(例如,浅色/深色主题)
Color
中有许多有用的方法,例如 copy
,您可以通过此方法使用不同的 alpha/red/green/blue 值来创建新的颜色
主题颜色
我们可以从主题中检索颜色
Surface(color = MaterialTheme.colors.primary)
通过使用 MaterialTheme
object
,其colors
属性会返回MaterialTheme
可组合项中设置的Colors
。也就是说,我们只需为主题提供不同的颜色集,即可支持不同的外观和风格,而无需处理应用代码
由于主题中的每种颜色都是Color
实例,因此我们可以通过copy
派生出不同的颜色
val derivedColor = MaterialTheme.colors.onSurface.copy(alpha = 0.1f)
这种方法可以确保颜色可以在不同主题下正常显示,无需编写静态颜色代码
背景色和内容颜色
许多组件都接受一对颜色和“内容颜色”:
Surface(
color: Color = MaterialTheme.colors.surface,
contentColor: Color = contentColorFor(color),
...
){ ... }
TopAppBar(
backgroundColor: Color = MaterialTheme.colors.primarySurface,
contentColor: Color = contentColorFor(backgroundColor),
...
){ ... }
这样我们不仅可以设置可组合项的背景颜色,还可以为"内容"(即包含在内的可组合项)提供默认颜色。默认情况下,许多可组合项都会使用这种内容颜色,如:Text、Icon
contentColorFor
方法可以为任何主题颜色检索适当的“on”颜色,例如,如果您设置 primary
背景,它就会返回 onPrimary
作为内容颜色。如果您设置非主题背景颜色,则应自行提供合理的内容颜色
跟踪
contentColorFor
方法最终会进入该方法,所以如果backgroundColor
使用非主题颜色,需要提供contentColor
fun Colors.contentColorFor(backgroundColor: Color): Color { return when (backgroundColor) { primary -> onPrimary primaryVariant -> onPrimary secondary -> onSecondary secondaryVariant -> onSecondary background -> onBackground surface -> onSurface error -> onError else -> Color.Unspecified } }
我们还可以使用 LocalContentColor
CompositionLocal
来检索与当前背景形成对比的颜色:
如果对于
CompositionLocal
很陌生,可以看看我的另一篇简书 简书-CompositionLocal
BottomNavigationItem(
unselectedContentColor = LocalContentColor.current
...
){ ... }
当设置任何元素的颜色时,最好使用 Surface
来实现此目的,因为它会设置适当的内容颜色 CompositionLocal
值
请慎用直接
Modifier.background
调用,这种调用不会设置适当的内容颜色
目前,我们的 Header
组件background
始终使用 Color.LightGray
背景。这在浅色主题中看起来没有问题,但在深色主题中,就会与背景形成高度对比。而且也不会指定与背景颜色形成对比的文本颜色
//使用uiMode,设置预览时使用深色主题
@Preview("Home",group="Home",uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun HomePreview() {
Home()
}
接下来,我们通过Surface
去解决这个问题
在 Header
可组合项中,移除用于指定硬编码颜色的 background
修饰符。改为将 Text
封装在包含主题派生颜色的 Surface
中,并指定相应内容应采用 primary
颜色:
@Composable
fun Header(
text: String,
modifier: Modifier = Modifier
) {
Surface(
color = MaterialTheme.colors.onSurface.copy(alpha = 0.1f),
contentColor = MaterialTheme.colors.primary,
modifier = modifier
) {
Text(
text = text,
modifier = Modifier
.fillMaxWidth()
.semantics { heading() }
.padding(horizontal = 16.dp, vertical = 8.dp)
)
}
}
内容 Alpha 值
通常情况下,我们通过强调或弱化内容来突出重点并体现出视觉上的层次感。Material Design 建议采用不同的不透明度来传达这些不同的重要程度
Jetpack Compose 通过 LocalContentAlpha
实现此功能。您可以通过为此 CompositionLocal
提供一个值来为层次结构指定内容 Alpha 值
子可组合项可以使用此值,例如 Text 和 Icon 默认使用LocalContentColor
的颜色值 ,其中的Alpha
会调整为 LocalContentAlpha
的值
@Composable
fun Text(...){
val textColor = color.takeOrElse {
style.color.takeOrElse {
LocalContentColor.current.copy(alpha = LocalContentAlpha.current)
}
}
...
}
@Composable
fun Icon(
...
tint: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current)
){ ... }
Material 指定了一些标准 Alpha 值(high、medium、disabled),这些值由 ContentAlpha
对象提供
object ContentAlpha {
val high: Float
@Composable
get()=...
val medium: Float
@Composable
get()=...
val disabled: Float
@Composable
get()=...
}
请注意,
MaterialTheme
默认将LocalContentAlpha
设置为ContentAlpha.high
我们将使用内容 Alpha 值来阐明精选博文的信息层次结构。在 PostMetadata
可组合项中,重点突出元数据 medium
:
@Composable
private fun PostMetadata(
post: Post,
modifier: Modifier = Modifier
) {
...
/*+*/CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
Text(
text = text,
modifier = modifier
)
/*+*/}
}
深色主题
若要在 Compose 中实现深色主题,我们只需提供不同的颜色集并通过主题查询颜色即可
我们可以通过下面代码检测是否在浅色主题中运行:
val isLightTheme = MaterialTheme.colors.isLight
此值由
lightColors/[darkColors
构建器函数设置
Material Design 建议避免在深色主题中使用大面积的明亮颜色
一种常见模式是在浅色主题中将容器背景颜色设为 primary
,并在深色主题中将其设为 surface
;许多组件都默认使用此策略,例如TopAppBar
和BottomNavigation
为了便于实现,Colors
提供了 primarySurface
颜色,以准确完成上述行为
@Composable
fun BottomNavigation(
...
backgroundColor: Color = MaterialTheme.colors.primarySurface,
...
){ ... }
@Composable
fun TopAppBar(
...
backgroundColor: Color = MaterialTheme.colors.primarySurface,
...
){ ... }
val Colors.primarySurface: Color get() = if (isLight) primary else surface
遵循此指南,我们需要将AppBar
中的TopAppBar
的backgroundColor
切换为primarySurface
或移除此参数(因为此参数为默认设置)即可
@Composable
private fun AppBar() {
TopAppBar(
navigationIcon = {
Icon(
imageVector = Icons.Rounded.Palette,
contentDescription = null,
modifier = Modifier.padding(horizontal = 12.dp)
)
},
title = {
Text(text = stringResource(R.string.app_title))
},
//更换为primarySurface或直接删除即可
backgroundColor = MaterialTheme.colors.primarySurface
)
}
在 Material 中,如果采用的是深色主题,则高度较高 (elevation) 的 Surface 会获得高度叠加层(其背景颜色会变浅)。在使用深色主题时,系统会自动实现此效果:
处理文本
在处理文本时,我们使用 Text
可组合项来显示文本,使用 TextField
和 OutlinedTextField
进行文本输入,并使用 TextStyle
对文本应用单一样式。我们可以使用 AnnotatedString
对文本应用多种样式
和颜色一样,用于显示文本的 Material 组件可以获取到主题排版自定义设置:
Button(...) {
Text("This text will use MaterialTheme.typography.button style by default")
}
不过实现此目的要比使用默认参数(如在设置颜色时所看到的那样)略微复杂一些
因为组件本身往往不会显示文本,而是提供槽 API,让您能够传入 Text
可组合项。那么,组件是如何设置主题排版样式的呢?
在后台,它们使用 ProvideTextStyle
可组合项(本身就使用 CompositionLocal
)来设置"current"TextStyle
。如果您未提供具体的 textStyle
参数,Text
可组合项会默认查询此"current"样式
@Composable
fun Button(
...
) {
val contentColor by colors.contentColor(enabled)
Surface(
...
) {
CompositionLocalProvider(LocalContentAlpha provides contentColor.alpha) {
ProvideTextStyle(
value = MaterialTheme.typography.button
) {
...
}
}
}
}
@Composable
fun Text(
...
style: TextStyle = LocalTextStyle.current
) { ... }
主题文本样式
就像颜色一样,最好从当前主题中检索 TextStyle
,使用一组数量少且一致的样式,并使其更易于维护
MaterialTheme.typography
会检索在 MaterialTheme
可组合项中设置的 Typography
实例,让我们能够使用自己定义的样式:
Text(
style = MaterialTheme.typography.subtitle2
)
如果您需要自定义 TextStyle
,可以对其执行 copy
操作并替换相关属性,或者让 Text
可组合项接受大量样式参数,这些参数会叠加到任何 TextStyle
的上层:
//使用copy操作替换属性
Text(
text = "Hello World",
style = MaterialTheme.typography.body1.copy(
background = MaterialTheme.colors.secondary
)
)
// 使用样式参数覆盖样式中的值
Text(
text = "Hello World",
style = MaterialTheme.typography.subtitle2,
fontSize = 22.sp
)
在我们的应用中,许多地方都会自动应用主题 TextStyle
,例如,TopAppBar
将其 title
的样式设为 h6
,而 ListItem
将其主要文本和辅助文本的样式分别设为 subtitle1
和 body2
接下来,我们要将主题排版样式应用于应用的其余部分。将 Header
设为使用 subtitle2
;对于 FeaturedPost
中的文本,将标题设为 h6
,并将作者信息和PostMetadata
设为 body2
:
@Composable
fun Header(
text: String,
modifier: Modifier = Modifier
) {
Surface(
color = MaterialTheme.colors.onSurface.copy(alpha = 0.1f),
contentColor = MaterialTheme.colors.primary,
modifier = modifier
) {
Text(
text = text,
/*+*/ style = MaterialTheme.typography.subtitle2,
modifier = Modifier
.fillMaxWidth()
.semantics { heading() }
.padding(horizontal = 16.dp, vertical = 8.dp)
)
}
}
//FeaturedPost和PostMetadata代码省略
多种样式
如果您需要对某些文本应用多种样式,可以使用 AnnotatedString
类来应用标记,从而为一系列文本添加 SpanStyle
。您可以动态添加这些元素,也可以使用 DSL 语法来创建内容:
val text = buildAnnotatedString {
append("This is some unstyled text\n")
withStyle(SpanStyle(color = Color.Red)) {
append("Red text\n")
}
withStyle(SpanStyle(fontSize = 24.sp)) {
append("Large text")
}
}
接下来,我们要为描述应用中的各个博文的标签设置样式。目前,它们使用与元数据其余部分相同的文本样式;我们将使用 overline
文本样式和背景颜色来区分它们。在 PostMetadata
可组合项中:
@Composable
private fun PostMetadata(
post: Post,
modifier: Modifier = Modifier
) {
val divider = " • "
val tagDivider = " "
val text = buildAnnotatedString {
append(post.metadata.date)
append(divider)
append(stringResource(R.string.read_time, post.metadata.readTimeMinutes))
append(divider)
/*+*/ val tagStyle=MaterialTheme.typography.overline.toSpanStyle().copy(
/*+*/ background = MaterialTheme.colors.primary.copy(alpha = 0.1f)
/*+*/ )
post.tags.forEachIndexed { index, tag ->
if (index != 0) {
append(tagDivider)
}
/*+*/ withStyle(tagStyle){
append(" ${tag.uppercase(Locale.getDefault())} ")
/*+*/ }
}
}
...
}
处理形状
与颜色和排版一样,如果设置形状主题,相应设置会反映在 Material 组件中。例如,Button
会获取为小型组件设置的形状:
@Composable
fun Button(
...
shape: Shape = MaterialTheme.shapes.small
...
) { ... }
与颜色一样,Material 组件使用默认参数,我们可以直接查看组件将要使用的形状类别,或提供替代方案。如需查看组件和形状类别的完整对应关系,请参阅此文档。
请注意,有些组件会使用经过修改的主题形状,以适应其上下文的要求。例如,默认情况下,
TextField
使用小型形状主题,但它会对底角应用零边角大小:@Composable fun TextField( ... shape: Shape = MaterialTheme.shapes.small.copy( bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize ), ... ) { ... }
主题形状
在创建自己的组件时,我们可以自行使用各种形状;为此,需要使用接受形状的可组合项或 Modifier
(例如,Surface
、Modifier.clip
、Modifier.background
、Modifier.border
等)
@Composable
fun UserProfile(
...
shape: Shape = MaterialTheme.shapes.medium
) {
Surface(shape = shape) {
...
}
}
接下来,我们要将形状主题添加到 PostItem
中显示的图片;我们要对其应用主题的 small
形状,并使用 Modifier.clip
应用该形状:
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun PostItem(
post: Post,
modifier: Modifier = Modifier
) {
ListItem(
modifier = modifier
.clickable { /* todo */ }
.padding(vertical = 8.dp),
icon = {
Image(
painter = painterResource(post.imageThumbId),
contentDescription = null,
/*+*/ modifier = Modifier.clip(shape = MaterialTheme.shapes.small)
)
},
text = {
Text(text = post.title)
},
secondaryText = {
PostMetadata(post)
}
)
}
组件样式
Compose 没有提供用于提取组件样式(例如,Android View 样式或 CSS 样式)的明确方法。由于所有 Compose 组件都是用 Kotlin 编写的,因此还可通过其他方法来实现相同的目的
我们可以改为创建自己的自定义组件库,并在整个应用中使用这些组件
比如 示例 中:
@Composable
fun Header(
text: String,
modifier: Modifier = Modifier
) {
Surface(
color = MaterialTheme.colors.onSurface.copy(alpha = 0.1f),
contentColor = MaterialTheme.colors.primary,
modifier = modifier.semantics { heading() }
) {
Text(
text = text,
style = MaterialTheme.typography.subtitle2,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp)
)
}
}
Header
可组合项本质上是样式化的 Text
,可供我们在整个应用中使用
所有组件都是由较低级别的构建块构造而成的,我们可以使用同样的构建块来自定义 Material 组件
例如, Button
使用 ProvideTextStyle
可组合项为传递给它的内容设置默认文本样式。您可以使用完全相同的机制来设置自己的文本样式