引言:Material Design 与 Compose 的完美结合
Material Design 是 Google 于 2014 年推出的设计语言,旨在为跨平台设备提供统一的视觉和交互体验。经过近十年的演进,Material Design 已经发展到了第三个版本(Material Design 3,也称作 Material You)。
Material Design 3 的核心特性包括:
-
动态配色:系统可以根据壁纸自动生成配色方案
-
组件更新:按钮、卡片、导航栏等组件样式升级
-
无障碍增强:更高的对比度要求,更好的触摸目标
-
自适应布局:对不同屏幕尺寸更好的支持
Jetpack Compose 从一开始就深度整合了 Material Design。Compose 的 material3 库提供了完整的 Material Design 3 组件实现,包括颜色系统、排版、形状以及各种 UI 组件。
在本篇中,我们将系统介绍 Compose Material Design 3 的各个组件:
-
MaterialTheme 与颜色系统
-
排版系统(Typography)
-
形状系统(Shapes)
-
按钮系列(Button、TextButton、OutlinedButton、IconButton)
-
导航组件(TopAppBar、BottomNavigation、NavigationRail)
-
容器组件(Card、Surface)
-
数据展示组件(Badge、Chip、Switch、Slider、Checkbox、RadioButton)
-
反馈组件(Snackbar、Dialog、ProgressIndicator)
注意:本篇假设你已经熟悉 Compose 基础。Material 3 组件在
androidx.compose.material3包中。
一、MaterialTheme 与颜色系统
1.1 基本主题设置
kotlin
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.material3.darkColorScheme
@Composable
fun MyAppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colorScheme = if (darkTheme) {
darkColorScheme(
primary = Color(0xFFBB86FC),
secondary = Color(0xFF03DAC6),
tertiary = Color(0xFFFFB74D),
background = Color(0xFF121212),
surface = Color(0xFF1E1E1E)
)
} else {
lightColorScheme(
primary = Color(0xFF6200EE),
secondary = Color(0xFF03DAC6),
tertiary = Color(0xFFFF9800),
background = Color(0xFFFFFFFF),
surface = Color(0xFFFFFFFF)
)
}
MaterialTheme(
colorScheme = colorScheme,
typography = MyTypography,
shapes = MyShapes,
content = content
)
}
1.2 使用主题颜色
kotlin
@Composable
fun ThemedComponent() {
// 使用主题中的颜色
Surface(
color = MaterialTheme.colorScheme.primaryContainer,
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
) {
Text(
text = "这是一个容器",
modifier = Modifier.padding(16.dp)
)
}
Button(
onClick = { },
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.secondary
)
) {
Text("次要按钮")
}
}
1.3 MaterialTheme 提供的颜色
| 颜色角色 | 用途 |
|---|---|
primary |
主要品牌色 |
onPrimary |
在 primary 上方的元素颜色 |
primaryContainer |
主要色容器 |
secondary |
次要品牌色 |
onSecondary |
在 secondary 上方的元素颜色 |
tertiary |
第三品牌色 |
background |
背景色 |
surface |
表面色 |
error |
错误色 |
surfaceVariant |
表面变体色 |
二、排版系统
kotlin
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
val MyTypography = Typography(
displayLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Bold,
fontSize = 57.sp,
lineHeight = 64.sp
),
displayMedium = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 45.sp,
lineHeight = 52.sp
),
displaySmall = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 36.sp,
lineHeight = 44.sp
),
headlineLarge = TextStyle(
fontWeight = FontWeight.SemiBold,
fontSize = 32.sp,
lineHeight = 40.sp
),
headlineMedium = TextStyle(
fontWeight = FontWeight.SemiBold,
fontSize = 28.sp,
lineHeight = 36.sp
),
headlineSmall = TextStyle(
fontWeight = FontWeight.SemiBold,
fontSize = 24.sp,
lineHeight = 32.sp
),
bodyLarge = TextStyle(
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp
),
bodyMedium = TextStyle(
fontWeight = FontWeight.Normal,
fontSize = 14.sp,
lineHeight = 20.sp
),
bodySmall = TextStyle(
fontWeight = FontWeight.Normal,
fontSize = 12.sp,
lineHeight = 16.sp
),
labelLarge = TextStyle(
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
lineHeight = 20.sp
),
labelMedium = TextStyle(
fontWeight = FontWeight.Medium,
fontSize = 12.sp,
lineHeight = 16.sp
)
)
@Composable
fun TypographyDemo() {
Column {
Text("Display Large", style = MaterialTheme.typography.displayLarge)
Text("Display Medium", style = MaterialTheme.typography.displayMedium)
Text("Display Small", style = MaterialTheme.typography.displaySmall)
Text("Headline Large", style = MaterialTheme.typography.headlineLarge)
Text("Body Large", style = MaterialTheme.typography.bodyLarge)
Text("Body Medium", style = MaterialTheme.typography.bodyMedium)
Text("Label Large", style = MaterialTheme.typography.labelLarge)
}
}
三、形状系统
kotlin
import androidx.compose.material3.Shapes
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.ui.unit.dp
val MyShapes = Shapes(
extraSmall = RoundedCornerShape(4.dp),
small = RoundedCornerShape(8.dp),
medium = RoundedCornerShape(12.dp),
large = RoundedCornerShape(16.dp),
extraLarge = RoundedCornerShape(28.dp)
)
@Composable
fun ShapesDemo() {
Column {
Surface(
shape = MaterialTheme.shapes.small,
modifier = Modifier.size(80.dp),
color = MaterialTheme.colorScheme.primary
) {
// 小圆角内容
}
Surface(
shape = MaterialTheme.shapes.medium,
modifier = Modifier.size(80.dp),
color = MaterialTheme.colorScheme.secondary
) {
// 中圆角内容
}
Card(
shape = MaterialTheme.shapes.large,
modifier = Modifier.fillMaxWidth()
) {
Text("大圆角卡片", modifier = Modifier.padding(16.dp))
}
}
}
四、按钮系列
4.1 FilledButton(填充按钮)
kotlin
@Composable
fun FilledButtonExample() {
Button(
onClick = { },
modifier = Modifier.fillMaxWidth()
) {
Text("填充按钮")
}
}
4.2 OutlinedButton(边框按钮)
kotlin
@Composable
fun OutlinedButtonExample() {
OutlinedButton(
onClick = { },
modifier = Modifier.fillMaxWidth()
) {
Text("边框按钮")
}
}
4.3 TextButton(文本按钮)
kotlin
@Composable
fun TextButtonExample() {
TextButton(
onClick = { },
modifier = Modifier.fillMaxWidth()
) {
Text("文本按钮")
}
}
4.4 IconButton(图标按钮)
kotlin
@Composable
fun IconButtonExample() {
IconButton(
onClick = { }
) {
Icon(Icons.Default.Favorite, contentDescription = "收藏")
}
}
4.5 FilledTonalButton(色调按钮)
kotlin
@Composable
fun TonalButtonExample() {
FilledTonalButton(
onClick = { },
modifier = Modifier.fillMaxWidth()
) {
Text("色调按钮")
}
}
4.6 带图标按钮
kotlin
@Composable
fun IconButtonExample() {
Button(
onClick = { },
modifier = Modifier.fillMaxWidth()
) {
Icon(Icons.Default.Add, contentDescription = null)
Spacer(modifier = Modifier.width(8.dp))
Text("添加")
}
}
4.7 按钮状态
kotlin
@Composable
fun ButtonStatesExample() {
var isLoading by remember { mutableStateOf(false) }
Column {
Button(
onClick = { },
enabled = false,
modifier = Modifier.fillMaxWidth()
) {
Text("禁用按钮")
}
Button(
onClick = { isLoading = !isLoading },
modifier = Modifier.fillMaxWidth()
) {
if (isLoading) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = Color.White
)
} else {
Text("点击加载")
}
}
}
}
五、导航组件
5.1 TopAppBar
kotlin
@Composable
fun TopAppBarExample() {
Scaffold(
topBar = {
TopAppBar(
title = { Text("标题") },
navigationIcon = {
IconButton(onClick = { }) {
Icon(Icons.Default.ArrowBack, contentDescription = "返回")
}
},
actions = {
IconButton(onClick = { }) {
Icon(Icons.Default.Search, contentDescription = "搜索")
}
IconButton(onClick = { }) {
Icon(Icons.Default.MoreVert, contentDescription = "更多")
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer
)
)
}
) { paddingValues ->
Box(modifier = Modifier.padding(paddingValues)) {
// 内容
}
}
}
// 中型顶部栏
@Composable
fun CenterAlignedTopAppBarExample() {
CenterAlignedTopAppBar(
title = { Text("居中标题") },
navigationIcon = {
IconButton(onClick = { }) {
Icon(Icons.Default.ArrowBack, null)
}
},
actions = {
IconButton(onClick = { }) {
Icon(Icons.Default.Share, null)
}
}
)
}
// 大型顶部栏
@Composable
fun LargeTopAppBarExample() {
LargeTopAppBar(
title = { Text("大标题") },
navigationIcon = {
IconButton(onClick = { }) {
Icon(Icons.Default.Menu, null)
}
}
)
}
5.2 BottomNavigation
kotlin
@Composable
fun BottomNavigationExample() {
val items = listOf(
NavigationItem("首页", Icons.Default.Home),
NavigationItem("搜索", Icons.Default.Search),
NavigationItem("收藏", Icons.Default.Favorite),
NavigationItem("个人", Icons.Default.Person)
)
var selectedIndex by remember { mutableStateOf(0) }
Scaffold(
bottomBar = {
NavigationBar {
items.forEachIndexed { index, item ->
NavigationBarItem(
selected = selectedIndex == index,
onClick = { selectedIndex = index },
icon = { Icon(item.icon, contentDescription = item.label) },
label = { Text(item.label) }
)
}
}
}
) { paddingValues ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
when (selectedIndex) {
0 -> HomeScreen()
1 -> SearchScreen()
2 -> FavoritesScreen()
3 -> ProfileScreen()
}
}
}
}
data class NavigationItem(val label: String, val icon: ImageVector)
5.3 NavigationRail(适用于平板/横屏)
kotlin
@Composable
fun NavigationRailExample() {
val items = listOf(
NavigationItem("首页", Icons.Default.Home),
NavigationItem("收藏", Icons.Default.Favorite),
NavigationItem("个人", Icons.Default.Person)
)
var selectedIndex by remember { mutableStateOf(0) }
Row {
NavigationRail(
modifier = Modifier.fillMaxHeight(),
containerColor = MaterialTheme.colorScheme.surfaceVariant
) {
items.forEachIndexed { index, item ->
NavigationRailItem(
selected = selectedIndex == index,
onClick = { selectedIndex = index },
icon = { Icon(item.icon, contentDescription = item.label) },
label = { Text(item.label) }
)
}
}
Box(
modifier = Modifier
.fillMaxSize()
.weight(1f)
) {
when (selectedIndex) {
0 -> HomeScreen()
1 -> FavoritesScreen()
2 -> ProfileScreen()
}
}
}
}
六、容器组件
6.1 Card
kotlin
@Composable
fun CardExample() {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
),
elevation = CardDefaults.cardElevation(
defaultElevation = 4.dp
)
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text("卡片标题", style = MaterialTheme.typography.titleLarge)
Spacer(modifier = Modifier.height(8.dp))
Text("卡片内容,可以包含文本、图片等元素。")
}
}
}
6.2 Surface
Surface 是 Material Design 的基础容器,支持背景、形状、高程等。
kotlin
@Composable
fun SurfaceExample() {
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.primaryContainer,
tonalElevation = 3.dp,
shadowElevation = 2.dp
) {
Text(
text = "Surface 容器",
modifier = Modifier.padding(24.dp),
color = MaterialTheme.colorScheme.onPrimaryContainer
)
}
}
七、数据展示组件
7.1 Badge(徽章)
kotlin
@Composable
fun BadgeExample() {
var notifications by remember { mutableStateOf(5) }
BadgedBox(
badge = {
Badge(
containerColor = MaterialTheme.colorScheme.error,
contentColor = Color.White
) {
Text(notifications.toString())
}
}
) {
IconButton(onClick = { notifications++ }) {
Icon(Icons.Default.Notifications, contentDescription = "通知")
}
}
}
7.2 Chip(芯片)
kotlin
@Composable
fun ChipExample() {
val chips = listOf("Android", "Kotlin", "Compose", "Material")
var selectedChips by remember { mutableStateOf(setOf<String>()) }
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
chips.forEach { chip ->
FilterChip(
selected = selectedChips.contains(chip),
onClick = {
selectedChips = if (selectedChips.contains(chip)) {
selectedChips - chip
} else {
selectedChips + chip
}
},
label = { Text(chip) }
)
}
}
}
// 可关闭的芯片
@Composable
fun AssistChipExample() {
var visible by remember { mutableStateOf(true) }
if (visible) {
AssistChip(
onClick = { },
label = { Text("帮助提示") },
trailingIcon = {
IconButton(onClick = { visible = false }) {
Icon(Icons.Default.Close, null, modifier = Modifier.size(16.dp))
}
}
)
}
}
7.3 Switch(开关)
kotlin
@Composable
fun SwitchExample() {
var checked by remember { mutableStateOf(false) }
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
Text("启用通知")
Switch(
checked = checked,
onCheckedChange = { checked = it }
)
}
}
7.4 Slider(滑块)
kotlin
@Composable
fun SliderExample() {
var value by remember { mutableStateOf(0.5f) }
Column {
Text("音量: ${(value * 100).toInt()}%")
Slider(
value = value,
onValueChange = { value = it },
valueRange = 0f..1f,
steps = 4 // 中间档位数
)
}
}
// 范围滑块
@Composable
fun RangeSliderExample() {
var range by remember { mutableStateOf(0.2f..0.8f) }
Column {
Text("价格区间: ¥${(range.start * 100).toInt()} - ¥${(range.endInclusive * 100).toInt()}")
RangeSlider(
value = range,
onValueChange = { range = it },
valueRange = 0f..1f,
steps = 9
)
}
}
7.5 Checkbox(复选框)
kotlin
@Composable
fun CheckboxExample() {
var checked by remember { mutableStateOf(false) }
Row(
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
checked = checked,
onCheckedChange = { checked = it }
)
Text("同意服务条款")
}
}
7.6 RadioButton(单选按钮)
kotlin
@Composable
fun RadioButtonExample() {
val options = listOf("选项一", "选项二", "选项三")
var selectedOption by remember { mutableStateOf(options[0]) }
Column {
options.forEach { option ->
Row(
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
selected = selectedOption == option,
onClick = { selectedOption = option }
)
Text(option, modifier = Modifier.padding(start = 8.dp))
}
}
}
}
7.7 ProgressIndicator(进度指示器)
kotlin
@Composable
fun ProgressIndicatorExample() {
var progress by remember { mutableStateOf(0f) }
val scope = rememberCoroutineScope()
Column {
// 圆形进度指示器
CircularProgressIndicator()
Spacer(modifier = Modifier.height(16.dp))
// 带进度的圆形进度指示器
CircularProgressIndicator(
progress = progress,
modifier = Modifier.size(48.dp)
)
Spacer(modifier = Modifier.height(16.dp))
// 线性进度指示器
LinearProgressIndicator(
progress = progress,
modifier = Modifier.fillMaxWidth()
)
Button(
onClick = {
scope.launch {
while (progress < 1f) {
delay(100)
progress += 0.01f
}
}
}
) {
Text("开始加载")
}
}
}
八、反馈组件
8.1 Snackbar
kotlin
@Composable
fun SnackbarExample() {
val scaffoldState = rememberScaffoldState()
val scope = rememberCoroutineScope()
Scaffold(
scaffoldState = scaffoldState,
snackbarHost = { SnackbarHost(it) }
) { paddingValues ->
Box(modifier = Modifier.padding(paddingValues)) {
Button(
onClick = {
scope.launch {
scaffoldState.snackbarHostState.showSnackbar(
message = "操作成功",
actionLabel = "撤销",
duration = SnackbarDuration.Short
)
}
}
) {
Text("显示 Snackbar")
}
}
}
}
8.2 AlertDialog
kotlin
@Composable
fun DialogExample() {
var showDialog by remember { mutableStateOf(false) }
Button(onClick = { showDialog = true }) {
Text("显示对话框")
}
if (showDialog) {
AlertDialog(
onDismissRequest = { showDialog = false },
title = { Text("确认操作") },
text = { Text("确定要执行此操作吗?") },
confirmButton = {
TextButton(onClick = { showDialog = false }) {
Text("确定")
}
},
dismissButton = {
TextButton(onClick = { showDialog = false }) {
Text("取消")
}
}
)
}
}
九、完整实战:购物应用主页
kotlin
@Composable
fun ShoppingHomeScreen() {
val scaffoldState = rememberScaffoldState()
var selectedCategory by remember { mutableStateOf("推荐") }
val categories = listOf("推荐", "手机", "电脑", "家电", "服饰")
Scaffold(
topBar = {
TopAppBar(
title = { Text("商城") },
actions = {
IconButton(onClick = { }) {
Icon(Icons.Default.Search, null)
}
IconButton(onClick = { }) {
BadgedBox(
badge = { Badge { Text("3") } }
) {
Icon(Icons.Default.ShoppingCart, null)
}
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primary,
titleContentColor = Color.White
)
)
},
bottomBar = {
NavigationBar {
val items = listOf(
"首页" to Icons.Default.Home,
"分类" to Icons.Default.Category,
"购物车" to Icons.Default.ShoppingCart,
"个人" to Icons.Default.Person
)
var selected by remember { mutableStateOf(0) }
items.forEachIndexed { index, (label, icon) ->
NavigationBarItem(
selected = selected == index,
onClick = { selected = index },
icon = { Icon(icon, null) },
label = { Text(label) }
)
}
}
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
// 分类标签栏
LazyRow(
contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
items(categories) { category ->
FilterChip(
selected = selectedCategory == category,
onClick = { selectedCategory = category },
label = { Text(category) }
)
}
}
// 商品列表
LazyVerticalGrid(
columns = GridCells.Fixed(2),
contentPadding = PaddingValues(16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(10) { index ->
ProductCard(
name = "商品 $index",
price = "¥${(index + 1) * 99}",
onAddToCart = {
// 添加到购物车
}
)
}
}
}
}
}
@Composable
fun ProductCard(
name: String,
price: String,
onAddToCart: () -> Unit
) {
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(
modifier = Modifier.padding(12.dp)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f)
.background(
color = MaterialTheme.colorScheme.surfaceVariant,
shape = RoundedCornerShape(8.dp)
)
) {
// 商品图片
Icon(
Icons.Default.Image,
null,
modifier = Modifier.align(Alignment.Center),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Spacer(modifier = Modifier.height(8.dp))
Text(
text = name,
style = MaterialTheme.typography.bodyMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = price,
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(8.dp))
Button(
onClick = onAddToCart,
modifier = Modifier.fillMaxWidth(),
shape = MaterialTheme.shapes.small
) {
Text("加入购物车")
}
}
}
}
十、Material Design 3 组件速查表
| 组件 | 用途 | 主要参数 |
|---|---|---|
Button |
主要操作按钮 | onClick, enabled, colors |
OutlinedButton |
次要操作 | onClick, colors |
TextButton |
低强调操作 | onClick |
IconButton |
图标按钮 | onClick |
FloatingActionButton |
主要悬浮操作 | onClick |
TopAppBar |
顶部导航栏 | title, navigationIcon, actions |
BottomNavigation |
底部导航 | items, selectedIndex |
NavigationRail |
侧边导航(大屏幕) | items, selectedIndex |
Card |
容器卡片 | colors, elevation |
Surface |
基础容器 | color, shape, elevation |
Badge |
徽章/角标 | containerColor |
Chip |
标签/筛选 | onClick, label, selected |
Switch |
开关控制 | checked, onCheckedChange |
Slider |
滑动选择 | value, onValueChange |
Checkbox |
多选 | checked, onCheckedChange |
RadioButton |
单选 | selected, onClick |
CircularProgressIndicator |
圆形进度 | progress |
LinearProgressIndicator |
线性进度 | progress |
Snackbar |
底部提示 | action, duration |
AlertDialog |
对话框 | title, text, confirmButton |
DropdownMenu |
下拉菜单 | expanded, onDismissRequest |
练习题
-
题一:创建一个完整的应用主题,包含深色和浅色模式切换功能。
-
题二 :使用
Card、Button、Switch等组件实现一个设备控制面板。 -
题三:实现一个带有底部导航栏的应用,包含三个页面:首页、收藏、个人中心。
-
题四 :使用
RangeSlider实现商品价格筛选功能。 -
题五:创建一组筛选芯片(Chip),支持单选和多选模式。
写在最后
Material Design 3 是 Compose 的官方设计语言,掌握 Material 3 组件库是开发现代化 Android 应用的必备技能。本系列已经发布了三十五篇教程,覆盖了从环境搭建到高级组件的方方面面。
希望这个系列能成为你在 Android 开发道路上的得力助手。
下一篇文章见。