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 以及 topBarbottomBar 等自身组件计算出的内边距信息。我们将这个 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 根据其内部的其他组件(如 topBarbottomBar 等)以及系统栏(如状态栏、导航栏)的插入(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 的位置。如果没有设置 topBarbottomBar,则 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 contentWindowInsetsconent lambda 中的 PaddingValues(innerPadding)

4.1 概念说明
  • contentWindowInsets 定义了 Scaffold 内容区域与系统窗口(如状态栏、导航栏、输入法)之间的间距。它是一个配置项;
  • innerPadding content lambda 接收一个 PaddingValues 参数,通常命名为 innerPadding。这个 innerPadding 是由 Scaffold 根据我们设置的 contentWindowInsets 以及 Scaffold 自身的组件(如topBarbottomBar)计算得出的;
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 对象,包含了 lefttoprightbottom 四个方向的内边距值;
  • 计算方法: innerPadding 是 Scaffold 根据以下信息综合计算得出的:
    • contentWindowInsets:首先,它会获取你配置的系统窗口插入值;
    • topBar 的高度:如果设置了 topBar,Scaffold 会测量其高度,并将其加到 innerPadding.top 中;
    • bottomBar 的高度:如果设置了 bottomBar,Scaffold 会测量其高度,并将其加到 innerPadding.bottom 中;
  • 如何使用: 我们必须将这个 innerPadding 应用到我们的 content lambda 中的根布局上,通常是通过 Modifer.padding(innerPadding)。这样做可以确保:
    • 不会被系统状态栏或导航栏遮挡;
    • 不会被 Scaffold 的 topBarbottomBar 遮挡;
  • 本质: 它是一个输出,是 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 组件没有内置的 drawerContentdrawerState 等抽屉相关的参数,而是将抽屉功能从 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")
                    }

                }
            )

        }
    )

}
相关推荐
我命由我123452 小时前
Android 消息机制 - Looper(Looper 静态方法、Looper 静态方法注意事项、Looper 实例方法、Looper 实例方法注意事项)
android·java·android studio·安卓·android jetpack·android-studio·android runtime
消失的旧时光-19432 小时前
从 JVM 到 Linux:一次真正的系统级理解
android·linux·jvm
习惯就好zz2 小时前
Android 12 RK3588平台电源菜单深度定制指南
android·rockchip·3588·电源按钮
nono牛2 小时前
Android.bp 语法编程指南 1
android
李坤林2 小时前
Android 12 BLASTBufferQueue 深度分析
android
感觉不怎么会2 小时前
Android13 - 网络模式默认 NR only(仅5G)
android·5g
盐焗西兰花2 小时前
鸿蒙学习实战之路-Core Vision Kit人脸检测实现指南
android·学习·harmonyos
码农搬砖_20202 小时前
【一站式学会compose】 Android UI体系之 Text的使用和介绍
android·compose
介一安全2 小时前
【Frida Android】实战篇18:Frida检测与绕过——基于内核指令的攻防实战
android·网络安全·逆向·安全性测试·frida