fellow developers! I’m transitioning from Swift to Jetpack Compose and I’m trying to replicate some behaviors that I’m used to from iOS development, specifically similar to what UIPickerView offers.
In Swift, using UIPickerView, it’s straightforward to create a picker where the centered item is automatically selected when the scrolling stops. I’m looking for a way to achieve a similar effect in Jetpack Compose, using LazyColumn, where the item that comes to the center of the view becomes the selected item.
- Swift image
enter image description here
Here are the challenges I’m facing:
Centering the Item: When a user taps on an item, I want this item to animate and come to the center of the LazyColumn. However, I’m encountering an issue where the wrong item is selected, or the item doesn’t center correctly in the viewport.
Automatic Selection: I need the item that scrolls into the center to be automatically selected and updated as the centered item.
I’ve been experimenting with LazyListState and animateScrollToItem to adjust the scroll position, but the results are not as expected. Below is a snippet of what I’ve tried:
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.launch
enum class HeightType(val unit: String) {
CM("cm"),
INCH("in");
companion object {
fun fromString(name: String): HeightType {
return when (name.lowercase()) {
"cm" -> CM
"in" -> INCH
else -> CM
}
}
}
}
@Composable
fun HeightView(
initialHeightType: HeightType = HeightType.CM,
initialHeight: Int = 170,
isEdit: Boolean = false,
onHeightSelected: (HeightType, Int) -> Unit = { _, _ -> },
onNextClicked: () -> Unit = {},
onBackClicked: () -> Unit = {}
) {
var heightType by remember { mutableStateOf(initialHeightType) }
var selectedHeight by remember { mutableStateOf(initialHeight) }
val cmList = remember { (132..228).toList() }
val inchList = remember { (52..90).toList() }
val heightList = if (heightType == HeightType.CM) cmList else inchList
val listState = rememberLazyListState()
val coroutineScope = rememberCoroutineScope()
val density = LocalDensity.current
LaunchedEffect(key1 = listState) {
snapshotFlow { Pair(listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset) }
.collect { (index, offset) ->
if (index < heightList.size) {
val centerItemIndex = (index + (listState.layoutInfo.visibleItemsInfo.size / 2))
val currentItem = heightList.getOrNull(centerItemIndex)
if (currentItem != null && selectedHeight != currentItem) {
selectedHeight = currentItem
onHeightSelected(heightType, selectedHeight)
}
}
}
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
.background(Color.DarkGray),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Height",
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
color = Color.White
)
Spacer(modifier = Modifier.height(16.dp))
HeightTypeSegmentControl(
heightType = heightType,
onHeightTypeChange = { newHeightType ->
heightType = newHeightType
selectedHeight = if (newHeightType == HeightType.CM) 170 else 67
coroutineScope.launch {
listState.scrollToItem(0)
}
onHeightSelected(newHeightType, selectedHeight)
}
)
Spacer(modifier = Modifier.height(16.dp))
Box(modifier = Modifier.height(200.dp).fillMaxWidth()) {
LazyColumn(
state = listState,
modifier = Modifier.align(Alignment.Center)
) {
itemsIndexed(heightList) { index, height ->
HeightPickerItem(
height = height,
heightType = heightType,
isSelected = height == selectedHeight,
onHeightSelected = {
selectedHeight = it
coroutineScope.launch {
val itemHeight = 48.dp
val offsetPx = (with(density) { itemHeight.toPx() }).toInt() * index -
with(density) { (200.dp / 2 - itemHeight / 2).roundToPx() }
listState.animateScrollToItem(index, offsetPx)
}
onHeightSelected(heightType, it)
}
)
}
}
Box(
modifier = Modifier
.fillMaxWidth()
.height(1.dp)
.align(Alignment.Center)
.background(Color.Red)
)
}
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = onNextClicked,
modifier = Modifier
.fillMaxWidth()
.height(48.dp)
.background(Color.Blue),
colors = ButtonDefaults.buttonColors(containerColor = Color.Blue)
) {
Text(
text = if (isEdit) "Save" else "Next",
fontSize = 18.sp,
color = Color.White
)
}
}
}
@Composable
fun HeightTypeSegmentControl(
heightType: HeightType,
onHeightTypeChange: (HeightType) -> Unit
) {
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth()
) {
Button(
onClick = { onHeightTypeChange(HeightType.CM) },
colors = ButtonDefaults.buttonColors(
containerColor = if (heightType == HeightType.CM) Color.Blue else Color.Gray
),
modifier = Modifier.weight(1f)
) {
Text("cm", color = Color.White)
}
Spacer(modifier = Modifier.width(8.dp))
Button(
onClick = { onHeightTypeChange(HeightType.INCH) },
colors = ButtonDefaults.buttonColors(
containerColor = if (heightType == HeightType.INCH) Color.Blue else Color.Gray
),
modifier = Modifier.weight(1f)
) {
Text("in", color = Color.White)
}
}
}
@Composable
fun HeightPickerItem(
height: Int,
heightType: HeightType,
isSelected: Boolean,
onHeightSelected: (Int) -> Unit
) {
Text(
text = "$height ${if (heightType == HeightType.CM) "cm" else "in"}",
fontSize = if (isSelected) 24.sp else 18.sp,
color = if (isSelected) Color.Yellow else Color.White,
modifier = Modifier
.fillMaxWidth()
.clickable { onHeightSelected(height) }
.padding(vertical = 8.dp),
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal
)
}
@Preview(showBackground = true, widthDp = 320, heightDp = 640)
@Composable
fun PreviewHeightView() {
HeightView()
}
Any help or guidance on how to fix this behavior would be greatly appreciated. Thank you!
history workout is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.