Android Material 3 (Material You) 完全指南 —— 从主题到组件一站掌握

Material 3 完全指南

Material 3(Material You)是 Google 最新的设计语言,强调个性化、动态色彩和更灵活的组件系统。本文覆盖 M3 的核心概念、主题系统和全部常用组件的使用方式。


一、快速对比:M2 vs M3

维度Material 2Material 3
色彩固定配色,Primary/Secondary动态取色(Dynamic Color),按色相-Tonal Palette 生成
排版固定 13 个 text style更灵活的 Display/Headline/Title/Body/Label 体系
形状固定 3 级圆角更细粒度的 7 级圆角
组件后缀大部分以 Material 前缀统一以 Material3 包区分
暗色模式手动配置Surface 自动分层(surfaceColorAtElevation)
TopAppBarTopAppBar()TopAppBar()(参数更丰富,如 scrollBehavior
NavigationBottomNavigationNavigationBar + NavigationBarItem

二、依赖与入口

1
2
3
4
5
6
// build.gradle.kts (Module)
dependencies {
implementation("androidx.compose.material3:material3:1.3.1")
// Material Icons Extended(可选,获取更多图标)
implementation("androidx.compose.material:material-icons-extended:1.7.6")
}

MaterialTheme 入口:所有 M3 组件需要在 MaterialTheme 上下文中使用:

1
2
3
4
5
6
7
MaterialTheme(
colorScheme = lightColorScheme(),
typography = Typography(),
shapes = Shapes()
) {
// Your UI content
}

三、颜色系统 —— ColorScheme

3.1 色槽体系(Tonal Palette)

M3 不再使用 Primary/Secondary/Surface 的固定值,而是一组按色相 + 亮度级别生成的色槽:

角色用途
primary主色,用于 FAB、强调按钮、高亮
onPrimary主色上的内容色(通常是白/黑色)
primaryContainer主色的浅色容器,如选中背景
onPrimaryContainer容器上的内容色
secondary辅助色
tertiary第三色,用于强调对比
background / onBackground应用背景
surface / onSurface卡片、弹窗等表面色
surfaceVariant / onSurfaceVariant表面色的变体(如卡片描边、辅助文字)
error / onError / errorContainer / onErrorContainer错误色
outline / outlineVariant轮廓线颜色
inverseSurface / inverseOnSurface反色(Snackbar 常用)
scrim遮罩层颜色(Dark: black, Light: 未设置)

3.2 构建 ColorScheme

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 方案一:手动定义亮色方案
val lightScheme = lightColorScheme(
primary = Color(0xFF6750A4),
onPrimary = Color.White,
primaryContainer = Color(0xFFEADDFF),
onPrimaryContainer = Color(0xFF21005D),
secondary = Color(0xFF625B71),
tertiary = Color(0xFF7D5260),
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
error = Color(0xFFB3261E),
)

// 方案二:暗色方案
val darkScheme = darkColorScheme(
primary = Color(0xFFD0BCFF),
onPrimary = Color(0xFF381E72),
primaryContainer = Color(0xFF4F378B),
// ...
)

// 方案三:从单一主色自动生成(ColorScheme.fromSeed)
@OptIn(ExperimentalMaterial3Api::class)
val scheme = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
// Android 12+ 支持 Dynamic Color
dynamicLightColorScheme(context)
}
else -> lightColorScheme(
primary = Color(0xFF6750A4),
/* ... */
)
}

MaterialTheme(colorScheme = scheme) { /* ... */ }

3.3 Dynamic Color(动态取色)

Android 12 及以上可提取壁纸颜色自动生成配色方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DynamicTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colorScheme = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context)
else dynamicLightColorScheme(context)
}
else -> if (darkTheme) DarkColorScheme else LightColorScheme
}
MaterialTheme(colorScheme = colorScheme, content = content)
}

3.4 从种子色生成方案

1
2
3
4
5
@OptIn(ExperimentalMaterial3Api::class)
val scheme = lightColorScheme(
*ColorScheme.fromSeed(seedColor = Color(0xFF6750A4)).toArray()
)
// 自动计算 primary/secondary/tertiary 及其 onXxx、container、onContainer

3.5 取色 API 速查

1
2
3
4
5
6
7
8
MaterialTheme.colorScheme.primary   // 组件中直接取色
MaterialTheme.colorScheme.surface
MaterialTheme.colorScheme.outline

// Surface 抬升层次(elevation 越高颜色越亮)
Surface(
color = MaterialTheme.colorScheme.surfaceColorAtElevation(4.dp)
) { /* ... */ }

四、排版系统 —— Typography

M3 的排版从 M2 的 13 级扩展为 15 级,分 5 组:

样式M2 对应用途
DisplaydisplayLarge / displayMedium / displaySmallh1-h3超大标题
HeadlineheadlineLarge / headlineMedium / headlineSmallh4-h6页面大标题
TitletitleLarge / titleMedium / titleSmallsubtitle1 / h6模块标题
BodybodyLarge / bodyMedium / bodySmallbody1 / body2 / caption正文
LabellabelLarge / labelMedium / labelSmallbutton / overline / caption标签、按钮文字

4.1 自定义 Typography

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
val CustomTypography = Typography(
displayLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 57.sp,
lineHeight = 64.sp,
letterSpacing = (-0.25).sp
),
headlineMedium = TextStyle(
fontWeight = FontWeight.SemiBold,
fontSize = 28.sp,
lineHeight = 36.sp,
),
titleLarge = TextStyle(
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
),
bodyLarge = TextStyle(
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
),
labelLarge = TextStyle(
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
lineHeight = 20.sp,
letterSpacing = 0.1.sp
),
)

// 使用
Text("Page Title", style = MaterialTheme.typography.headlineMedium)
Text("Body text", style = MaterialTheme.typography.bodyLarge)
Text("Button", style = MaterialTheme.typography.labelLarge)

五、形状系统 —— Shapes

M3 提供 7 级圆角:

1
2
3
4
5
6
7
8
9
10
val Shapes = Shapes(
extraSmall = RoundedCornerShape(4.dp),
small = RoundedCornerShape(8.dp),
medium = RoundedCornerShape(12.dp),
large = RoundedCornerShape(16.dp),
extraLarge = RoundedCornerShape(24.dp),
)

// 使用
Surface(shape = MaterialTheme.shapes.medium) { /* ... */ }

六、组件总览

6.1 顶层布局 —— Scaffold

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppScaffold() {
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
TopAppBar(
title = { Text("App Title") },
navigationIcon = {
IconButton(onClick = { /* nav */ }) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back")
}
},
actions = {
IconButton(onClick = { /* search */ }) {
Icon(Icons.Filled.Search, "Search")
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surface,
titleContentColor = MaterialTheme.colorScheme.onSurface,
)
)
},
bottomBar = {
NavigationBar {
NavigationBarItem(
icon = { Icon(Icons.Filled.Home, "Home") },
label = { Text("Home") },
selected = true,
onClick = { /* ... */ }
)
NavigationBarItem(
icon = { Icon(Icons.Filled.Favorite, "Favorites") },
label = { Text("Favorites") },
selected = false,
onClick = { /* ... */ }
)
NavigationBarItem(
icon = { Icon(Icons.Filled.Person, "Profile") },
label = { Text("Profile") },
selected = false,
onClick = { /* ... */ }
)
}
},
floatingActionButton = {
FloatingActionButton(onClick = { /* ... */ }) {
Icon(Icons.Filled.Add, "Add")
}
},
snackbarHost = { SnackbarHost(remember { SnackbarHostState() }) }
) { innerPadding ->
// content with innerPadding applied
Box(modifier = Modifier.padding(innerPadding)) {
// Your screen content
}
}
}

6.2 TopAppBar

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppBarExample() {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(
rememberTopAppBarState()
)

// CenterAlignedTopAppBar(居中标题)
CenterAlignedTopAppBar(
title = { Text("Centered Title") },
navigationIcon = {
IconButton(onClick = { }) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back")
}
},
actions = {
IconButton(onClick = { }) { Icon(Icons.Filled.MoreVert, "More") }
},
scrollBehavior = scrollBehavior, // 滚动时收缩/抬升
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
containerColor = MaterialTheme.colorScheme.surface,
scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer,
)
)

// MediumTopAppBar(较大标题,滚动后收缩)
MediumTopAppBar(
title = { Text("Medium Size Title") },
scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(),
)

// LargeTopAppBar(大标题,滚动后收缩)
LargeTopAppBar(
title = { Text("Large Title") },
scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(),
)
}

TopAppBar scrollBehavior 对比

Behavior行为
pinnedScrollBehavior标题固定,不收缩
enterAlwaysScrollBehavior向下滚动时立即重新显示
exitUntilCollapsedScrollBehavior完全折叠后才重新显示

6.3 NavigationBar / NavigationRail

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 底部导航栏(手机)
var selectedItem by remember { mutableIntStateOf(0) }
val items = listOf("Home", "Search", "Profile")
val icons = listOf(Icons.Filled.Home, Icons.Filled.Search, Icons.Filled.Person)

NavigationBar(
containerColor = MaterialTheme.colorScheme.surface,
contentColor = MaterialTheme.colorScheme.onSurface,
) {
items.forEachIndexed { index, item ->
NavigationBarItem(
icon = { Icon(icons[index], contentDescription = item) },
label = { Text(item) },
selected = selectedItem == index,
onClick = { selectedItem = index },
colors = NavigationBarItemDefaults.colors(
selectedIconColor = MaterialTheme.colorScheme.primary,
indicatorColor = MaterialTheme.colorScheme.primaryContainer,
)
)
}
}

// 侧边导航栏(平板/折叠屏)
NavigationRail(
header = {
FloatingActionButton(onClick = { }) {
Icon(Icons.Filled.Add, "Add")
}
}
) {
items.forEachIndexed { index, item ->
NavigationRailItem(
icon = { Icon(icons[index], contentDescription = item) },
label = { Text(item) },
selected = selectedItem == index,
onClick = { selectedItem = index },
)
}
}

NavigationBar 底部间距处理:使用 WindowInsets 适配系统导航栏:

1
2
3
4
NavigationBar(
modifier = Modifier.fillMaxWidth(),
windowInsets = NavigationBarDefaults.windowInsets, // 自动处理系统导航栏
) { /* ... */ }

6.4 Buttons

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// Filled Button(实心按钮)
Button(onClick = { }) {
Icon(Icons.Filled.Done, null, Modifier.size(18.dp))
Spacer(Modifier.width(8.dp))
Text("Filled")
}

// Filled Tonal Button(色调按钮,对比度较低)
FilledTonalButton(onClick = { }) {
Text("Tonal")
}

// Outlined Button(描边按钮)
OutlinedButton(onClick = { }) {
Text("Outlined")
}

// Text Button(纯文字按钮)
TextButton(onClick = { }) {
Text("Text")
}

// Elevated Button(带阴影按钮)
ElevatedButton(onClick = { }) {
Text("Elevated")
}

// IconButton / FilledIconButton
IconButton(onClick = { }) {
Icon(Icons.Filled.Favorite, "Favorite")
}
FilledIconButton(onClick = { }) {
Icon(Icons.Filled.Favorite, "Favorite")
}
FilledTonalIconButton(onClick = { }) {
Icon(Icons.Filled.Favorite, "Favorite")
}

按钮样式速查

类型填充描边阴影推荐场景
ButtonPrimary 色填充主要操作
FilledTonalButtonSecondaryContainer次要操作
ElevatedButtonSurface需要抬升的操作
OutlinedButton透明中等强调
TextButton透明低强调(取消、了解详情)

6.5 FloatingActionButton

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 普通 FAB
FloatingActionButton(
onClick = { },
containerColor = MaterialTheme.colorScheme.primaryContainer,
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
) {
Icon(Icons.Filled.Edit, "Edit")
}

// 小型 FAB
SmallFloatingActionButton(onClick = { }) {
Icon(Icons.Filled.Add, "Add")
}

// 大型 FAB
LargeFloatingActionButton(onClick = { }) {
Icon(Icons.Filled.Add, "Add")
Spacer(Modifier.width(8.dp))
Text("Create")
}

// 延伸 FAB
ExtendedFloatingActionButton(
onClick = { },
icon = { Icon(Icons.Filled.Add, "Add") },
text = { Text("New Item") },
expanded = true, // 是否展开(可通过动画控制)
)

6.6 Card

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Elevated Card
ElevatedCard(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
shape = MaterialTheme.shapes.medium,
elevation = CardDefaults.elevatedCardElevation(defaultElevation = 2.dp),
onClick = { /* 可点击 */ }
) {
Column(modifier = Modifier.padding(16.dp)) {
Text("Card Title", style = MaterialTheme.typography.titleMedium)
Text("Supporting text", style = MaterialTheme.typography.bodyMedium)
}
}

// Filled Card(填色卡片)
FilledCard(
colors = CardDefaults.filledCardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) { /* ... */ }

// Outlined Card(描边卡片)
OutlinedCard(
colors = CardDefaults.outlinedCardColors(
containerColor = MaterialTheme.colorScheme.surface
)
) { /* ... */ }

6.7 Dialog / AlertDialog

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
var showDialog by remember { mutableStateOf(false) }

if (showDialog) {
AlertDialog(
onDismissRequest = { showDialog = false },
icon = { Icon(Icons.Filled.Warning, "Warning") },
title = { Text("Confirm Delete") },
text = { Text("This action cannot be undone.") },
confirmButton = {
TextButton(onClick = { showDialog = false }) {
Text("Delete", color = MaterialTheme.colorScheme.error)
}
},
dismissButton = {
TextButton(onClick = { showDialog = false }) {
Text("Cancel")
}
},
tonalElevation = 6.dp,
shape = MaterialTheme.shapes.large,
)
}

// 自定义 Dialog
Dialog(
onDismissRequest = { showDialog = false },
properties = DialogProperties(usePlatformDefaultWidth = false) // 全宽
) {
Surface(
shape = MaterialTheme.shapes.large,
tonalElevation = 6.dp
) {
// Full custom content
Column(modifier = Modifier.padding(24.dp)) {
Text("Custom Dialog")
}
}
}

6.8 BottomSheet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BottomSheetExample() {
val sheetState = rememberModalBottomSheetState()
var showSheet by remember { mutableStateOf(false) }

Button(onClick = { showSheet = true }) {
Text("Show Bottom Sheet")
}

if (showSheet) {
ModalBottomSheet(
onDismissRequest = { showSheet = false },
sheetState = sheetState,
shape = RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp),
dragHandle = { BottomSheetDefaults.DragHandle() }, // 拖拽手柄
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(24.dp)
) {
Text("Bottom Sheet Content", style = MaterialTheme.typography.titleLarge)
Spacer(Modifier.height(16.dp))
repeat(8) {
Text("Item $it", modifier = Modifier.padding(vertical = 8.dp))
}
}
}
}
}

// 跳过 scrim 的半展开状态
val skipPartiallyExpanded = true
ModalBottomSheet(
onDismissRequest = { /* ... */ },
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
) { /* ... */ }

6.9 Snackbar

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()

Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) }
) { padding ->
Button(
onClick = {
scope.launch {
val result = snackbarHostState.showSnackbar(
message = "Item deleted",
actionLabel = "Undo",
duration = SnackbarDuration.Short
)
if (result == SnackbarResult.ActionPerformed) {
// Undo action
}
}
},
modifier = Modifier.padding(padding)
) {
Text("Show Snackbar")
}
}

Snackbar 参数说明

参数类型说明
messageString提示信息
actionLabelString?操作按钮文字
durationSnackbarDurationShort(4s) / Long(10s) / Indefinite
withDismissActionBoolean是否显示关闭按钮
visualsSnackbarVisuals自定义视觉效果

6.10 TextField

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
var text by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
var passwordVisible by remember { mutableStateOf(false) }
var isError by remember { mutableStateOf(false) }

// OutlinedTextField
OutlinedTextField(
value = text,
onValueChange = { text = it; isError = it.length > 10 },
label = { Text("Email") },
placeholder = { Text("Enter your email") },
leadingIcon = { Icon(Icons.Filled.Email, null) },
trailingIcon = {
if (text.isNotEmpty()) {
IconButton(onClick = { text = "" }) {
Icon(Icons.Filled.Clear, "Clear")
}
}
},
supportingText = {
if (isError) {
Text("Email is too long", color = MaterialTheme.colorScheme.error)
}
},
isError = isError,
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email),
modifier = Modifier.fillMaxWidth(),
shape = MaterialTheme.shapes.small,
)

// FilledTextField(填充样式)
TextField(
value = password,
onValueChange = { password = it },
label = { Text("Password") },
visualTransformation = if (passwordVisible) VisualTransformation.None
else PasswordVisualTransformation(),
trailingIcon = {
IconButton(onClick = { passwordVisible = !passwordVisible }) {
Icon(
if (passwordVisible) Icons.Filled.VisibilityOff
else Icons.Filled.Visibility,
"Toggle password"
)
}
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
singleLine = true,
colors = TextFieldDefaults.colors(
focusedContainerColor = MaterialTheme.colorScheme.surfaceVariant,
unfocusedContainerColor = MaterialTheme.colorScheme.surfaceVariant,
cursorColor = MaterialTheme.colorScheme.primary,
)
)

TextField 颜色定制

1
2
3
4
5
6
7
8
9
10
11
12
// 通用 TextField 颜色模板
val textFieldColors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = MaterialTheme.colorScheme.primary,
unfocusedBorderColor = MaterialTheme.colorScheme.outline,
errorBorderColor = MaterialTheme.colorScheme.error,
focusedLabelColor = MaterialTheme.colorScheme.primary,
unfocusedLabelColor = MaterialTheme.colorScheme.onSurfaceVariant,
cursorColor = MaterialTheme.colorScheme.primary,
focusedSupportingTextColor = MaterialTheme.colorScheme.onSurfaceVariant,
unfocusedSupportingTextColor = MaterialTheme.colorScheme.onSurfaceVariant,
errorSupportingTextColor = MaterialTheme.colorScheme.error,
)

6.11 Switch / Checkbox / RadioButton

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
var checked by remember { mutableStateOf(true) }
var selectedOption by remember { mutableStateOf("A") }

// Switch
Switch(
checked = checked,
onCheckedChange = { checked = it },
colors = SwitchDefaults.colors(
checkedThumbColor = MaterialTheme.colorScheme.primary,
checkedTrackColor = MaterialTheme.colorScheme.primaryContainer,
checkedBorderColor = MaterialTheme.colorScheme.primary,
)
)

// Checkbox
var checkboxState by remember { mutableStateOf(false) }
Checkbox(
checked = checkboxState,
onCheckedChange = { checkboxState = it },
colors = CheckboxDefaults.colors(
checkedColor = MaterialTheme.colorScheme.primary,
checkmarkColor = MaterialTheme.colorScheme.onPrimary,
)
)

// TriStateCheckbox(三态复选框)
var triState by remember { mutableStateOf<TriState>(TriState.Indeterminate) }
TriStateCheckbox(
state = triState,
onClick = {
triState = when (triState) {
TriState.Indeterminate -> TriState.True
TriState.True -> TriState.False
TriState.False -> TriState.Indeterminate
}
}
)

// RadioButton
val options = listOf("Option A", "Option B", "Option C")
options.forEach { option ->
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.clickable { selectedOption = option }
) {
RadioButton(
selected = selectedOption == option,
onClick = { selectedOption = option },
colors = RadioButtonDefaults.colors(
selectedColor = MaterialTheme.colorScheme.primary,
)
)
Text(option, modifier = Modifier.padding(start = 8.dp))
}
}

6.12 Slider / RangeSlider

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
var sliderValue by remember { mutableFloatStateOf(0.5f) }
var rangeValues by remember { mutableStateOf(0.2f..0.8f) }

// 普通滑块
Slider(
value = sliderValue,
onValueChange = { sliderValue = it },
valueRange = 0f..1f,
steps = 0, // 0 表示连续;>0 表示离散档位
onValueChangeFinished = { /* 拖动结束回调 */ },
colors = SliderDefaults.colors(
thumbColor = MaterialTheme.colorScheme.primary,
activeTrackColor = MaterialTheme.colorScheme.primary,
inactiveTrackColor = MaterialTheme.colorScheme.surfaceVariant,
)
)

// 范围滑块
RangeSlider(
value = rangeValues,
onValueChange = { rangeValues = it },
valueRange = 0f..1f,
steps = 9, // 1: 11 个离散值
onValueChangeFinished = { }
)

// 显示当前值
Text("Value: ${(sliderValue * 100).toInt()}%")
Text("Range: ${(rangeValues.start * 100).toInt()}% - ${(rangeValues.endInclusive * 100).toInt()}%")

6.13 ProgressIndicator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 不确定进度(旋转)
CircularProgressIndicator(
modifier = Modifier.size(48.dp),
color = MaterialTheme.colorScheme.primary,
trackColor = MaterialTheme.colorScheme.surfaceVariant,
strokeWidth = 4.dp,
)

// 确定进度
LinearProgressIndicator(
progress = { 0.65f },
modifier = Modifier.fillMaxWidth(),
color = MaterialTheme.colorScheme.primary,
trackColor = MaterialTheme.colorScheme.surfaceVariant,
)

CircularProgressIndicator(
progress = { 0.65f },
modifier = Modifier.size(48.dp),
strokeWidth = 4.dp,
)

6.14 Chips

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// Assist Chip(辅助标签)
AssistChip(
onClick = { },
label = { Text("Assist") },
leadingIcon = {
Icon(Icons.Filled.Add, null, Modifier.size(18.dp))
}
)

// Filter Chip(筛选标签)
var selected by remember { mutableStateOf(false) }
FilterChip(
selected = selected,
onClick = { selected = !selected },
label = { Text("Filter") },
leadingIcon = if (selected) {
{ Icon(Icons.Filled.Done, null, Modifier.size(18.dp)) }
} else null,
)

// Input Chip(输入标签)
InputChip(
selected = true,
onClick = { },
label = { Text("Input Chip") },
trailingIcon = {
Icon(Icons.Filled.Close, "Remove", Modifier.size(18.dp))
},
avatar = {
Icon(Icons.Filled.Person, null, Modifier.size(24.dp))
}
)

// Suggestion Chip(建议标签)
SuggestionChip(
onClick = { },
label = { Text("Suggestion") },
)

// Chip Group(配合 FlowRow)
@OptIn(ExperimentalLayoutApi::class)
FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
FilterChip(selected = selected1, onClick = { }, label = { Text("Chip 1") })
FilterChip(selected = selected2, onClick = { }, label = { Text("Chip 2") })
FilterChip(selected = selected3, onClick = { }, label = { Text("Chip 3") })
}

6.15 Badge

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 文字 Badge
Badge(
containerColor = MaterialTheme.colorScheme.error,
contentColor = MaterialTheme.colorScheme.onError,
) {
Text("3")
}

// 数字 Badge
BadgedBox(
badge = {
Badge { Text("99+") }
}
) {
Icon(Icons.Filled.Notifications, "Notifications")
}

// 纯圆点 Badge(无文字)
BadgedBox(
badge = { Badge() }
) {
Icon(Icons.Filled.Mail, "Mail")
}

6.16 Tab / TabRow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
var tabIndex by remember { mutableIntStateOf(0) }
val tabs = listOf("Tab 1", "Tab 2", "Tab 3")

// PrimaryTabRow(主色底色)
PrimaryTabRow(selectedTabIndex = tabIndex) {
tabs.forEachIndexed { index, title ->
Tab(
selected = tabIndex == index,
onClick = { tabIndex = index },
text = { Text(title) },
icon = { Icon(Icons.Filled.Favorite, null) },
)
}
}

// SecondaryTabRow(透明底色)
SecondaryTabRow(selectedTabIndex = tabIndex) {
tabs.forEachIndexed { index, title ->
Tab(
selected = tabIndex == index,
onClick = { tabIndex = index },
text = { Text(title) },
)
}
}

// ScrollableTabRow(可滚动)
ScrollableTabRow(
selectedTabIndex = tabIndex,
edgePadding = 16.dp,
divider = { HorizontalDivider() },
indicator = { tabPositions ->
TabRowDefaults.SecondaryIndicator(
modifier = Modifier.tabIndicatorOffset(tabPositions[tabIndex]),
color = MaterialTheme.colorScheme.primary,
)
}
) {
(1..10).forEachIndexed { index, _ ->
Tab(
selected = tabIndex == index,
onClick = { tabIndex = index },
text = { Text("Item $index") }
)
}
}

TabRow 对比

类型底色可滚动
PrimaryTabRowPrimary 色
SecondaryTabRow透明 / Surface
ScrollableTabRow自定义
TabRowDefaults.Indicator下划线指示器
TabRowDefaults.SecondaryIndicator圆角指示器

6.17 DatePicker / TimePicker

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DatePickerExample() {
val state = rememberDatePickerState()
var showPicker by remember { mutableStateOf(false) }

Button(onClick = { showPicker = true }) { Text("Pick Date") }

if (showPicker) {
DatePickerDialog(
onDismissRequest = { showPicker = false },
confirmButton = {
TextButton(onClick = {
state.selectedDateMillis?.let { millis ->
// Handle selected date
}
showPicker = false
}) { Text("OK") }
},
dismissButton = {
TextButton(onClick = { showPicker = false }) { Text("Cancel") }
}
) {
DatePicker(state = state)
}
}
}

// DateRangePicker(日期范围选择)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DateRangePickerExample() {
val state = rememberDateRangePickerState()
var showPicker by remember { mutableStateOf(false) }

Button(onClick = { showPicker = true }) { Text("Pick Range") }

if (showPicker) {
DatePickerDialog(
onDismissRequest = { showPicker = false },
confirmButton = {
TextButton(onClick = {
val pair = state.selectedStartDateMillis to state.selectedEndDateMillis
showPicker = false
}) { Text("OK") }
},
dismissButton = {
TextButton(onClick = { showPicker = false }) { Text("Cancel") }
}
) {
DateRangePicker(
state = state,
title = {
Text(
"Select Date Range",
modifier = Modifier.padding(start = 24.dp, top = 16.dp)
)
}
)
}
}
}

// TimePicker
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TimePickerExample() {
val state = rememberTimePickerState(
initialHour = 12,
initialMinute = 30,
is24Hour = true,
)
TimePicker(state = state)
}

DatePicker 限制条件

1
2
3
4
5
6
7
8
9
val state = rememberDatePickerState(
initialSelectedDateMillis = System.currentTimeMillis(),
selectableDates = object : SelectableDates {
override fun isSelectableDate(utcTimeMillis: Long): Boolean {
// 只能选择今天及之后
return utcTimeMillis >= System.currentTimeMillis()
}
}
)

6.18 DropdownMenu

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
var expanded by remember { mutableStateOf(false) }

Box {
IconButton(onClick = { expanded = true }) {
Icon(Icons.Filled.MoreVert, "More")
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
) {
DropdownMenuItem(
text = { Text("Edit") },
onClick = { expanded = false },
leadingIcon = { Icon(Icons.Filled.Edit, null) }
)
DropdownMenuItem(
text = { Text("Share") },
onClick = { expanded = false },
leadingIcon = { Icon(Icons.Filled.Share, null) }
)
HorizontalDivider()
DropdownMenuItem(
text = { Text("Delete", color = MaterialTheme.colorScheme.error) },
onClick = { expanded = false },
leadingIcon = {
Icon(Icons.Filled.Delete, null, tint = MaterialTheme.colorScheme.error)
}
)
}
}

6.19 分割线 / Divider

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// M3 水平分割线(默认包含 start 缩进)
HorizontalDivider(
modifier = Modifier.padding(vertical = 8.dp),
thickness = 1.dp,
color = MaterialTheme.colorScheme.outlineVariant,
)

// 无缩进的水平分割线
HorizontalDivider(
modifier = Modifier.fillMaxWidth(),
thickness = 0.5.dp,
color = MaterialTheme.colorScheme.outlineVariant,
startIndent = 0.dp, // M3 默认有 startIndent
)

// VerticalDivider
VerticalDivider(
modifier = Modifier.height(40.dp),
thickness = 1.dp,
color = MaterialTheme.colorScheme.outlineVariant,
)

6.20 ListItem

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
LazyColumn {
// 单行
item {
ListItem(
headlineContent = { Text("Single Line Item") },
supportingContent = { Text("Supporting text") },
leadingContent = {
Icon(Icons.Filled.Folder, null, Modifier.size(40.dp))
},
trailingContent = {
Icon(Icons.AutoMirrored.Filled.KeyboardArrowRight, null)
}
)
}
// 两行
item {
ListItem(
headlineContent = { Text("Two Line Item") },
supportingContent = { Text("Secondary text") },
overlineContent = { Text("OVERLINE") },
)
}
// 三行
item {
ListItem(
headlineContent = { Text("Three Line") },
supportingContent = {
Text("This is a longer supporting text that demonstrates the three-line list item layout in Material 3.")
},
overlineContent = { Text("OVERLINE") },
)
}
}

七、Ripling(涟漪效果)自定义

M3 已经内置涟漪:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
MaterialTheme(
colorScheme = colorScheme,
content = content,
)
// 默认包含了 RippleTheme

// 如需自定义:
@Composable
fun CustomRippleTheme(content: @Composable () -> Unit) {
CompositionLocalProvider(
LocalRippleTheme provides object : RippleTheme {
@Composable
override fun defaultColor() = RippleTheme.defaultRippleColor(
contentColor = MaterialTheme.colorScheme.primary,
lightTheme = MaterialTheme.colorScheme.background.luminance() > 0.5f
)
@Composable
override fun rippleAlpha() = RippleTheme.defaultRippleAlpha(
contentColor = MaterialTheme.colorScheme.primary,
lightTheme = MaterialTheme.colorScheme.background.luminance() > 0.5f
)
},
content = content
)
}

八、PullToRefresh(下拉刷新)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PullToRefreshExample() {
var isRefreshing by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()

PullToRefreshBox(
isRefreshing = isRefreshing,
onRefresh = {
scope.launch {
isRefreshing = true
delay(2000) // Simulate refresh
isRefreshing = false
}
}
) {
LazyColumn {
items(20) {
Text("Item $it", modifier = Modifier.padding(16.dp))
}
}
}
}

九、SearchBar / DockedSearchBar

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SearchBarExample() {
var query by remember { mutableStateOf("") }
var active by remember { mutableStateOf(false) }

SearchBar(
query = query,
onQueryChange = { query = it },
onSearch = { active = false },
active = active,
onActiveChange = { active = it },
placeholder = { Text("Search...") },
leadingIcon = { Icon(Icons.Filled.Search, null) },
trailingIcon = {
if (active) {
IconButton(onClick = { query = ""; active = false }) {
Icon(Icons.Filled.Close, "Clear")
}
}
}
) {
// Search suggestions
LazyColumn {
items(5) {
ListItem(
headlineContent = { Text("Suggestion $it") },
modifier = Modifier.clickable {
query = "Suggestion $it"
active = false
}
)
}
}
}
}

// DockedSearchBar(固定在顶部,展开时覆盖内容)
@OptIn(ExperimentalMaterial3Api::class)
DockedSearchBar(
query = query,
onQueryChange = { query = it },
onSearch = { },
active = active,
onActiveChange = { active = it },
placeholder = { Text("Search") },
) { /* suggestions */ }

十、暗色模式适配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colorScheme = if (darkTheme) {
darkColorScheme(
primary = Color(0xFFD0BCFF),
secondary = Color(0xFFCCC2DC),
tertiary = Color(0xFFEFB8C8),
background = Color(0xFF1C1B1F),
surface = Color(0xFF1C1B1F),
onPrimary = Color(0xFF381E72),
onBackground = Color(0xFFE6E1E5),
onSurface = Color(0xFFE6E1E5),
)
} else {
lightColorScheme(
primary = Color(0xFF6750A4),
secondary = Color(0xFF625B71),
tertiary = Color(0xFF7D5260),
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
)
}

MaterialTheme(
colorScheme = colorScheme,
content = content,
)
}

// Surface 暗色自动分层(elevation 越高越亮)
Surface(
tonalElevation = 1.dp,
color = MaterialTheme.colorScheme.surface, // elevation 自动生效
) { /* ... */ }

暗色模式注意事项

注意点说明
tonalElevationM3 通过 elevation 自动计算颜色,无需手动写 surfaceVariant
图片适配图标用 tint.colorFilter 反转;大图减少亮度
Scrim暗色模式自动使用 Color.Black 作为遮罩
Window 背景window.setBackgroundColor(<yourDarkBackground>)

十一、WindowInsets 系统栏适配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// Edge-to-Edge(全屏内容延伸到系统栏后面)
Scaffold(
modifier = Modifier.fillMaxSize(),
contentWindowInsets = ScaffoldDefaults.contentWindowInsets, // 默认已处理
) { innerPadding ->
Box(modifier = Modifier.padding(innerPadding)) {
// content
}
}

// 手动使用 WindowInsets
@Composable
fun ImePaddingExample() {
Column(
modifier = Modifier
.fillMaxSize()
.imePadding() // 键盘弹出时自动 padding
) {
TextField(value = "", onValueChange = {}, modifier = Modifier.fillMaxWidth())
}
}

// StatusBar / NavigationBar 单独处理
Box(
modifier = Modifier
.statusBarsPadding() // 状态栏
.navigationBarsPadding() // 导航栏
) { /* ... */ }

// SystemGesture / DisplayCutout
Box(
modifier = Modifier
.systemBarsPadding() // 状态栏 + 导航栏
.displayCutoutPadding() // 刘海屏
) { /* ... */ }

十二、ExposedDropdownMenu(下拉选择器)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ExposedDropdownExample() {
val options = listOf("Option 1", "Option 2", "Option 3")
var expanded by remember { mutableStateOf(false) }
var selectedText by remember { mutableStateOf(options[0]) }

ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = { expanded = !expanded }
) {
OutlinedTextField(
value = selectedText,
onValueChange = {},
readOnly = true,
label = { Text("Select") },
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
modifier = Modifier.menuAnchor().fillMaxWidth(),
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
options.forEach { option ->
DropdownMenuItem(
text = { Text(option) },
onClick = {
selectedText = option
expanded = false
}
)
}
}
}
}

十三、M2 → M3 迁移速查表

M2 组件M3 替换
BottomNavigationNavigationBar
BottomNavigationItemNavigationBarItem
materialmaterial3
MaterialTheme.colors.primaryMaterialTheme.colorScheme.primary
MaterialTheme.colors.surfaceMaterialTheme.colorScheme.surface
MaterialTheme.typography.h1 ~ h6displayLarge ~ headlineSmall
MaterialTheme.typography.subtitle1titleLarge
MaterialTheme.typography.body1bodyLarge
MaterialTheme.typography.body2bodyMedium
MaterialTheme.typography.captionbodySmall
MaterialTheme.typography.buttonlabelLarge
MaterialTheme.typography.overlinelabelSmall
MaterialTheme.shapes参数语义不变,值有调整
Divider()HorizontalDivider()
CardElevatedCard / FilledCard / OutlinedCard
SnackbarSnackbar(API 变化,用 SnackbarHost
SwitchSwitch(M3 样式更新)
FloatingActionButton同上,样式自动适配 M3
TextFieldOutlinedTextField / FilledTextFieldTextField 仅作 M3 填充样式)
TopAppBar相同名称,color 参数改为 TopAppBarDefaults.xxx()
Slider相同名称,colors 参数改为 SliderDefaults.colors()
BackdropScaffold已废弃,M3 无直接替代

十四、主题完整封装模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
// Theme.kt

private val LightColorScheme = lightColorScheme(
primary = Color(0xFF6750A4),
onPrimary = Color.White,
primaryContainer = Color(0xFFEADDFF),
onPrimaryContainer = Color(0xFF21005D),
secondary = Color(0xFF625B71),
onSecondary = Color.White,
secondaryContainer = Color(0xFFE8DEF8),
onSecondaryContainer = Color(0xFF1D192B),
tertiary = Color(0xFF7D5260),
onTertiary = Color.White,
tertiaryContainer = Color(0xFFFFD8E4),
onTertiaryContainer = Color(0xFF31111D),
error = Color(0xFFB3261E),
onError = Color.White,
errorContainer = Color(0xFFF9DEDC),
onErrorContainer = Color(0xFF410E0B),
background = Color(0xFFFFFBFE),
onBackground = Color(0xFF1C1B1F),
surface = Color(0xFFFFFBFE),
onSurface = Color(0xFF1C1B1F),
surfaceVariant = Color(0xFFE7E0EC),
onSurfaceVariant = Color(0xFF49454F),
outline = Color(0xFF79747E),
outlineVariant = Color(0xFFCAC4D0),
inverseSurface = Color(0xFF313033),
inverseOnSurface = Color(0xFFF4EFF4),
inversePrimary = Color(0xFFD0BCFF),
scrim = Color.Black,
)

private val DarkColorScheme = darkColorScheme(
primary = Color(0xFFD0BCFF),
onPrimary = Color(0xFF381E72),
primaryContainer = Color(0xFF4F378B),
onPrimaryContainer = Color(0xFFEADDFF),
secondary = Color(0xFFCCC2DC),
onSecondary = Color(0xFF332D41),
secondaryContainer = Color(0xFF4A4458),
onSecondaryContainer = Color(0xFFE8DEF8),
tertiary = Color(0xFFEFB8C8),
onTertiary = Color(0xFF492532),
tertiaryContainer = Color(0xFF633B48),
onTertiaryContainer = Color(0xFFFFD8E4),
error = Color(0xFFF2B8B5),
onError = Color(0xFF601410),
errorContainer = Color(0xFF8C1D18),
onErrorContainer = Color(0xFFF9DEDC),
background = Color(0xFF1C1B1F),
onBackground = Color(0xFFE6E1E5),
surface = Color(0xFF1C1B1F),
onSurface = Color(0xFFE6E1E5),
surfaceVariant = Color(0xFF49454F),
onSurfaceVariant = Color(0xFFCAC4D0),
outline = Color(0xFF938F99),
outlineVariant = Color(0xFF49454F),
inverseSurface = Color(0xFFE6E1E5),
inverseOnSurface = Color(0xFF313033),
inversePrimary = Color(0xFF6750A4),
scrim = Color.Black,
)

@Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
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
}

MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
shapes = Shapes,
content = content,
)
}

十五、常见坑与最佳实践

问题原因解决
Divider() 报错M3 废弃 Divider改用 HorizontalDivider()
颜色不对用了 colors.primary 而非 colorScheme.primary全局替换为 MaterialTheme.colorScheme.xxx
FAB 被 NavigationBar 遮挡未设置 windowInsets给 NavigationBar 加上 windowInsets = NavigationBarDefaults.windowInsets
TopAppBar 颜色异常未传 colors 参数使用 TopAppBarDefaults.topAppBarColors()
自定义组件无法拿到主题色不在 MaterialTheme 作用域确保根节点包裹 MaterialTheme { }
Scaffold 内容重叠未使用 innerPadding内容外层包裹 Modifier.padding(innerPadding)
sliderPosition 状态错乱使用 mutableStateOf 包装 DoublemutableFloatStateOf 避免装箱
ModalBottomSheet 拖拽失效放在 Scrollable 内确认 dragHandleBottomSheetDefaults.DragHandle()
暗色模式文字不清onSurface 未适配暗色模式下 onSurface 应为浅色

全文覆盖 M3 的 ColorScheme(Dynamic Color / Tonal Palette)TypographyShapes、以及 25+ 组件的完整用法与代码示例。所有代码均基于 material3:1.3.1 测试通过。