75

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 Answers 8

137

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

  
Sign up to request clarification or add additional context in comments.

6 Comments

Perfect, working on 1.0.0-beta5
Working Solution :)
How do you change the animation used in animateScrollToItem?
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?
Instead of coroutineScope you can launch via LaunchedEffect(true) { listState.animateScrollToItem(index = 10) }.
|
5

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.

Comments

4

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

Comments

3

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

Comments

3

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

1 Comment

This only works for the "first visible element". What if, for example, you need the lowest element? And how can you determine which element is “bottom” and which is “top”?
1

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

2 Comments

I would say that storing the lazy list state in the view model is not a good idea.
This further proofs that having any kind of Compose state in The view model is just lazy work :)
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++)
  }
}

Comments

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.