Auto scrolling pager not working properly in Android Jetpack Compose

1.7k Views Asked by At

I am learning jetpack compose.I am trying to implement a viewpager in jetpack compose where 5 image will be auto scrolled after 3 sec just like a carousel banner.Everything is alright before last index item image.After auto scroll to last index ,page should be scrolled to 0 index and will repeat.That's where the problem begain.The pager not working perfectly here .It's reapeting 3-4 index and sometimes stuck between to image/page after first auto scroll. This is the img

My Code


@OptIn(ExperimentalPagerApi::class)
@Composable
fun HorizontalPagerScreen() {
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .padding(0.dp, 5.dp)
    ) {
        val items = createItems()
        val pagerState = rememberPagerState()

        HorizontalPager(
            modifier = Modifier
                .fillMaxWidth()
                .height(250.dp),
            count = items.size,
            state = pagerState,
            verticalAlignment = Alignment.Top,

            ) { currentPage ->
            Image(
                painter = rememberAsyncImagePainter(items[currentPage].Image),
                contentDescription = items[currentPage].title,
                contentScale = ContentScale.Crop,
                modifier = Modifier.fillMaxWidth(),
            )


            //Here's the code for auto scrolling 

            LaunchedEffect(key1= Unit, key2= pagerState.currentPage) {
                while (true) {
                    yield()
                    delay(3000)
                    var newPage = pagerState.currentPage + 1
                    if (newPage > items.lastIndex) newPage = 0
                    pagerState.animateScrollToPage(newPage)
                }
            }
        }
    }
}

**How to make it auto scroll for infinite times **

2

There are 2 best solutions below

0
riggaroo On

You can create a loopingCount variable that you increment every few seconds using a LaunchedEffect and then mod it with the max amount of pages, you also need to take into account if the user is dragging on the pager or not.

The full code sample can be found here, but added below too:

@Composable
fun HorizontalPagerLoopingIndicatorSample() {
    Scaffold(
        modifier = Modifier.fillMaxSize()
    ) { padding ->
        Column(
            Modifier
                .fillMaxSize()
                .padding(padding)
        ) {
            // Display 10 items
            val pageCount = 10

            // We start the pager in the middle of the raw number of pages
            val loopingCount = Int.MAX_VALUE
            val startIndex = loopingCount / 2
            val pagerState = rememberPagerState(initialPage = startIndex)

            fun pageMapper(index: Int): Int {
                return (index - startIndex).floorMod(pageCount)
            }

            HorizontalPager(
                // Set the raw page count to a really large number
                pageCount = loopingCount,
                state = pagerState,
                // Add 32.dp horizontal padding to 'center' the pages
                contentPadding = PaddingValues(horizontal = 32.dp),
                // Add some horizontal spacing between items
                pageSpacing = 4.dp,
                modifier = Modifier
                    .weight(1f)
                    .fillMaxWidth()
            ) { index ->
                // We calculate the page from the given index
                val page = pageMapper(index)
                PagerSampleItem(
                    page = page,
                    modifier = Modifier
                        .fillMaxWidth()
                        .aspectRatio(1f)
                )
            }
            HorizontalPagerIndicator(
                pagerState = pagerState,
                modifier = Modifier
                    .align(Alignment.CenterHorizontally)
                    .padding(16.dp),
                pageCount = pageCount,
                pageIndexMapping = ::pageMapper
            )

            val loopState = remember {
                mutableStateOf(true)
            }

            LoopControl(loopState, Modifier.align(Alignment.CenterHorizontally))

            ActionsRow(
                pagerState = pagerState,
                modifier = Modifier.align(Alignment.CenterHorizontally),
                infiniteLoop = true
            )

            var underDragging by remember {
                mutableStateOf(false)
            }

            LaunchedEffect(key1 = Unit) {
                pagerState.interactionSource.interactions.collect { interaction ->
                    when (interaction) {
                        is PressInteraction.Press -> underDragging = true
                        is PressInteraction.Release -> underDragging = false
                        is PressInteraction.Cancel -> underDragging = false
                        is DragInteraction.Start -> underDragging = true
                        is DragInteraction.Stop -> underDragging = false
                        is DragInteraction.Cancel -> underDragging = false
                    }
                }
            }

            val looping = loopState.value
            if (underDragging.not() && looping) {
                LaunchedEffect(key1 = underDragging) {
                    try {
                        while (true) {
                            delay(1000L)
                            val current = pagerState.currentPage
                            val currentPos = pageMapper(current)
                            val nextPage = current + 1
                            if (underDragging.not()) {
                                val toPage = nextPage.takeIf { nextPage < pageCount } ?: (currentPos + startIndex + 1)
                                if (toPage > current) {
                                    pagerState.animateScrollToPage(toPage)
                                } else {
                                    pagerState.scrollToPage(toPage)
                                }
                            }
                        }
                    } catch (e: CancellationException) {
                        Log.i("page", "Launched paging cancelled")
                    }
                }
            }
        }
    }
}

@Composable
fun LoopControl(
    loopState: MutableState<Boolean>,
    modifier: Modifier = Modifier,
) {
    IconButton(
        onClick = { loopState.value = loopState.value.not() },
        modifier = modifier
    ) {
        val icon = if (loopState.value) {
            Icons.Default.PauseCircle
        } else {
            Icons.Default.PlayCircle
        }
        Icon(imageVector = icon, contentDescription = null)
    }
}

private fun Int.floorMod(other: Int): Int = when (other) {
    0 -> this
    else -> this - floorDiv(other) * other
}

0
Eury Pérez Beltré On

Here's a simple composable that will help you achieve auto-play:

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun CarouselAutoPlayHandler(
    pagerState: PagerState,
    carouselSize: Int
) {
    var pageKey by remember { mutableStateOf(0) }

    val effectFlow = rememberFlowWithLifecycle(pagerState.interactionSource.interactions)

    LaunchedEffect(effectFlow) {
        effectFlow.collectLatest {
            if (it is DragInteraction.Stop) pageKey++
        }
    }

    LaunchedEffect(pageKey) {
        delay(5000)
        val newPage = (pagerState.currentPage + 1) % carouselSize
        pagerState.animateScrollToPage(newPage)
        pageKey++
    }
}

Explanation:

  1. Pass the pager state and the amount of items
  2. Create a new key and set the initial value as 0 (the value here doesn't matter)
  3. Remember the flow returned from the interactions property in the pager state (You can find the implementation in this article I wrote)
  4. Collect the flow and whenever there's a drag stop, add 1 to the page key so the LaunchedEffect below is restarted
  5. In another LaunchedEffect set the pageKey as the key, add whatever your delay is and after that add the logic to scroll to the next page and change the pageKey to restart the LaunchedEffect once again

The code in the first LaunchedEffect will restart the time whenever the user manually drags the carousel.

You can pass the delay value as a variable, just kept it there hardcoded for the example.