Compose
默认提供了 Material Design
的主题实现,基于这个主题可以实现 Google 亲儿子般的 UI:
Material 主题
从 Compose
的架构分层来看,Material
的实现处于最上层,也就是说如果脱离 androidx.compose.material.*
, Compose
也是一样可以用的。
Compose
中 MaterialTheme
的定义:
MaterialTheme(
colors = …,
typography = …,
shapes = …
) {
// app content
}
颜色 - Color
Google 说强烈建议使用 Compose
中的 Color
类来定义和管理颜色,这样可以轻松支持主题的定制,甚至还能把主题嵌套起来用。
val Red = Color(0xffff0000)
val Blue = Color(red = 0f, green = 0f, blue = 1f)
例如:定义一个支持深色和浅色的主题:
private val Yellow200 = Color(0xffffeb46)
private val Blue200 = Color(0xff91a4fc)
// ...
private val DarkColors = darkColors(
primary = Yellow200,
secondary = Blue200,
// ...
)
private val LightColors = lightColors(
primary = Yellow500,
primaryVariant = Yellow400,
secondary = Blue700,
// ...
)
应用到 MaterialTheme
:
MaterialTheme(
colors = if (darkTheme) DarkColors else LightColors
) {
// app content
}
使用:
Text(
text = "Hello theming",
color = MaterialTheme.colors.primary
)
深色主题
通过向 MaterialThem
e 提供一组不同的 colors
来实现深浅两种主题:
@Composable
fun MyTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
MaterialTheme(
colors = if (darkTheme) DarkColors else LightColors,
/*...*/
content = content
)
}
isSystemInDarkTheme()
会查找设备当前是否为深色主题。
字体排版 - Typography
MaterialTheme
采用 Typography
、TextStyle
类实现默认的字体字型定义,构造MaterialTheme
时传入:
val Rubik = FontFamily(
Font(R.font.rubik_regular),
Font(R.font.rubik_medium, FontWeight.W500),
Font(R.font.rubik_bold, FontWeight.Bold)
)
val MyTypography = Typography(
h1 = TextStyle(
fontFamily = Rubik,
fontWeight = FontWeight.W300,
fontSize = 96.sp
),
body1 = TextStyle(
fontFamily = Rubik,
fontWeight = FontWeight.W600,
fontSize = 16.sp
)
/*...*/
)
MaterialTheme(typography = MyTypography, /*...*/)
然后通过 MaterialTheme.typrography
访问对应 TextStyle
即可:
Text(
text = "Subtitle2 styled",
style = MaterialTheme.typography.subtitle2
)
形状 - Shapes
Compose
使用 Shapes
实现形状系统,可以设置每个大小类别的形状默认状态:
val Shapes = Shapes(
small = RoundedCornerShape(percent = 50),
medium = RoundedCornerShape(0f),
large = CutCornerShape(
topStart = 16.dp,
topEnd = 0.dp,
bottomEnd = 0.dp,
bottomStart = 16.dp
)
)
MaterialTheme(shapes = Shapes, /*...*/)
MaterialTheme
认为默认情况下组件使用的形状基本上符合这几个大小状态,比如Button
、Text
等默认为 “小”,AlertDialog
默认为“中”, Drawer
为 “大”
使用:
Surface(
shape = MaterialTheme.shapes.medium, /*...*/
) {
/*...*/
}
Material Design 3(下一代 Material Design)
为了适配下一代Material Design
,Compose
已经开始实现 Material Design 3
, 包含更新的主题和组件,以及对Material You的个性风格的支持。
注意:
androidx.compose.material3
目前为 Alpha 版。此 API 随时可能发生变化,因此不应视为最终版本。“Material Design 3”、“Material 3”和“M3”这三个术语可以互换。现有的 Material Design 规范和相应的
androidx.compose.material
库称为“Material Design 2”、“Material 2”或“M2”。
M3 主题定义了新的配置方案和文字排版单元,目前还没有对形状的支持。但是官方说 Updates to shapes coming soon
@Composable
fun MaterialTheme(
colorScheme: ColorScheme? = MaterialTheme.colorScheme,
typography: Typography? = MaterialTheme.typography,
content: (@Composable () -> Unit)?
): Unit
M3 中的 ColorScheme
M2
中的 Color
变成了 ColorScheme
,同样以深色浅色举例:
private val Blue40 = Color(0xff1e40ff)
private val DarkBlue40 = Color(0xff3e41f4)
private val Yellow40 = Color(0xff7d5700)
// Remaining colors from tonal palettes
private val LightColorScheme = lightColorScheme(
primary = Blue40,
secondary = DarkBlue40,
tertiary = Yellow40,
// error, primaryContainer, onSecondary, etc.
)
private val DarkColorScheme = darkColorScheme(
primary = Blue80,
secondary = DarkBlue80,
tertiary = Yellow80,
// error, primaryContainer, onSecondary, etc.
)
// 使用:
val darkTheme = isSystemInDarkTheme()
MaterialTheme(
colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme
) {
// M3 app content
}
M3 中使用的“primary”、“background”和“error”等等这些值有了”插槽“的概念,他们组合在一起形成一个配色方案,每个槽位的颜色获取来自”[色调模板](Color system – Material Design 3)“,
在Material You中使用动态配色方案
Material You
是在 Android 12
中引入的概念,以用户壁纸为原型派生自定义颜色,并将其应用到系统。M3中的动态配色方案以此为起点:
// Dynamic color is available on Android 12+
val dynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
val colorScheme = when {
dynamicColor && darkTheme -> dynamicDarkColorScheme(LocalContext.current)
dynamicColor && !darkTheme -> dynamicLightColorScheme(LocalContext.current)
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
// 使用:
Text(
text = "Hello M3 theming",
color = MaterialTheme.colorScheme.tertiary
)
M3 中的 Typography
M3 中用了新的 Typography
和已有的 TextStyle
, 新增了字体比例的定义,同时命名和分组简化为: 显示、大标题、标题、正文和标签,每个都有大号、中号和小号。
val KarlaFontFamily = FontFamily(
Font(R.font.karla_regular),
Font(R.font.karla_bold, FontWeight.Bold)
)
val AppTypography = Typography(
bodyLarge = TextStyle(
fontFamily = KarlaFontFamily,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.15.sp
),
// titleMedium, labelSmall, etc.
)
MaterialTheme(
typography = AppTypography
) {
// M3 app content
}
// 使用:
Text(
text = "Hello M3 theming",
style = MaterialTheme.typography.bodyLarge
)
自定义主题
MaterialTheme
的设计看上去在 UI 层面考虑已经比较全面了,但是回到现实世界问题依然很明显:很多情况下依然无法满足设计上的需求与多样化。我们需要比现成的Material Theme
更复杂的主题。
在实际开发中更方便的做法是基于 Material Design
之上对主题进行自定义。
扩展 Material
一种比较常见的情况是尽管主题中已经提供了很多颜色,但是依然无法满足我们的需求,如果我们的使用需求与MaterialTheme
的 API
是一致的,只是不够丰富,那么我们对已有的Colors
进行扩展即可:
// Use with MaterialTheme.colors.snackbarAction
val Colors.snackbarAction: Color
get() = if (isLight) Red300 else Red700
同样的情况也适用于 Typography
Shapes
:
// Use with MaterialTheme.typography.textFieldInput
val Typography.textFieldInput: TextStyle
get() = TextStyle(/* ... */)
// Use with MaterialTheme.shapes.card
val Shapes.card: Shape
get() = RoundedCornerShape(size = 20.dp)
基于 CompositionLocal 扩展 Material
CompositionLocal
可以在 Composable
中以某个节点开始将数据“向下”传递到每一个子节点。用 Google 官方的话说就是将数据的作用域限定在局部。 为什么要这样用呢?
对于广泛使用的常用数据,在Composable
中显示的传递是一个非常麻烦的过程,特别是这种主题相关的数据:
@Composable
fun MyApp() {
// Theme information tends to be defined near the root of the application
val colors = …
}
// Some composable deep in the hierarchy
@Composable
fun SomeTextLabel(labelText: String) {
Text(
text = labelText,
color = // ← need to access colors here
)
}
为了让 colors
无需显式传递给大多数子节点Composable
, Compose
提供 CompositionLocal
来创建以树为作用域的对象,它通常在界面树的某个节点以值的形式提供,这个值可以在子节点中使用,而无需将这个对象声明为参数。
MaterialTheme
的 Color
、Typography
、Shapes
就是基于这个组件实现的,对应 LocalColors
、LocalShapes
和 LocalTypography
属性。
CompositionLocal
实例的作用域限定在Composable
中,因此可以在树的不同级别提供不同的值。如果需要更新CompositionLocal
提供新值,需使用 CompositionLocalProvider
的 infix
函数 provider
。Composable
作为 CompositionLocalProvider
的 content lambda
,会获取 CompositionLocal
的 current
以达到在任意节点读取新值的目的。
例如: LocalContentAlpha
CompositionLocal
的值是给作用域内的文本和图标提供当前主题定义的 Aplha
。强调或弱化界面不同部分,在其内部的Composable
在获取LocalContentAlpha
时,就会获取到最近一次提供的新的值,如下示例 CompositionLocalProvider
用于为 Composable
的不同部分提供不同的值:
@Composable
fun CompositionLocalExample() {
MaterialTheme { // MaterialTheme sets ContentAlpha.high as default
Column {
Text("Uses MaterialTheme's provided alpha")
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
Text("Medium value provided for LocalContentAlpha")
Text("This Text also uses the medium value")
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.disabled) {
DescendantExample()
}
}
}
}
}
@Composable
fun DescendantExample() {
// CompositionLocalProviders also work across composable functions
Text("This Text uses the disabled alpha now")
}
// Text Composable 的内部实现:
@Composable
fun Text(
text: String,
modifier: Modifier = Modifier,
color: Color = Color.Unspecified,
...
) {
val textColor = color.takeOrElse {
style.color.takeOrElse {
// 读取 colorAlpha
LocalContentColor.current.copy(alpha = LocalContentAlpha.current)
}
}
...
}
由此引入另一种方式: 封装 MaterialTheme
, 保留其原本的设定的情况下,扩展出其它值。
例如颜色:
@Immutable
data class ExtendedColors(
val tertiary: Color,
val onTertiary: Color
)
val LocalExtendedColors = staticCompositionLocalOf {
ExtendedColors(
tertiary = Color.Unspecified,
onTertiary = Color.Unspecified
)
}
@Composable
fun ExtendedTheme(
/* ... */
content: @Composable () -> Unit
) {
val extendedColors = ExtendedColors(
tertiary = Color(0xFFA8EFF0),
onTertiary = Color(0xFF002021)
)
CompositionLocalProvider(LocalExtendedColors provides extendedColors) {
MaterialTheme(
/* colors = ..., typography = ..., shapes = ... */
content = content
)
}
}
// Use with eg. ExtendedTheme.colors.tertiary
object ExtendedTheme {
val colors: ExtendedColors
@Composable
get() = LocalExtendedColors.current
}
应用到 Material
组件:
@Composable
fun ExtendedButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
content: @Composable RowScope.() -> Unit
) {
Button(
colors = ButtonDefaults.buttonColors(
backgroundColor = ExtendedTheme.colors.tertiary,
contentColor = ExtendedTheme.colors.onTertiary
/* Other colors use values from MaterialTheme */
),
onClick = onClick,
modifier = modifier,
content = content
)
}
替换 Material 系统
对 MaterialTheme
中的一个或多个单元(Colors
、Typography
或 Shapes
)进行替换,同时保留其它单元:
@Immutable
data class ReplacementTypography(
val body: TextStyle,
val title: TextStyle
)
@Immutable
data class ReplacementShapes(
val component: Shape,
val surface: Shape
)
val LocalReplacementTypography = staticCompositionLocalOf {
ReplacementTypography(
body = TextStyle.Default,
title = TextStyle.Default
)
}
val LocalReplacementShapes = staticCompositionLocalOf {
ReplacementShapes(
component = RoundedCornerShape(ZeroCornerSize),
surface = RoundedCornerShape(ZeroCornerSize)
)
}
@Composable
fun ReplacementTheme(
/* ... */
content: @Composable () -> Unit
) {
val replacementTypography = ReplacementTypography(
body = TextStyle(fontSize = 16.sp),
title = TextStyle(fontSize = 32.sp)
)
val replacementShapes = ReplacementShapes(
component = RoundedCornerShape(percent = 50),
surface = RoundedCornerShape(size = 40.dp)
)
CompositionLocalProvider(
LocalReplacementTypography provides replacementTypography,
LocalReplacementShapes provides replacementShapes
) {
MaterialTheme(
/* colors = ... */
content = content
)
}
}
// Use with eg. ReplacementTheme.typography.body
object ReplacementTheme {
val typography: ReplacementTypography
@Composable
get() = LocalReplacementTypography.current
val shapes: ReplacementShapes
@Composable
get() = LocalReplacementShapes.current
}
应用到Material
组件:
@Composable
fun ReplacementButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
content: @Composable RowScope.() -> Unit
) {
Button(
shape = ReplacementTheme.shapes.component,
onClick = onClick,
modifier = modifier,
content = {
ProvideTextStyle(
value = ReplacementTheme.typography.body
) {
content()
}
}
)
}
实现完全自定义主题系统
设计并非仅限于 Material Design
,完全自主定义一套设计语言也是可行的。 正如最开始所说,如果整个系统完全不依赖Material
层,也是完全可行的。
如下示例就实现了一套跟Material Design
一丝儿关系都没有的主题。
@Immutable
data class CustomColors(
val content: Color,
val component: Color,
val background: List<Color>
)
@Immutable
data class CustomTypography(
val body: TextStyle,
val title: TextStyle
)
@Immutable
data class CustomElevation(
val default: Dp,
val pressed: Dp
)
val LocalCustomColors = staticCompositionLocalOf {
CustomColors(
content = Color.Unspecified,
component = Color.Unspecified,
background = emptyList()
)
}
val LocalCustomTypography = staticCompositionLocalOf {
CustomTypography(
body = TextStyle.Default,
title = TextStyle.Default
)
}
val LocalCustomElevation = staticCompositionLocalOf {
CustomElevation(
default = Dp.Unspecified,
pressed = Dp.Unspecified
)
}
@Composable
fun CustomTheme(
/* ... */
content: @Composable () -> Unit
) {
val customColors = CustomColors(
content = Color(0xFFDD0D3C),
component = Color(0xFFC20029),
background = listOf(Color.White, Color(0xFFF8BBD0))
)
val customTypography = CustomTypography(
body = TextStyle(fontSize = 16.sp),
title = TextStyle(fontSize = 32.sp)
)
val customElevation = CustomElevation(
default = 4.dp,
pressed = 8.dp
)
CompositionLocalProvider(
LocalCustomColors provides customColors,
LocalCustomTypography provides customTypography,
LocalCustomElevation provides customElevation,
content = content
)
}
// Use with eg. CustomTheme.elevation.small
object CustomTheme {
val colors: CustomColors
@Composable
get() = LocalCustomColors.current
val typography: CustomTypography
@Composable
get() = LocalCustomTypography.current
val elevation: CustomElevation
@Composable
get() = LocalCustomElevation.current
}