Jetpack Compose LazyColumn programmatically scroll to Item

53.3k views Asked by At

Is there any way to programmatically scroll LazyColumn to some item in the list? I thought that it can be done by hoisting the LazyColumn argument state: LazyListState = rememberLazyListState() but I have no idea how I can change this state e.g. on Button click.

8

There are 8 answers

5
Gabriele Mariotti On BEST ANSWER

The LazyListState supports the scroll position via

Something like:

val listState = rememberLazyListState()
// Remember a CoroutineScope to be able to launch
val coroutineScope = rememberCoroutineScope()

LazyColumn(state = listState) {
    // ...
}

Button (
    onClick = { 
        coroutineScope.launch {
            // Animate scroll to the 10th item
            listState.animateScrollToItem(index = 10)
        }
    }
){
    Text("Click")
}

  
0
Muthukrishnan Rajendran On

In Compose 1.0.0-alpha07, There is no public API, But some internal API is there to LazyListState#snapToItemIndex.

/**
 * Instantly brings the item at [index] to the top of the viewport, offset by [scrollOffset]
 * pixels.
 *
 * Cancels the currently running scroll, if any, and suspends until the cancellation is
 * complete.
 *
 * @param index the data index to snap to
 * @param scrollOffset the number of pixels past the start of the item to snap to
 */
@OptIn(ExperimentalFoundationApi::class)
suspend fun snapToItemIndex(
    @IntRange(from = 0)
    index: Int,
    @IntRange(from = 0)
    scrollOffset: Int = 0
) = scrollableController.scroll {
    scrollPosition.update(
        index = DataIndex(index),
        scrollOffset = scrollOffset,
        // `true` will be replaced with the real value during the forceRemeasure() execution
        canScrollForward = true
    )
    remeasurement.forceRemeasure()
}

Maybe in the upcoming release, we can see the updates.

0
Umesh Chhabra On

Here is my code that makes sticky headers, list and scroll

@ExperimentalFoundationApi
@Composable
private fun ListView(data: YourClass) { 

//this is to remember state, internal API also use the same
    val state = rememberLazyListState()

    LazyColumn(Modifier.fillMaxSize(), state) {
        itemsIndexed(items = data.list, itemContent = { index, dataItem ->
            ItemView(dataItem)// your row
        })
       // header after some data, according to your condition
            stickyHeader {
                ItemDecoration()// compose fun for sticky header 
            }
// More items after header
            itemsIndexed(items = data.list2, itemContent = { index, dataItem ->
            ItemView(dataItem)// your row
        })
        }

       // scroll to top
      // I am scrolling to top every time data changes, use accordingly
        CoroutineScope(Dispatchers.Main).launch {
            state.snapToItemIndex(0, 0)
        }
    }
}
0
alekshandru On

try with

lazyListState.animateScrollToItem(lazyListState.firstVisibleItemIndex)


@Composable
fun CircularScrollList(
    value: Long,
    onValueChange: () -> Unit = {}
) {
    val lazyListState = rememberLazyListState()
    val scope = rememberCoroutineScope()

    val items = CircularAdapter(listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9))
    scope.launch { lazyListState.scrollToItem(items.midIndex()) }

    LazyColumn(
        modifier = Modifier
            .requiredHeight(height = 120.dp)
            .border(width = 1.dp, color = Color.Black),
        state = lazyListState,
    ) {
        items(items) {
            if (!lazyListState.isScrollInProgress) {
                scope.launch {
                    lazyListState.animateScrollToItem(lazyListState.firstVisibleItemIndex)
                }
            }
            Text(
                text = "$it",
                modifier = Modifier.requiredHeight(40.dp),
                style = TextStyle(fontSize = 30.sp)
            )
        }
    }
}

class CircularAdapter(
    private val content: List<Int>
) : List<Int> {
    fun midIndex(): Int = Int.MAX_VALUE / 2 + 6
    override val size: Int = Int.MAX_VALUE
    override fun contains(element: Int): Boolean = content.contains(element = element)
    override fun containsAll(elements: Collection<Int>): Boolean = content.containsAll(elements)
    override fun get(index: Int): Int = content[index % content.size]
    override fun indexOf(element: Int): Int = content.indexOf(element)
    override fun isEmpty(): Boolean = content.isEmpty()
    override fun iterator(): Iterator<Int> = content.iterator()
    override fun lastIndexOf(element: Int): Int = content.lastIndexOf(element)
    override fun listIterator(): ListIterator<Int> = content.listIterator()
    override fun listIterator(index: Int): ListIterator<Int> = content.listIterator(index)
    override fun subList(fromIndex: Int, toIndex: Int): List<Int> =
        content.subList(fromIndex, toIndex)
}
0
CHATAM_ On

I have solved 2 way

one way: I choosed this best way

LaunchedEffect(true) {
  repeat(Int.MAX_VALUE) {
      delay(TIME_DELAY_BANNER)
      pagerState.animateScrollToPage(page = it % pagerState.pageCount)
  }
}

two way:

var index = pagerState.currentPage
LaunchedEffect(true) {
            while (true) {
                delay(TIME_DELAY_BANNER)
                if (index == pagerState.pageCount) {
                    index = 0
                }
      pagerState.animateScrollToPage(page = index++)
  }
}
2
Pavel Jelínek On

This is working solution if you want to do this in viewModel. You probably want to use delay when you call it right after recomposition call:

private val _lazyColumnScrollState = MutableStateFlow(LazyListState())
val lazyColumnScrollState get() = _lazyColumnScrollState.asStateFlow()

private fun scrollUp(itemIndex: Int) {
    viewModelScope.launch {
        // Delay: need to wait for the content recomposition
        delay(250)
        _lazyColumnScrollState.value.scrollToItem(itemIndex)
    }
}
0
Gilvan Gomes On

If someone needs to do this without any user interaction, you can listen for the LazyList's loadState. Google has recommended it here.

In my case, I needed to scroll to the top after prepending items. Because I use a Mediator, just filtering by the "main" loadState.prepend wasn't enough, so this is what worked for me. It should work for other similar operations as well.

LaunchedEffect(listState) {
    snapshotFlow { content.loadState }
        // We are only interested on the top items being added (prepend operation)
        .distinctUntilChangedBy { it.prepend }
        // These may differ and update at different times, so we want to make sure all of them
        // are updated to NotLoading before proceeding
        .filter {
            it.prepend is LoadState.NotLoading &&
                it.source.prepend is LoadState.NotLoading &&
                it.mediator?.prepend is LoadState.NotLoading
        }
        .collect { listState.scrollToItem(0) }
}
0
EGH On

I solved it like this

@Composable
fun LazyColumDeItemLineaEstIyF(
    listadoDeParadas: List<LineasYIndice>,
    onConfirm: (Int) -> Unit,
    preselectedPos: Int,
     modifier: Modifier = Modifier
) {
    val listState = rememberLazyListState()
    val coroutineScope = rememberCoroutineScope()
    var unaVez=true

    LaunchedEffect(unaVez) {
        coroutineScope.launch {
            listState.animateScrollToItem(index = preselectedPos)
            unaVez=false
        }
    }

    LazyColumn(
        state=listState,
        verticalArrangement = Arrangement.spacedBy(0.dp),
        contentPadding = PaddingValues(
            horizontal = 1.dp,
            vertical =1.dp
        ),
    ) {
        items(
            items= listadoDeParadas,
        ) { item ->
            ItemLineaGM(
                item= item,
                selected = (item.id== preselectedPos),
                onConfirm=onConfirm
            )
        }
    }
}