Scaffold

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: content lambda 接收一个 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 应用到我们的 content lambda 中的根布局上,通常是通过 Modifer.padding(innerPadding)。这样做可以确保:
    • 不会被系统状态栏或导航栏遮挡;
    • 不会被 Scaffold 的 topBar 或 bottomBar 遮挡;
  • 本质: 它是一个输出,是 Scaffold 为我们计算好的,可以直接使用的安全内边距;
    • 作为开发者,我们不需要关系 Scaffold 是如何测量和计算的,我们只需要遵循它给出的 innerPadding 即可;
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")
                    }

                }
            )

        }
    )

}
相关推荐
Android打工仔2 小时前
Kotlin 协程源码解析:DispatchedContinuation 里的 Dispatcher 从哪里来?
android·kotlin
打工仔折腾 AI2 小时前
从BPE到SentencePiece:Transformer分词原理与Python实战对比
android·人工智能·python·深度学习·langchain·transformer·ai agent 实战
传奇开心果编程3 小时前
【现代声明式UI学与练】第1课 从命令式UI到声明式UI
学习·flutter·ui·swiftui·react·android jetpack
Dovis(誓平步青云)3 小时前
几个方案来回选不定?做一个随时切换的候选推荐页
android·java·服务器·开发语言·javascript·数据库·智能化
恋猫de小郭7 小时前
KMP 又改了编译流程, Separate Compilation 禁止了 `commonMain` 的依赖穿透
android·前端·flutter
执明wa7 小时前
Android RecyclerView 实战:点击频道栏切换对应内容
android·开发语言·windows·microsoft·android studio
新鲜势力呀8 小时前
PHP 定时数据同步实战:从第三方接口超时到异步同步 + 增量更新架构优化全过程
android
星间都市山脉9 小时前
RecoveryUI 圆角屏安全边距配置及代码调用链
android·java·linux·windows·ubuntu
打工仔折腾 AI10 小时前
DiPlay 实测:iPhone 绕过硬件盒子直连 BYD 车机的思路拆解
android·人工智能·后端·python·gradle·iphone·ai agent 实战
hengdonghui10 小时前
Writeup 4 NUAA 2017 robots
android·ctf·re