Compose Multiplatform中基于Voyager实现底部TabBar滚动显隐
实现方案
1. 定义CompositionLocal共享TabBar状态
创建CompositionLocal,用于在Application与HomeScreen之间传递TabBar的显示状态:
import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.mutableStateOf val LocalTabBarVisibility = compositionLocalOf<MutableState<Boolean>> { error("LocalTabBarVisibility must be provided") }
2. 修改Application Screen控制TabBar显隐
在Application的Content中维护TabBar显示状态,添加平滑动画,并通过CompositionLocal提供状态更新能力,同时处理切换Tab时恢复显示的逻辑:
@OptIn(ExperimentalMaterial3Api::class) @Composable override fun Content() { var isTabBarVisible by remember { mutableStateOf(true) } val tabNavigator = LocalTabNavigator.current // 切换Tab时强制显示TabBar LaunchedEffect(tabNavigator.current) { isTabBarVisible = true } // 平滑显隐动画 val bottomBarAlpha by animateFloatAsState(if (isTabBarVisible) 1f else 0f) val bottomBarOffset by animateDpAsState(if (isTabBarVisible) 0.dp else 56.dp) // 56dp为NavigationBar默认高度 Scaffold( scaffoldState = rememberScaffoldState(), bottomBar = { if (isTabBarVisible || bottomBarAlpha > 0) { NavigationBar( tonalElevation = 4.dp, modifier = Modifier .alpha(bottomBarAlpha) .translate(y = bottomBarOffset) ) { TabNavigationItemM3(HomeTab) TabNavigationItemM3(FavoritesTab) TabNavigationItemM3(InterestsTab) TabNavigationItemM3(ProfileTab) } } }, ) { paddingValues -> CompositionLocalProvider( LocalTabBarVisibility provides remember { mutableStateOf(isTabBarVisible) } ) { CurrentTab(modifier = Modifier.padding(paddingValues)) } } }
3. 传递状态更新回调到HomeScreen
修改HomeTab的Content,从CompositionLocal获取状态,将更新逻辑传递给HomeScreen:
object HomeTab : Tab { @Composable override fun Content() { val tabBarVisibility = LocalTabBarVisibility.current HomeScreen( onScrollDirectionChanged = { shouldShow -> tabBarVisibility.value = shouldShow }, onScrollStopped = {} ) } override val options: TabOptions @Composable get() { val icon = rememberVectorPainter(Icons.Rounded.Home) return remember { TabOptions(index = 0u, title = "Home", icon = icon) } } }
4. 优化HomeScreen滚动监听逻辑
修正滚动方向判断逻辑,确保准确识别上下滚动,并处理滚动停止后的状态:
@Composable fun HomeScreen( screenModel: HomeScreenViewModel = koinInject(), navigator: Navigator = LocalNavigator.currentOrThrow, onScrollDirectionChanged: (Boolean) -> Unit, onScrollStopped: () -> Unit = {} ) { LaunchedEffect(Unit) { screenModel.onLaunch() } val pagingItems = screenModel.allMoviesPagingState.collectAsLazyPagingItemsForRecipe(screenModel::onLoadMore) val trendingPagingItems = screenModel.trendingMoviesPagingState.collectAsLazyPagingItemsForMovies(screenModel::onLoadMoreTrending) val listState = rememberLazyListState() var previousIndex by remember { mutableStateOf(listState.firstVisibleItemIndex) } var previousOffset by remember { mutableStateOf(listState.firstVisibleItemScrollOffset) } LaunchedEffect(listState) { snapshotFlow { listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset }.collect { (currentIndex, currentOffset) -> // 判断滚动方向:向下滚动返回true,向上返回false val isScrollingDown = when { currentIndex > previousIndex -> true currentIndex < previousIndex -> false else -> currentOffset > previousOffset } // 向上滚动时显示TabBar,向下滚动时隐藏 onScrollDirectionChanged(!isScrollingDown) previousIndex = currentIndex previousOffset = currentOffset // 滚动停止时,若在列表顶部则强制显示TabBar if (!listState.isScrollInProgress) { onScrollStopped() if (listState.firstVisibleItemIndex == 0 && listState.firstVisibleItemScrollOffset == 0) { onScrollDirectionChanged(true) } } } } Box { LazyColumn( state = listState, horizontalAlignment = Alignment.Start, verticalArrangement = Arrangement.Top ) { trendingMovies(trendingPagingItems, { id -> navigator.push(DetailsScreen(id)) }, {}) allMovies(pagingItems, { id -> navigator.push(DetailsScreen(id)) }, {}) } MoviesSearchBar { } } }
优化建议
- 动画体验优化:结合
animateContentSize让Scaffold内容区域自动适配TabBar显隐,避免内容被遮挡。 - 状态防抖:在滚动监听流中添加
debounce操作符,减少频繁状态更新导致的重绘。 - 边界处理:当列表滚动到底部时,强制显示TabBar,避免用户无法切换Tab。
- 状态重置:确保切换到其他Tab时,TabBar立即恢复显示,避免异常状态延续。
内容的提问来源于stack exchange,提问作者Madhan Shah
相关产品推荐
相关产品推荐

