Skip to content

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 ​

参数类型默认值说明
itemsList<BottomNavItem>—导航项,等宽平分整行宽度
selectedIndexInt—选中下标。越界时全部按未选中渲染,不崩
onSelect(Int) -> Unit—点击回调,参数是下标
modifierModifierModifier修饰符
colorsBottomNavBarColorsBottomNavBarDefaults.colors()配色

BottomNavItem ​

字段类型默认值说明
labelString—文字,同时用作图标的 contentDescription
iconImageVector—未选中时的图标
selectedIconImageVectoricon选中时的图标

BottomNavBarColors ​

字段默认值
containerColorMaterialTheme.colorScheme.surfaceContainerLow
selectedContentColorMaterialTheme.colorScheme.primary
unselectedContentColorMaterialTheme.colorScheme.onSurfaceVariant

没有分割线槽位 —— 理由见「要点」。

要点 ​

  • icon(未选中)在前、selectedIcon(选中)在后 —— 和直觉相反。 用「实心 / 描边」两套图标时(例如 Phosphor 的 Fill 作选中态、Bold 作未选中态)一定用命名参数写,位置传参很容易把选中和未选中写反。

  • items 一般 3~5 个。 再多文字会被挤掉(每项等宽平分)。

  • 不要自己补底部 inset。 组件内部自带 navigationBarsPadding(),全面屏 / 手势导航设备上不会被系统导航栏压住;重复叠加会多出一段空白。

  • 不要再给它加底部内边距。 它是贴在内容下方的一层(内部 fillMaxWidth,不含任何外边距),放在 weight(1f) 的内容之后、Column 的末尾:

    kotlin
    Column {
        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。比 M3 NavigationBar 的 80dp 矮一档,且不开放尺寸参数 —— 底部栏高度全站一致。
  • 选中 / 未选中只换图标和颜色,不做位移、不做缩放、不加背景胶囊。底部栏是导航而不是内容,动效留白给页面本身。