9

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

enter image description here

Charles Woodson
  • 682
  • 1
  • 8
  • 22

4 Answers4

12

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:

  1. Create a layout with a 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" />
  1. Then, use it in your composable function.
@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
        }
    )
}
  1. Finally, use it in a ModalBottomSheetLayout

Editing 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.

enter image description here

nglauber
  • 18,674
  • 6
  • 70
  • 75
10

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:

Preview

nhcodes
  • 1,206
  • 8
  • 20
  • Any suggestions in modifying this , for the list not to do infinite scrolling...to scroll only the items and not repeating them? – Iulia Barbu Jul 10 '23 at 10:37
  • Yes, that should be pretty easy. `items(Integer.MAX_VALUE) { index ->` makes the list scroll infinitely, so you can replace it by `items(items) { item ->`. You have to adjust a few other things too but that's the most important part. – nhcodes Jul 11 '23 at 09:12
  • What to change also to make it an non infinite list! Please if you can help – EliodeBeirut Jul 14 '23 at 16:54
  • @nhcodes can you please tell me how can we change this implementation to be a non infinite list! – EliodeBeirut Jul 17 '23 at 09:22
  • I used this picker in a bottomsheet, I want to reset the list to first position when expanding the bottom sheet. I don't havy any success in achieving that. Seems that rememberLazyListState is blocking the recomposition. – Iulia Barbu Jul 28 '23 at 10:33
3

I know maybe you are not looking for something like this. But since there is no such widget in compose yet and compose is all about making your way easier to build your own component. So Apart from android.widget NumberPicker, you can make something like this one. You can change the visualization more like the NumberPicker widget and add your callback and stuff.

Have you checked this one on github? ComposeNumberPicker.Kt

Rafiul
  • 1,560
  • 7
  • 19
3

We are using this library in our compose project for number picker widget. https://github.com/ChargeMap/Compose-NumberPicker

SoftwareGuy
  • 1,121
  • 2
  • 11
  • 23
  • Hello sir I want to use this library but I dont know where do I find latest version of this library to implement this to my project so please tell me that would be very big help. Thank you. – heet kanabar Sep 05 '22 at 07:23
  • 1
    The link I have above has information on downloading . And setting up in build gradle file too. Check it . Click on green button "Code" – SoftwareGuy Sep 06 '22 at 15:29