Jetpack Compose LazyColumn programmatically scroll to Item
F

8

66

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.

Fetter answered 29/11, 2020 at 21:18 Comment(0)
E
117

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")
}

  
Erepsin answered 19/3, 2021 at 17:44 Comment(5)
Perfect, working on 1.0.0-beta5Tieratierce
Working Solution :)Hedley
How do you change the animation used in animateScrollToItem?Mulderig
Neither function does anything for me, with Compose 1.2.0. They seem to get stuck internally in waitForFirstLayout, which never continues. Any idea what could be wrong?Boneset
Instead of coroutineScope you can launch via LaunchedEffect(true) { listState.animateScrollToItem(index = 10) }.Huelva
S
4

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.

Session answered 30/11, 2020 at 1:19 Comment(0)
B
2

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)
        }
    }
}
Braxton answered 25/2, 2021 at 8:41 Comment(0)
T
2

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)
}
Tocci answered 10/3, 2021 at 20:50 Comment(0)
S
1

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) }
}
Stinky answered 5/3 at 20:37 Comment(0)
A
0

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++)
  }
}
Aloin answered 6/4, 2021 at 7:43 Comment(0)
F
0

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)
    }
}
Faitour answered 4/6, 2023 at 19:38 Comment(2)
I would say that storing the lazy list state in the view model is not a good idea.Midkiff
This further proofs that having any kind of Compose state in The view model is just lazy work :)Awl
M
0

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
            )
        }
    }
}
Meijer answered 4/9, 2023 at 17:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.