外观
BottomNavBar — 底部导航
页面底部的 Tab 栏:图标 + 文字等宽平分,选中项取主色。顶上那条通栏分割线由组件自己渲染,调用方不用管。
┌──────────────────────────────┐
│──────────────────────────────│ ← 全宽分割线(组件自带)
│ ⌂ ▦ 🔖 </> │ ← 56dp
│ 首页 分类 书架 书源 │
└──────────────────────────────┘
↑ navigationBarsPadding 在这里基本用法
kotlin
private val mainTabs = listOf(
BottomNavItem("首页", icon = Phosphor.Bold.House, selectedIcon = Phosphor.Fill.House),
BottomNavItem("分类", icon = Phosphor.Bold.SquaresFour, selectedIcon = Phosphor.Fill.SquaresFour),
BottomNavItem("消息", icon = Phosphor.Bold.Chat, selectedIcon = Phosphor.Fill.Chat),
BottomNavItem("我的", icon = Phosphor.Bold.User, selectedIcon = Phosphor.Fill.User),
)
@Composable
fun MainScreen(vm: MainViewModel) {
Column(Modifier.fillMaxSize()) {
Box(Modifier.weight(1f)) {
when (vm.mainTab) {
0 -> HomeTab()
1 -> CategoryTab()
2 -> MessageTab()
3 -> ProfileTab()
}
}
BottomNavBar(
items = mainTabs,
selectedIndex = vm.mainTab,
onSelect = { vm.switchTab(it) },
)
}
}单图标形态(只靠颜色区分选中):
kotlin
BottomNavItem("我的", Phosphor.Bold.User)深色底 + 白字:
kotlin
BottomNavBar(
items = mainTabs,
selectedIndex = selected,
onSelect = { selected = it },
colors = BottomNavBarDefaults.colors(
containerColor = Color.Black,
selectedContentColor = Color.White,
unselectedContentColor = Color.White.copy(alpha = 0.6f),
),
)参数
BottomNavBar
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
items | List<BottomNavItem> | — | 导航项,等宽平分整行宽度 |
selectedIndex | Int | — | 选中下标。越界时全部按未选中渲染,不崩 |
onSelect | (Int) -> Unit | — | 点击回调,参数是下标 |
modifier | Modifier | Modifier | 修饰符 |
colors | BottomNavBarColors | BottomNavBarDefaults.colors() | 配色 |
BottomNavItem
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
label | String | — | 文字,同时用作图标的 contentDescription |
icon | ImageVector | — | 未选中时的图标 |
selectedIcon | ImageVector | icon | 选中时的图标 |
BottomNavBarColors
| 字段 | 默认值 |
|---|---|
containerColor | MaterialTheme.colorScheme.surfaceContainerLow |
selectedContentColor | MaterialTheme.colorScheme.primary |
unselectedContentColor | MaterialTheme.colorScheme.onSurfaceVariant |
没有分割线槽位 —— 理由见「要点」。
要点
icon(未选中)在前、selectedIcon(选中)在后 —— 和直觉相反。 用「实心 / 描边」两套图标时(例如 Phosphor 的Fill作选中态、Bold作未选中态)一定用命名参数写,位置传参很容易把选中和未选中写反。items一般 3~5 个。 再多文字会被挤掉(每项等宽平分)。不要自己补底部 inset。 组件内部自带
navigationBarsPadding(),全面屏 / 手势导航设备上不会被系统导航栏压住;重复叠加会多出一段空白。不要再给它加底部内边距。 它是贴在内容下方的一层(内部
fillMaxWidth,不含任何外边距),放在weight(1f)的内容之后、Column的末尾:kotlinColumn { Box(Modifier.weight(1f)) { /* 内容 */ } BottomNavBar(…) // 最后,且不要再加底部内边距 }没有点击水波纹。 点击用
indication = null关掉了涟漪 —— 底部栏是常驻控件,切一次 Tab 就扩散一圈灰光太吵;选中态本身的颜色变化已经够强的反馈了。这条是规格不是疏漏,别「顺手补上」indication。页面的列表底部留白按内容区的需要算,不用给导航栏再额外留 —— 导航栏自己不占内容区的高度。
说明
- 顶上那条分割线固定由组件渲染,用的是
HorizontalDivider(startIndent = 0.dp, endIndent = 0.dp)—— 通栏,不留两端缩进(其他位置的默认缩进是 16dp,这里不能用)。规格见分隔线。 - 为什么不让调用方管那条线:底部栏压在内容之上,没有这条线,滚动内容会看起来「漏」进导航栏里。
BottomNavBarColors里没有分割线槽位,这是刻意的:分割线规格全站统一(1 物理像素 +outlineVariant@70%),理由同BottomSheet。开了这个口子,同一个界面上迟早出现两种深浅的线。- 尺寸规格:内容区高 56dp(不含系统导航栏 inset)、图标 22dp、图标与文字间距 3dp、文字用
labelSmall。比 M3NavigationBar的 80dp 矮一档,且不开放尺寸参数 —— 底部栏高度全站一致。 - 选中 / 未选中只换图标和颜色,不做位移、不做缩放、不加背景胶囊。底部栏是导航而不是内容,动效留白给页面本身。