原型:BottomBar + MiniPlayer 折叠联动
Apple Music 风格:底部 Tab Bar 向下滚动时缩起,MiniPlayer 顶上来占位;向上滚动(或列表回到顶部)时 Bar 回弹,点击折叠态也能重新展开。
本原型是一个最小可跑的独立 Demo,只依赖 Compose BOM、Material3 和 Material Icons Extended。跑通后再把 CollapsibleBottomBarScaffold 接到你的 LibraryScreen 里。
原理速览
1 | 展开态(collapseProgress = 0f): |
- 状态:
collapseProgress: Float(0=展开,1=折叠),由NestedScrollConnection的滚动增量驱动。 - Bar 位移:
translationY = collapseProgress * barHeightPx。 - 回弹:监听
LazyListState,当列表回到顶部(canScrollBackward == false)且手指抬起时animateTo(0f)。 - 点击展开:外层暴露
onExpandRequest,点击 MiniPlayer 时调用。
文件清单
Demo 放在一个独立 package,不影响你现有代码:
1 | prototype/ |
1. CollapsibleBottomBarState.kt
1 | package io.kyonqi.musicplayer.prototype |
关键修正:
onPreScroll里用scope.launch { suspend }会丢掉"消耗了多少"的同步返回值,列表会继续滚。实际实现应把consumeScroll改成同步(用Animatable.snapTo的同步封装),见下方"正确版"。
正确版 CollapsibleBottomBarState(同步 consume)
1 | package io.kyonqi.musicplayer.prototype |
2. CollapsibleBottomBarScaffold.kt
1 | package io.kyonqi.musicplayer.prototype |
3. PrototypeScreen.kt — 可直接跑的 Demo
1 | package io.kyonqi.musicplayer.prototype |
onMiniPlayerClick参数在 Scaffold 签名里我写过但 Demo 没用到——你接到真实工程时可以把它串到导航(点击跳 NowPlaying)。
如何跑这个 Demo
把 PrototypeScreen() 挂到 MainActivity.setContent { MusicPlayerTheme { PrototypeScreen() } },跑起来应该看到:
- ✅ 向上滑列表:BottomBar 跟手指同步缩下去(不是先滑列表再收 Bar)
- ✅ 滑到底继续上滑:Bar 完全藏起,MiniPlayer 顶到屏幕底部
- ✅ 向下滑:列表先滚;到顶后继续下滑 → Bar 展开
- ✅ 惯性结束半折叠 → 吸附到最近端点
- ✅ 列表滚回顶部停手 → 自动展开
- ✅ 折叠态点击 MiniPlayer → Bar 展开(而不是打开 NowPlaying)
已知坑 / 需要你在接入真实工程时注意
- 多 Tab 切换丢进度:你的 LibraryScreen 有 4 个 Tab,每个 Tab 一个 LazyColumn。
CollapsibleBottomBarState要提升到 Tab 宿主之上(Scaffold 层),所有子 LazyColumn 都挂在同一个nestedScroll(connection)之下。示例里是单层所以不用考虑。 - WindowInsets:Demo 没处理手势导航条。接入时 BottomBar 要用
Modifier.windowInsetsPadding(WindowInsets.navigationBars),否则折叠后会有一条透明的空隙。 - MiniPlayer 尺寸插值(可选增强):如果想让 MiniPlayer 在折叠时变小(像 Apple Music 那样),在
Box { miniPlayer { ... } }外再加个graphicsLayer { scaleX = 1f - 0.1f * state.progress; scaleY = ... },或者把 progress 传给 MiniPlayer 让它内部响应。 - 与 LargeTopAppBar 协同:如果你的 LibraryScreen 顶部是
LargeTopAppBar(scrollBehavior=...),两个NestedScrollConnection会同时接收事件,它们互不冲突(都读available.y),但展开/收起的方向相反——上滑时 TopAppBar 收起 + BottomBar 也收起,这正是我们想要的。 - 嵌套滚动的
onPreScroll返回值符号:Compose 的约定是Offset(x, y)里的y表示"已消耗的位移量,正值=向下"。我们消耗掉 dy 的一部分时返回Offset(0f, -consumed),因为consumed按我们的定义是正的(吃掉了多少"向上滚"的量)。跑的时候如果方向反了,把这里的符号翻过来。 onPreFling吸附:用户快速滑动松手后会走 fling,我们在 fling 前强制吸附。如果你想让 fling 的惯性也影响 Bar(像 iOS 那样),要改成onPostFling里消耗 velocity —— 但那会让行为变得更难预测,先用吸附方案。
下一步
跑通这个 Demo 后,把 CollapsibleBottomBarScaffold 替换到 MainActivity / AppNavigation 层:
- 外层:
CollapsibleBottomBarScaffold { padding, state -> NavHost(...) } bottomBar:你的 4-Tab NavigationBarminiPlayer:现有的MiniPlayer组件- 每个 Tab 的 LazyColumn 无需改动——nestedScroll 自动向上冒泡
只要 MainActivity 一处挂接,整个 app 都能享受折叠联动。
说些什么吧!