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

如何实现Jetpack Compose TabRow指示器随Pager实时滑动动画(仿WhatsApp)

Jetpack Compose实现WhatsApp风格TabRow实时滑动指示器

在Jetpack Compose中使用HorizontalPager搭配TabRow时,当前实现的指示器仅在页面切换完成后才触发动画,无法随Pager的轻微滑动实时跟随移动位置。需实现类似WhatsApp的TabRow平滑切换指示器效果,以下是现有实现代码、当前效果与期望效果对比:

现有实现代码

@Composable
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
fun TabRowHorizontalPager(
    pagerState: PagerState,
    fancyIndicator: @Composable (List<TabPosition>) -> Unit,
    titles: List<String>,
    coroutineScope: CoroutineScope,
    scrollBehavior: TopAppBarScrollBehavior,
    navController: NavHostController
) {
    TabRow(
        selectedTabIndex = pagerState.currentPage,
        containerColor = MaterialTheme.colorScheme.primary,
        contentColor = MaterialTheme.colorScheme.onPrimary,
        indicator = fancyIndicator,
    ) {
        titles.forEachIndexed { index, title ->
            Tab(
                selected = pagerState.currentPage == index,
                onClick = {
                    coroutineScope.launch {
                        pagerState.animateScrollToPage(index)
                    }
                },
                text = {
                    Text(
                        text = title,
                        maxLines = 2,
                        overflow = TextOverflow.Ellipsis
                    )
                }
            )
        }
    }
    HorizontalPager(
        pageCount = 3,
        state = pagerState,
        modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection)
    ) { tabIndex ->
        when (tabIndex) {
            0 -> ChatsTabContent(navController = navController)
            1 -> StatusTabContent(navController = navController)
            2 -> CallsTabContent(navController = navController)
        }
    }
}

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun IndicatorHome(
    index: PagerState,
    tabPositions: List<TabPosition>,
    tabColors: List<Color> = listOf()
) {
    val selectedTabIndex = index.currentPage
    val colors = tabColors.ifEmpty { listOf(MaterialTheme.colorScheme.surface) }
    val transition = updateTransition(selectedTabIndex, label = "")
    val indicatorStart by transition.animateDp(
        transitionSpec = {
            if (initialState < targetState) {
                spring(dampingRatio = 1f, stiffness = 50f)
            } else {
                spring(dampingRatio = 1f, stiffness = 1000f)
            }
        }, label = ""
    ) {
        tabPositions[it].left
    }

    val indicatorEnd by transition.animateDp(
        transitionSpec = {
            if (initialState < targetState) {
                spring(dampingRatio = 1f, stiffness = 1000f)
            } else {
                spring(dampingRatio = 1f, stiffness = 50f)
            }
        }, label = ""
    ) {
        tabPositions[it].right
    }

    val indicatorColor by transition.animateColor(label = "") {
        colors[it % colors.size]
    }

    FancyIndicator(
        indicatorColor,
        modifier = Modifier
            
            .fillMaxSize()
            .wrapContentSize(align = Alignment.BottomStart)
            .offset(x = indicatorStart, y = 0.dp)
            .width(indicatorEnd - indicatorStart)
    )
}

@Composable
fun FancyIndicator(indicatorColor: Color, modifier: Modifier) {
    TabRowDefaults.Indicator(modifier, color = indicatorColor, height = 4.dp)
}

当前效果

当前效果
指示器仅在页面切换完成后才会触发移动动画,无法响应Pager的实时滑动偏移。

期望效果

期望效果
实现WhatsApp风格的平滑指示器,随HorizontalPager的滑动实时跟随移动。

解决方案

问题根源在于原代码使用updateTransition监听currentPage,而currentPage仅在页面切换完成后才会更新。需改用PagerState的currentPageOffsetFraction获取实时滑动偏移比例,计算指示器的实时位置:

修改后的指示器代码

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun IndicatorHome(
    pagerState: PagerState,
    tabPositions: List<TabPosition>,
    tabColors: List<Color> = listOf()
) {
    val colors = tabColors.ifEmpty { listOf(MaterialTheme.colorScheme.surface) }
    val currentPage = pagerState.currentPage
    val pageOffset = pagerState.currentPageOffsetFraction

    // 计算指示器实时起始位置,处理最后一页边界情况
    val indicatorStart = if (currentPage < tabPositions.lastIndex) {
        tabPositions[currentPage].left + pageOffset * (tabPositions[currentPage + 1].left - tabPositions[currentPage].left)
    } else {
        tabPositions[currentPage].left
    }

    // 计算指示器实时结束位置,处理最后一页边界情况
    val indicatorEnd = if (currentPage < tabPositions.lastIndex) {
        tabPositions[currentPage].right + pageOffset * (tabPositions[currentPage + 1].right - tabPositions[currentPage].right)
    } else {
        tabPositions[currentPage].right
    }

    val indicatorColor = colors[currentPage % colors.size]

    FancyIndicator(
        indicatorColor,
        modifier = Modifier
            .fillMaxSize()
            .wrapContentSize(align = Alignment.BottomStart)
            .offset(x = indicatorStart, y = 0.dp)
            .width(indicatorEnd - indicatorStart)
    )
}

代码说明

  1. 移除updateTransition,直接通过pagerState.currentPage和currentPageOffsetFraction获取实时页面状态
  2. 根据当前页面位置 + 滑动偏移比例 × 相邻页面位置差,计算指示器的实时起始/结束位置
  3. 增加边界判断,避免最后一页时索引越界
  4. 指示器位置随Pager滑动实时更新,实现平滑跟随效果

内容的提问来源于stack exchange,提问作者Manish Prajapati

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.21 10:14:56