You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 04:47:33