Scaffold 是 Jetpack Compose 中 Material Design 3 提供的一个核心布局组件,它提供了规范的 Material Design 页面结构,方便开发者快速搭建标准化的界面。整合顶部栏、底部导航栏、浮动按钮等常见组件,同时自动处理布局间距和系统适配,替代传统 Android 的 ConstrainLayout + AppBarLayout 组合。
1 核心作用
- 提供标准化页面结构: 封装了 Material Design 推荐的页面布局范式,无需手动拼接顶部栏、底部导航等组件;
- 简化组件整合: 支持一键集成 TopAppBar(顶部栏)、BottomNavigation(底部导航)、FloatingActionButton(浮动按钮)等 Material 组件,自动处理组件间的布局冲突;
- 自动适配安全区域: 通过
innerPadding参数,自动预留状态栏、导航栏及组件占用空间,避免内容被遮挡; - 统一交互逻辑:内置组件的默认交互(如顶部栏返回按钮、底部导航切换),减少重复代码;
2 属性
kotlin
@Composable
fun Scaffold(
modifier: Modifier = Modifier,
topBar: @Composable () -> Unit = {},
bottomBar: @Composable () -> Unit = {},
snackbarHost: @Composable () -> Unit = {},
floatingActionButton: @Composable () -> Unit = {},
floatingActionButtonPosition: FabPosition = FabPosition.End,
containerColor: Color = MaterialTheme.colorScheme.background,
contentColor: Color = contentColorFor(containerColor),
contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets,
content: @Composable (PaddingValues) -> Unit
) {
}
2.1 modifier: Modifier = Modifier
- 作用: 用于修饰 Scaffold 组件的布局和行为,可以通过它设置 Scaffold 的大小、边距、背景等;
- 默认值:
Modifier,即无任何修饰;
kotlin
Scaffold(
modifier = Modifier
.fillMaxSize() // 让 Scaffold 占满整个屏幕
.background(Color.LightGray) // 设置背景色
) {
// ...
}
2.2 topBar: @Composable () -> Unit = {}
- 作用: 用于设置 Scaffold 顶部的应用栏(App Bar),通常是一个 TopAppBar 组件。可以显示标题、导航图标、操作菜单等。
- 默认值: 空的 Composable,即不显示顶部栏;
kotlin
Scaffold(
topBar = {
SmallTopAppBar(
title = { Text("首页") },
navigationIcon = {
IconButton(onClick = { /* 打开抽屉 */ }) {
Icon(Icons.Default.Menu, contentDescription = "菜单")
}
},
actions = {
IconButton(onClick = { /* 搜索 */ }) {
Icon(Icons.Default.Search, contentDescription = "搜索")
}
}
)
}
) {
// ...
}
2.3 bottomBar: @Composable () -> Unit = {}
- 作用: 用于设置 Scaffold 的底部导航栏,通常是一个 BottomAppBar 或 NavigationBar;
- 默认值: 空的 Composable,即不显示底部导航栏;
kotlin
Scaffold(
bottomBar = {
NavigationBar {
NavigationBarItem(
selected = true,
onClick = { /* 切换到首页 */ },
icon = { Icon(Icons.Default.Home, contentDescription = "首页") },
label = { Text("首页") }
)
NavigationBarItem(
selected = false,
onClick = { /* 切换到我的 */ },
icon = { Icon(Icons.Default.Person, contentDescription = "我的") },
label = { Text("我的") }
)
}
}
) {
// ...
}
2.4 snackbarHost: @Composable () -> Unit = {}
- 作用: 用于显示 Snackbar 组件(信息栏组件,用于在屏幕底部显示简短的消息)。Snackbar 是一种轻量级的反馈组件;
- 默认值: 空的 Composable,即不显示 Snackbar;
kotlin
val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()
Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) },
floatingActionButton = {
FloatingActionButton(onClick = {
scope.launch {
snackbarHostState.showSnackbar("操作成功")
}
}) {
Icon(Icons.Default.Check, contentDescription = "显示提示")
}
}
) {
}
2.5 floatingActionButton: @Composable () -> Unit = {}
- 作用: 用于设置屏幕右下角的浮动操作按钮(FAB),通常是一个 FloatingActionButton 组件。常用于执行主要操作;
- 默认值: 空的 Composable,即不显示浮动按钮
kotlin
Scaffold(
floatingActionButton = {
FloatingActionButton(onClick = {
scope.launch {
snackbarHostState.showSnackbar("操作成功")
}
}) {
Icon(Icons.Default.Check, contentDescription = "显示提示")
}
}
) {
}
2.6 floatingActionButtonPosition: FabPosition = FabPosition.End
- 作用: 用于设置浮动操作按钮的位置;
- 默认值:
FabPosition.End,即右下角; - 可选值:
FabPosition.End(右下角)、FabPosition.Center(底部中央)、FabPosition.Start(左下角);
kotlin
Scaffold(
floatingActionButton = { /* ... */ },
floatingActionButtonPosition = FabPosition.Center
) {
// ...
}
2.7 containerColor: Color = MaterialTheme.colorScheme.background
- 作用: 设置 Scaffold 容器(即整个 Scaffold)的背景颜色;
- 默认值:
MaterialTheme.colorScheme.background,即当前主题的背景色;
kotlin
Scaffold(
containerColor = Color.Cyan
) {
// ...
}
2.8 contentColor: Color = contentColorFor(containerColor)
- 作用: 设置 Scaffold 内容的默认文本和图标颜色;
- 说明:
contentColorFor(containerColor)函数会根据背景色自动计算合适的文本颜色,以保证对比度;
kotlin
Scaffold(
containerColor = Color.DarkGray,
contentColor = Color.White
) {
// ...
}
2.9 contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets
- 作用: 定义了 Scaffold 内容区域与系统窗口(如状态栏、导航栏、输入法)之间的间距,它是一个配置项;
- 默认值:
SacffoldDefaults.contentWindowInsets即默认的窗口插入配置,会自动处理状态栏和导航栏的间距;
kotlin
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = { },
bottomBar = { },
contentWindowInsets = WindowInsets(
left = 16.dp,
top = 16.dp,
right = 16.dp,
bottom = 16.dp
)
) { innerPadding ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding) // 应用内边距,避免内容被遮挡
) {
Text("这是主要内容")
}
}
2.10 content: @Composable (PaddingValues) -> Unit
- 作用: 定义 Scaffold 的主要内容区域。它是一个 Composable 函数,接收一个 PaddingValues 参数;
- 参数: PaddingValues,是 Scaffold 根据
contentWindowInsets以及topBar、bottomBar等自身组件计算出的内边距信息。我们将这个 PaddingValues 应用到内容根布局上,以确保内容不会被遮挡;
kotlin
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
TopAppBar(
title = { Text("My APP") },
navigationIcon = {
IconButton(onClick = { /* 打开抽屉 */ }) {
Icon(Icons.Filled.Menu, contentDescription = "菜单")
}
}
)
},
bottomBar = {
NavigationBar {
}
},
) { innerPadding ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding) // 应用内边距,避免内容被遮挡
) {
Text("这是主要内容")
}
}
3 内容(lambda 表达式 { innerPadding -> ...})
Scaffold 组件内容是一个 Composable lambda,它接收一个 PaddingValues 参数,通常命名为 innerPadding。这个 innerPadding 是 Scaffold 根据其内部的其他组件(如 topBar、bottomBar 等)以及系统栏(如状态栏、导航栏)的插入(insets)自动计算出的内边距,以确保内容不会被遮挡。
简单来说,innerPadding 是一个 PaddingValues 对象,由 Scaffold 自动计算并提供。 包含以下信息:
- 系统栏:状态栏和导航栏的安全区域(如果使用了
enableEdgeToEdge,并且没有手动处理状态栏); - 顶部:如果设置了
topBar,则包含topBar的高度; - 底部:如果设置了
bottomBar,则包含bottomBar的高度; - FAB 区域:浮动按钮占用的空间,避免内容被 FAB 遮挡;
使用 innerPadding 的方式通常是在内容区域的根布局上应用 Modifier.padding(innerPadding)。这样,内容就会在安全区域内显示,不会被遮挡。
总结:innerPadding 是 Scaffold 为我们计算好的内边距,用于避免内容被顶部栏、底部栏或系统栏遮挡。我们只需要在根布局上应用这个内边距即可。
PaddingValues 的四个内边距属性:
innerPadding.calculateTopPadding():顶部内边距,通常等于状态栏高度 +topBar高度(如果有的话);innerPadding.calculateBottomPadding():底部内边距,通常等于导航栏高度 +bottomBar高度(如果有的话);innerPadding.calculateLeftPadding(layoutDirection)和innerPadding.calculateRightPadding(layoutDirection):左右内边距,在大多数情况下为 0,但在某些特殊布局或语言环境下可能不为零。
基本语法:
kotlin
Scaffold(
topBar = { TopAppBar(title = { Text("标题") }) },
bottomBar = { NavigationBar {} }
// 其他参数
) { innerPadding -> // lambda 接收 PaddingValues 参数
// 内容
Box(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
) {
Text("Hello World")
}
}
// 或者
Scaffold(
topBar = { TopAppBar(title = { Text("标题") }) },
bottomBar = { NavigationBar {} },
content = { innerPadding ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding) // 关键
) {
Text("Hello World")
}
}
)
这里,Box 使用了 innerPadding,从而在顶部留出 topBar 的位置,在底部留出 bottomBar 的位置。如果没有设置 topBar 或 bottomBar,则 innerPadding 可能只包含系统栏的插入(如果应用使用了 enableEdgeToEdge 并且 Scaffold 默认处理了系统栏)。
需要注意的是,Scaffold 默认会处理系统栏的插入,因此我们不需要再手动使用 Modifier.systemBarsPadding()。但是,如果我们需要更精细的控制,可以使用 contentWindowInsets 参数来改变 Scaffold 计算 innerPadding 方式。
另外,如果我们在 Scaffold 中使用了 enableEdgeToEdge(),那么 Scaffold 会自动处理系统栏,并将系统栏的插入考虑到 innerPadding 中。
下面举几个简单的例子。
直接应用内边距:
kotlin
Scaffold(
topBar = { TopAppBar(title = { Text("标题") }) },
bottomBar = { NavigationBar {} }
) { innerPadding ->
// 直接在内容布局上应用内边距
LazyColumn(
modifier = Modifier.padding(innerPadding)
) {
items(100) { index ->
Text("Item $index", modifier = Modifier.padding(16.dp))
}
}
}
使用 Box 包装:
kotlin
Scaffold(
topBar = { TopAppBar(title = { Text("标题") }) },
bottomBar = { NavigationBar {} }
) { innerPadding ->
Box(
modifier = Modifier.padding(innerPadding)
) {
Text("Item", modifier = Modifier.padding(16.dp))
}
}
组合多个内边距:
kotlin
Scaffold(
topBar = { TopAppBar(title = { Text("标题") }) },
bottomBar = { NavigationBar {} }
) { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding) // 先定义 Scaffold 的安全边距
.padding(horizontal = 16.dp) // 再应用自定义边距
.fillMaxSize()
) {
Text("Item")
}
}
使用总结:
- 总是在内容根布局上应用
Modifier.padding(innerPadding); - 保持内边距应用顺序:先
innerPadding,后自定义边距; - 避免在多个层级重复应用
innerPadding; - 结合
enableEdgeToEdge()实现现代化全屏体验;
4 contentWindowInsets 和 conent lambda 中的 PaddingValues(innerPadding)
4.1 概念说明
contentWindowInsets: 定义了 Scaffold 内容区域与系统窗口(如状态栏、导航栏、输入法)之间的间距。它是一个配置项;innerPadding:contentlambda 接收一个 PaddingValues 参数,通常命名为innerPadding。这个innerPadding是由 Scaffold 根据我们设置的contentWindowInsets以及 Scaffold 自身的组件(如topBar、bottomBar)计算得出的;
4.2 contentWindowInsets: WindowInsets
- 作用: 允许我们为 Scaffold 的内容区域指定一组窗口插入(Window Insets)。窗口插入表示系统 UI(如状态栏、导航栏)所占据的屏幕区域;
- 默认值:
ScaffoldDefaults.contentWindowInsets。这个默认值通常包含了状态栏(statusBars) 和 导航栏(navigationBars),意味着 Scaffold 会自动为状态栏和导航栏留出空间。但不包括像 IME(软键盘)这样的插入; - 使用: 通常不需要修改。如果有特殊要求。我们可以通过传递自定义的 WindowSets 来改变 Scaffold 内容区域的插入,例如:
- 如果我们想让内容延伸到状态栏后面,可以使用
WindowInsts(0),但这通常需要配合WindowCompat.setDecorFitsSystemWindows(window, false)和手动处理处理状态栏文字颜色; - 如果有一个可滚动的列表,当键盘弹出时,可能希望列表内容自动向上滚动,这也与
WindowInsets的处理有关;
- 如果我们想让内容延伸到状态栏后面,可以使用
- 本质: 它是一个输入,告诉 Scaffold 在计算最终内边距时需要考虑哪些系统层面的"占用空间";
4.3 innerPadding: PaddingValues
- 作用: 这是 Scaffold 传递给我们的最终计算结果。它是一个 PaddingValues 对象,包含了
left、top、right、bottom四个方向的内边距值; - 计算方法:
innerPadding是 Scaffold 根据以下信息综合计算得出的:contentWindowInsets:首先,它会获取你配置的系统窗口插入值;topBar的高度:如果设置了topBar,Scaffold 会测量其高度,并将其加到innerPadding.top中;bottomBar的高度:如果设置了bottomBar,Scaffold 会测量其高度,并将其加到innerPadding.bottom中;
- 如何使用: 我们必须将这个
innerPadding应用到我们的contentlambda 中的根布局上,通常是通过Modifer.padding(innerPadding)。这样做可以确保:- 不会被系统状态栏或导航栏遮挡;
- 不会被 Scaffold 的
topBar或bottomBar遮挡;
- 本质: 它是一个输出,是 Scaffold 为我们计算好的,可以直接使用的安全内边距;
- 作为开发者,我们不需要关系 Scaffold 是如何测量和计算的,我们只需要遵循它给出的
innerPadding即可;
- 作为开发者,我们不需要关系 Scaffold 是如何测量和计算的,我们只需要遵循它给出的
kotlin
// 这是一种特殊情况,Scaffold 不处理任何系统窗口的插入。这意味着内容会眼神到状态栏和导航栏下方
val noInsets = WindowInsets(0)
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
TopAppBar(
title = { Text("My APP") },
)
},
// 配置:Scaffold 不要为系统窗口留出空间
contentWindowInsets = noInsets
) { innerPadding -> // Scaffold 会计算出 innerPadding 并传递出来
// 打印一下 innerPadding 的值
LaunchedEffect(Unit) {
Log.d(
"ScaffoldInsets",
"innerPadding.top: ${innerPadding.calculateTopPadding()}"
)
Log.d(
"ScaffoldInsets",
"innerPadding.bottom: ${innerPadding.calculateBottomPadding()}"
)
}
Box(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding) // 应用内边距,避免内容被遮挡
.background(Color.LightGray)
) {
Text(
modifier = Modifier.align(Alignment.TopStart),
text = "我的内容区域"
)
}
}
5 基本用法(基础页面结构)
kotlin
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BasicScaffoldExample() {
Scaffold(
// 顶部栏
topBar = {
SmallTopAppBar(
title = { Text(text = "首页") } // 顶部标题
)
},
// 主内容区域(必须使用 innerPadding 避免被顶部栏遮挡)
content = { innerPadding ->
Text(
text = "这是主内容区域",
modifier = Modifier
.fillMaxSize()
.padding(innerPadding) // 应用自动计算内边距
)
}
)
}
6 高级用法
6.1 带底部导航 + 浮动按钮
kotlin
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun FullFeatureScaffold() {
// 底部导航栏选中状态
var selectedItem by rememberSaveable { mutableStateOf(0) }
// 底部导航栏菜单数据
val navItems = listOf("首页", "我的")
val navIcons = listOf(Icons.Default.Home, Icons.Default.Person)
Scaffold(
topBar = { SmallTopAppBar(title = { Text(navItems[selectedItem]) }) },
// 底部导航栏
bottomBar = {
NavigationBar {
navItems.forEachIndexed { index, item ->
NavigationBarItem(
selected = selectedItem == index,
onClick = { selectedItem = index },
icon = { Icon(navIcons[index], contentDescription = item) },
label = { Text(item) }
)
}
}
},
// 浮动操作按钮
floatingActionButton = {
FloatingActionButton(onClick = { /* 核心功能逻辑 */ }) {
Icon(Icons.Default.Add, contentDescription = "添加")
}
},
// 主内容(根据选中的底部导航切换)
content = { innerPadding ->
when (selectedItem) {
0 -> HomeScreen(modifier = Modifier.padding(innerPadding))
1 -> ProfileScreen(modifier = Modifier.padding(innerPadding))
}
}
)
}
/**
* 首页内容
*/
@Composable
fun HomeScreen(modifier: Modifier) {
Text(text = "首页内容", modifier = modifier.fillMaxSize())
}
@Composable
fun ProfileScreen(modifier: Modifier) {
Text(text = "我的页面内容", modifier = modifier.fillMaxSize())
}
6.2 带侧边抽屉(Drawer)
在 Material 3 中,Scaffold 组件没有内置的 drawerContent、drawerState 等抽屉相关的参数,而是将抽屉功能从 Scaffold 中剥离出来,使其成为一个独立的、可组合的组件,即 ModaNavigaionDrawer。
核心组件:
- ModalNavigationDrawer:实现抽屉的核心组件
drawerContent:定义抽屉内部的内容;drawerState:管理抽屉的状态(打开/关闭),通常由rememberDrawerState创建;content:主内容区域,这里放置 Scaffold;
rememberDrawerState:创建并管理抽屉状态的 Composable 函数;- NavigationDrawerItem:Material 3 提供的一个便捷组件,用于创建抽屉中的菜单项,包含了图标、文本、选中状态等。
kotlin
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ScaffoldWithDrawer() {
// 抽屉状态(默认关闭)
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val scope = rememberCoroutineScope()
ModalNavigationDrawer(
drawerContent = {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
Text(
text = "Menu",
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(bottom = 16.dp)
)
// 使用 NavigationDrawerItem 构建菜单项
var selectedItem by remember { mutableStateOf("Home") }
NavigationDrawerItem(
label = { Text(text = "Home") },
selected = selectedItem == "Home",
onClick = {
selectedItem = "Home"
// 点击后关闭抽屉
scope.launch {
drawerState.close()
}
},
icon = { Icon(Icons.Default.Home, contentDescription = "Home") }
)
NavigationDrawerItem(
label = { Text(text = "Profile") },
selected = selectedItem == "Profile",
onClick = {
selectedItem = "Profile"
scope.launch {
drawerState.close()
}
},
icon = { Icon(Icons.Default.Person, contentDescription = "Profile") }
)
}
},
// 抽屉状态
drawerState = drawerState,
// 主内容区域
content = {
// 这里是 Scaffold
Scaffold(
topBar = {
SmallTopAppBar(
title = { Text("Home") },
navigationIcon = {
// 点击图标打开抽屉
IconButton(onClick = {
scope.launch {
drawerState.open()
}
}) {
Icon(Icons.Default.Menu, contentDescription = "open menu")
}
}
)
},
content = { innerPadding ->
// 页面主内容
Column(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
.padding(16.dp)
) {
Text("Main Content")
}
}
)
}
)
}