What is the recommended solution for creating a NumberPicker Widget in Jetpack Compose? Similar to the image below. I am able to create an NumberPicker using an AndroidView within my composable but the view does not seem to allow flings or snap to position. Btw the UI below shows three NumberPickers placed in a row. It is not supposed to represent a DatePicker

I implemented a NumberPicker in Jetpack Compose (without using AndroidView): https://gist.github.com/nhcodes/dc68c65ee586628fda5700911e44543f
Picker.kt
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun Picker(
items: List<String>,
state: PickerState = rememberPickerState(),
modifier: Modifier = Modifier,
startIndex: Int = 0,
visibleItemsCount: Int = 3,
textModifier: Modifier = Modifier,
textStyle: TextStyle = LocalTextStyle.current,
dividerColor: Color = LocalContentColor.current,
) {
val visibleItemsMiddle = visibleItemsCount / 2
val listScrollCount = Integer.MAX_VALUE
val listScrollMiddle = listScrollCount / 2
val listStartIndex = listScrollMiddle - listScrollMiddle % items.size - visibleItemsMiddle + startIndex
fun getItem(index: Int) = items[index % items.size]
val listState = rememberLazyListState(initialFirstVisibleItemIndex = listStartIndex)
val flingBehavior = rememberSnapFlingBehavior(lazyListState = listState)
val itemHeightPixels = remember { mutableStateOf(0) }
val itemHeightDp = pixelsToDp(itemHeightPixels.value)
val fadingEdgeGradient = remember {
Brush.verticalGradient(
0f to Color.Transparent,
0.5f to Color.Black,
1f to Color.Transparent
)
}
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex }
.map { index -> getItem(index + visibleItemsMiddle) }
.distinctUntilChanged()
.collect { item -> state.selectedItem = item }
}
Box(modifier = modifier) {
LazyColumn(
state = listState,
flingBehavior = flingBehavior,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.height(itemHeightDp * visibleItemsCount)
.fadingEdge(fadingEdgeGradient)
) {
items(listScrollCount) { index ->
Text(
text = getItem(index),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = textStyle,
modifier = Modifier
.onSizeChanged { size -> itemHeightPixels.value = size.height }
.then(textModifier)
)
}
}
Divider(
color = dividerColor,
modifier = Modifier.offset(y = itemHeightDp * visibleItemsMiddle)
)
Divider(
color = dividerColor,
modifier = Modifier.offset(y = itemHeightDp * (visibleItemsMiddle + 1))
)
}
}
private fun Modifier.fadingEdge(brush: Brush) = this
.graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)
.drawWithContent {
drawContent()
drawRect(brush = brush, blendMode = BlendMode.DstIn)
}
@Composable
private fun pixelsToDp(pixels: Int) = with(LocalDensity.current) { pixels.toDp() }
PickerState.kt
@Composable
fun rememberPickerState() = remember { PickerState() }
class PickerState {
var selectedItem by mutableStateOf("")
}
PickerExample.kt
@Composable
fun PickerExample() {
Surface(modifier = Modifier.fillMaxSize()) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxSize()
) {
val values = remember { (1..99).map { it.toString() } }
val valuesPickerState = rememberPickerState()
val units = remember { listOf("seconds", "minutes", "hours") }
val unitsPickerState = rememberPickerState()
Text(text = "Example Picker", modifier = Modifier.padding(top = 16.dp))
Row(modifier = Modifier.fillMaxWidth()) {
Picker(
state = valuesPickerState,
items = values,
visibleItemsCount = 3,
modifier = Modifier.weight(0.3f),
textModifier = Modifier.padding(8.dp),
textStyle = TextStyle(fontSize = 32.sp)
)
Picker(
state = unitsPickerState,
items = units,
visibleItemsCount = 3,
modifier = Modifier.weight(0.7f),
textModifier = Modifier.padding(8.dp),
textStyle = TextStyle(fontSize = 32.sp)
)
}
Text(
text = "Interval: ${valuesPickerState.selectedItem} ${unitsPickerState.selectedItem}",
modifier = Modifier.padding(vertical = 16.dp)
)
}
}
}
Preview:

By coincidence I've implemented a screen like that last week. I can't share the whole code here, but basically what I did was:
DatePicker (res/layout/date_picker.xml).<DatePicker xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/datePicker"
android:theme="@style/DatePickerStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:calendarViewShown="false"
android:datePickerMode="spinner" />
@Composable
fun DatePicker(
onDateSelected: (Date) -> Unit
) {
AndroidView(
modifier = Modifier.fillMaxWidth(),
factory = { context ->
val view = LayoutInflater.from(context).inflate(R.layout.date_picker, null)
val datePicker = view.findViewById<DatePicker>(R.id.datePicker)
val calendar = Calendar.getInstance() // show today by default
datePicker.init(
calendar.get(Calendar.YEAR),
calendar.get(Calendar.MONTH),
calendar.get(Calendar.DAY_OF_MONTH)
) { _, year, monthOfYear, dayOfMonth ->
val date = Calendar.getInstance().apply {
set(year, monthOfYear, dayOfMonth)
}.time
onSelectedDateUpdate(date)
}
datePicker
}
)
}
ModalBottomSheetLayoutEditing my answer... Using a NumberPicker working as well...
AndroidView(
modifier = Modifier.fillMaxWidth(),
factory = { context ->
NumberPicker(context).apply {
setOnValueChangedListener { numberPicker, i, i2 -> }
minValue = 0
maxValue = 50
}
}
)
Here is the result.

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With