Youb
Youb

Reputation: 215

How can I make the TabRow indicator scroll in sync with the HorizontalPager?

I'm trying to achieve a simple thing: I have a ScrollableTabRow and a HorizontalPager and I want to make the indicator of the ScrollableTabRow to move when I drag the HorizontalPager left or right.

Right now, it moves only when I change the page.

It works out of the box with XML, but now I feel that I have to do this, but I don't know what.

Code:

      ScrollableTabRow(selectedTabIndex = selectedTabIndex.value,
            indicator = { positions ->
                TabRowDefaults.SecondaryIndicator(
                    Modifier.tabIndicatorOffset(positions[pagerState.currentPage])
                )
            },
            modifier = Modifier.fillMaxWidth(),
            containerColor = getTopBarColor()) {
            data.forEachIndexed { index, _ ->
                Tab(
                    text = { Text(tabTitle, maxLines = 1) },
                    selected = selectedTabIndex.value == index,
                    onClick = {
                        scope.launch {
                            pagerState.animateScrollToPage(index)
                        }
                    }
                )
            }
        }
        HorizontalPager(state = pagerState, modifier = Modifier
            .fillMaxWidth()
            .weight(1f)) { page ->
            // Show the page
        }
    ....

Upvotes: 1

Views: 688

Answers (1)

BenjyTec
BenjyTec

Reputation: 10777

You can use the following code:

@Composable
fun TabbedPager() {

    val tabs = listOf("Videos", "Shorts", "Podcasts", "Courses", "Playlists", "Community")

    val coroutineScope = rememberCoroutineScope()
    val pagerState = rememberPagerState(
        initialPage = 0,
        pageCount = { tabs.size }
    )
    val selectedTabIndex by remember { derivedStateOf { pagerState.currentPage }}
    var previousTabIndex by remember { mutableIntStateOf(0) }
    var targetTabIndex by remember { mutableIntStateOf(0) }

    LaunchedEffect(pagerState.currentPageOffsetFraction) {
        val scrollFraction = pagerState.currentPageOffsetFraction
        if (scrollFraction > 0) {
            previousTabIndex = pagerState.currentPage
            targetTabIndex = previousTabIndex + 1
        }
        if (scrollFraction < 0) {
            previousTabIndex = pagerState.currentPage
            targetTabIndex = previousTabIndex - 1
        }
    }

    Scaffold(
        modifier = Modifier.fillMaxSize(),
        topBar = {
            ScrollableTabRow(
                selectedTabIndex = selectedTabIndex,
                edgePadding = 0.dp,
                indicator = { tabPositions ->
                    TabRowDefaults.SecondaryIndicator(
                        Modifier.smoothTabIndicatorOffset(
                            previousTabPosition = tabPositions[previousTabIndex],
                            newTabPosition = tabPositions[targetTabIndex],
                            swipeProgress = pagerState.currentPageOffsetFraction
                        )
                    )
                }
            ) {
                tabs.forEachIndexed { index, title ->
                    Tab(
                        text = { Text(title) },
                        selected = selectedTabIndex == index,
                        onClick = {
                            coroutineScope.launch {
                                pagerState.animateScrollToPage(index)
                            }
                        },
                    )
                }
            }
        }
    ) { innerPadding ->
        HorizontalPager(
            modifier = Modifier
                .padding(innerPadding)
                .fillMaxSize(),
            state = pagerState
        ) { pageIndex ->
            Column(
                modifier = Modifier.fillMaxSize(),
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                Text("Page $pageIndex")
            }
        }
    }
}

fun Modifier.smoothTabIndicatorOffset(previousTabPosition: TabPosition, newTabPosition: TabPosition, swipeProgress: Float): Modifier =
    composed {
        val currentTabWidth = previousTabPosition.width + (newTabPosition.width - previousTabPosition.width) * abs(swipeProgress)
        val indicatorOffset = previousTabPosition.left + (newTabPosition.left - previousTabPosition.left) * abs(swipeProgress)
        fillMaxWidth()
            .wrapContentSize(Alignment.BottomStart)
            .offset { IntOffset(x = indicatorOffset.roundToPx(), y = 0) }
            .width(currentTabWidth)
    }

Output:

Screen Recording

Upvotes: 0

Related Questions