Reputation: 301
How can I calculate the position of the selected item in a LazyRow and use it for centering the item on the screen? The items will have various sizes depending on their content.
If I go with lazyListState.animateScrollToItem(index)
the selected item will be positioned to the left in the LazyRow.
What I want to achieve is to have the selected item centered like this
The code I have currently:
@Composable
fun Test() {
Column {
TestRow(listOf("Angola", "Bahrain", "Afghanistan", "Denmark", "Egypt", "El Salvador", "Fiji", "Japan", "Kazakhstan", "Kuwait", "Laos", "Mongolia"))
TestRow(listOf("Angola", "Bahrain", "Afghanistan"))
}
}
@OptIn(ExperimentalSnapperApi::class)
@Composable
fun TestRow(items: List<String>) {
val selectedIndex = remember { mutableStateOf(0) }
val lazyListState = rememberLazyListState()
val coroutineScope = rememberCoroutineScope()
LazyRow(
modifier = Modifier.fillMaxWidth(),
state = lazyListState,
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally),
contentPadding = PaddingValues(horizontal = 12.dp),
flingBehavior = rememberSnapperFlingBehavior(
lazyListState = lazyListState,
snapOffsetForItem = SnapOffsets.Start
)
) {
itemsIndexed(items) { index, item ->
TestItem(
content = item,
isSelected = index == selectedIndex.value,
onClick = {
selectedIndex.value = index
coroutineScope.launch {
lazyListState.animateScrollToItem(index)
}
}
)
}
}
}
@Composable
fun TestItem(
content: String,
isSelected: Boolean,
onClick: () -> Unit
) {
Button(
modifier = Modifier.height(40.dp),
colors = ButtonDefaults.buttonColors(
backgroundColor = if (isSelected) Color.Green else Color.Yellow,
contentColor = Color.Black
),
elevation = null,
shape = RoundedCornerShape(5.dp),
contentPadding = PaddingValues(0.dp),
onClick = onClick
) {
Text(
modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp),
text = (content).uppercase(),
)
}
}
The code horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally)
is to make sure the LazyRow centers all the items, when there is not enough items for scrolling.
Example with 3 items:
I have tried various suggestions posted at similar questions, but with no luck in finding a solution. jetpack-compose-lazylist-possible-to-zoom-the-center-item how-to-focus-a-invisible-item-in-lazycolumn-on-jetpackcompose /jetpack-compose-make-the-first-element-in-a-lazyrow-be-aligned-to-the-center-o
Upvotes: 17
Views: 7385
Reputation: 235
Here is a clean solution:
suspend fun LazyListState.animateScrollToItemCenter(index: Int) {
layoutInfo.resolveItemOffsetToCenter(index)?.let {
animateScrollToItem(index, it)
return
}
scrollToItem(index)
layoutInfo.resolveItemOffsetToCenter(index)?.let {
animateScrollToItem(index, it)
}
}
private fun LazyListLayoutInfo.resolveItemOffsetToCenter(index: Int): Int? {
val itemInfo = visibleItemsInfo.firstOrNull { it.index == index } ?: return null
val containerSize = viewportSize.width - beforeContentPadding - afterContentPadding
return -(containerSize - itemInfo.size) / 2
}
And use it like this:
val lazyListState = rememberLazyListState()
LaunchedEffect(selectedIndex) {
lazyListState.animateScrollToItemCenter(selectedIndex)
}
// Or use rememberCoroutineScope() instead
Upvotes: 2
Reputation: 4956
Here is the version that works on off-screen items. Because the animation is fast, the flash is hard to notice.
A more perfect but complex solution is to use scroll { ... }
to control the whole animation as animateScrollToItem
does.
suspend fun LazyListState.centerItem(index: Int) {
suspend fun locateTarget(): Boolean {
val layoutInfo = layoutInfo
val containerSize =
layoutInfo.viewportSize.width - layoutInfo.beforeContentPadding - layoutInfo.afterContentPadding
val target = layoutInfo.visibleItemsInfo.firstOrNull { it.index == index }
?: return false
val targetOffset = containerSize / 2f - target.size / 2f
animateScrollBy(target.offset - targetOffset)
return true
}
if (!locateTarget()) {
val visibleItemsInfo = layoutInfo.visibleItemsInfo
val currentIndex = visibleItemsInfo.getOrNull(visibleItemsInfo.size / 2)?.index ?: -1
scrollToItem(
if (index > currentIndex) {
(index - visibleItemsInfo.size + 1)
} else {
index
}.coerceIn(0, layoutInfo.totalItemsCount)
)
locateTarget()
}
}
Upvotes: 2
Reputation: 61
Improving both Soares and Markman answers you can reference the methods without this for readability and make the function suspend instead of passing a coroutine scope.
suspend fun LazyListState.animateScrollAndCentralizeItem(index: Int) {
val itemInfo = this.layoutInfo.visibleItemsInfo.firstOrNull { it.index == index }
if (itemInfo != null) {
val center = layoutInfo.viewportEndOffset / 2
val childCenter = itemInfo.offset + itemInfo.size / 2
animateScrollBy((childCenter - center).toFloat())
} else {
animateScrollToItem(index)
}
}
Then you can just call it as you'd normally call animateScrollToItem
scope.launch {
lazyListState.animateScrollAndCentralizeItem(index)
}
Upvotes: 4
Reputation: 301
I found a solution that fits my needs, since I don't expect to navigate to a selected item, without being able to see it.
This solution can't center items that is not visible on the screen.
coroutineScope.launch {
val itemInfo = lazyListState.layoutInfo.visibleItemsInfo.firstOrNull { it.index == index }
if (itemInfo != null) {
val center = lazyListState.layoutInfo.viewportEndOffset / 2
val childCenter = itemInfo.offset + itemInfo.size / 2
lazyListState.animateScrollBy((childCenter - center).toFloat())
} else {
lazyListState.animateScrollToItem(index)
}
}
Hope t
Upvotes: 11
Reputation: 157
Improving the @Markram answer
You can create an extension of "LazyListState":
fun LazyListState.animateScrollAndCentralizeItem(index: Int, scope: CoroutineScope) {
val itemInfo = this.layoutInfo.visibleItemsInfo.firstOrNull { it.index == index }
scope.launch {
if (itemInfo != null) {
val center = [email protected] / 2
val childCenter = itemInfo.offset + itemInfo.size / 2
[email protected]((childCenter - center).toFloat())
} else {
[email protected](index)
}
}
}
And then just use it directly in your Composable:
val coroutineScope = rememberCoroutineScope()
coroutineScope.launch {
listState.animateScrollAndCentralizeItem(index , this)
}
Upvotes: 7