Jeptpack Compose 官网教程学习笔记(四)主题

主题

主要学习内容

  • 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 定义了一些从语义上命名的颜色,我们可以在应用中使用:

Material Theme 调色板

其中 primary 是应用的主要颜色,secondary 用于提供强调色,我们可以通过这两种颜色的设置凸显出需要对比区域

backgroundsurface两种颜色用于在概念上驻留在"应用表面"的组件的容器,也就是背景颜色

Material Design 中还定义了on颜色,是与具名颜色产生明显对比的颜色

例如:采用surface作为背景颜色的容器中文本应该采用onSurface颜色

Material 组件已配置并使用这些主题颜色。例如,FloatingActionButton的默认颜色为 secondaryCard的默认颜色为 surface,诸如此类。

排版

同样,Material Theme还定义了一些从语义上命名的字体样式:

Material Theme 排版

虽然我们可能不会按主题来更改字体样式,但使用 Material Design 中的字体样式可提升应用内部的一致性

Material 组件已配置并使字体样式。例如,TopAppBar默认使用 h6 样式,Button默认使用 button样式,诸如此类。

形状

Material Theme 中定义了 3 个类别:小型、中型和大型组件;每种组件都可以定义要使用的形状,从而自定义角的样式(切角和圆角)和大小

Material Theme 形状

默认情况下,ButtonTextField使用小型形状主题,CardDialog使用中型形状主题,Sheet使用大型形状主题

如需查看组件和形状主题的完整对应关系,请点击此处

基准

Material 默认采用“基准”主题,即紫色的配色方案、Roboto 字体比例,以及以上图片所示的略呈圆形的形状。如果您未指定或自定义主题,组件就会使用基准主题

定义主题

MaterialTheme

在 Jetpack Compose 中实现主题设置的核心元素是 MaterialTheme 可组合项。如果将此可组合项放在 Compose 层次结构中,您就可以为其中的所有组件指定颜色、字体和形状的自定义设置

@Composable
fun MaterialTheme(
    colors: Colors,
    typography: Typography,
    shapes: Shapes,
    content: @Composable () -> Unit
) { ... }

我们可以使用 MaterialTheme object 检索传递到此可组合项的参数,以公开 colorstypographyshapes 属性。在之后,我们将逐一进行深入介绍

找到 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(...){...}
  }
}

同样在PostItemPreviewFeaturedPostPreview 以使用新的 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 变为左上切角形状

image-20220517113932065.png

深色主题

在应用中支持深色主题不仅有助于您的应用在用户设备上更好地集成(从 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
)

darkColorslightColors一样,当我们没有提供调色板颜色时提供默认值

然后,更新 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()
}
image-20220517132639268.png

接下来,我们通过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 ;许多组件都默认使用此策略,例如TopAppBarBottomNavigation

为了便于实现,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中的TopAppBarbackgroundColor 切换为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 可组合项来显示文本,使用 TextFieldOutlinedTextField 进行文本输入,并使用 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将其主要文本和辅助文本的样式分别设为 subtitle1body2

接下来,我们要将主题排版样式应用于应用的其余部分。将 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())} ")
/*+*/       }
        }
    }
    ...
}
image-20220517151906032.png

处理形状

与颜色和排版一样,如果设置形状主题,相应设置会反映在 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(例如,SurfaceModifier.clipModifier.backgroundModifier.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)
        }
    )
}
image-20220517153124898.png

组件样式

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 可组合项为传递给它的内容设置默认文本样式。您可以使用完全相同的机制来设置自己的文本样式

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容