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
The LazyListState supports the scroll position via
- the
scrollToItem()function, which ‘immediately’ snaps the scroll position, animateScrollToItem()which scrolls using an animation
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")
}
6 Comments
coroutineScope you can launch via LaunchedEffect(true) { listState.animateScrollToItem(index = 10) }.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
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
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
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 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 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
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
)
}
}
}