Android Compose implements number selector

Android Compose implements number selector

  • Preface
    • Blogs to learn from
    • The text is here
    • Use process
    • Conclusion

Foreword

I haven’t written a blog for a long time. Now let’s write an article about how to implement a numerical scrolling selector in compose. This is because a component in my company’s project uses a similar function. At the beginning, I also searched online and learned a lot. The big guy wrote it to achieve a similar function. Later, I studied it based on what the big guy wrote and changed it to a writing method that is more suitable for me. I also think it will be a more general method. Without further ado, I will present the finished product to you. have a look.

Blogs to learn from

Jetpack Compose: Super simple implementation of wheel control (WheelPicker)
Time selection component implemented by Jetpack Compose

The text is here

First of all, when making this scrolling list, I thought of Column. Although to make it slide, you need to add the .verticalScroll(rememberScrollState()) attribute to make it slide. But then I still chose the list in compose. Component LazyColumn; in this way, we can naturally write its general framework.

val listState = rememberLazyListState()
LazyColumn(
            modifier = Modifier,
            state = listState
        ) {<!-- -->}

Then in order to make it reusable, the Item inside is generated directly based on the size of the data.

items(size) {<!-- --> index ->
                Box(
                    modifier = Modifier
                        .fillMaxWidth()
                        .height(itemHeight),
                    contentAlignment = Alignment.Center,
                ) {<!-- -->
                //Here the data to be displayed is called back to the outside for customized display.
                    content(data[index])
                }
            }

At the same time, when we check the source code of LazyColunm, we can find: flingBehavior interface to specify throwing behavior. When the drag ends with velocity in the scrollable, performFling is called to perform the fling animation and update state via ScrollScope.scrollBy. So we can change the LazyColumn:

LazyColumn(
            modifier = Modifier,
            state = listState,
            flingBehavior = rememberSnapFlingBehavior(listState),
        )

If you want to set the initial value, you can set it according to the attribute initialFirstVisibleItemIndex in rememberLazyListState. Therefore, the complete LazyColum configuration is as follows:

val listState = rememberLazyListState(
            initialFirstVisibleItemIndex = selectIndex
        )
        LazyColumn(
            modifier = Modifier,
            state = listState,
            flingBehavior = rememberSnapFlingBehavior(listState),
        ){<!-- -->}

The source code of the entire component is:

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun <T> ListNumberPicker(
    data: List<T>,
    selectIndex: Int,
    visibleCount: Int,
    modifier: Modifier = Modifier,
    onSelect: (index: Int, item: T) -> Unit,
    content: @Composable (item: T) -> Unit,
) {<!-- -->
    BoxWithConstraints(modifier = modifier, propagateMinConstraints = true) {<!-- -->
        val pickerHeight = maxHeight
        val size = data.size
        val itemHeight = pickerHeight / visibleCount
        val listState = rememberLazyListState(
            initialFirstVisibleItemIndex = selectIndex
        )
        val firstVisibleItemIndex by remember {<!-- --> derivedStateOf {<!-- --> listState.firstVisibleItemIndex } }
        LazyColumn(
            modifier = Modifier,
            state = listState,
            flingBehavior = rememberSnapFlingBehavior(listState),
        ) {<!-- -->
        //Occupy the corresponding height. For example, if 5 are displayed, the middle one is selected and the others are unselected, but they must also occupy a certain space.
            for (i in 1..visibleCount / 2) {<!-- -->
                item {<!-- -->
                    Surface(modifier = Modifier.height(itemHeight)) {<!-- -->}
                }
            }
            items(size) {<!-- --> index ->
            //Prevent array out-of-bounds occurrence when sliding
                if (firstVisibleItemIndex >= size) {<!-- -->
                    onSelect(size - 1, data[size - 1])
                } else {<!-- -->
                    onSelect(firstVisibleItemIndex, data[firstVisibleItemIndex])
                }
                Box(
                    modifier = Modifier
                        .fillMaxWidth()
                        .height(itemHeight),
                    contentAlignment = Alignment.Center,
                ) {<!-- -->
                    content(data[index])
                }
            }
            for (i in 1..visibleCount / 2) {<!-- -->
                item {<!-- -->
                    Surface(modifier = Modifier.height(itemHeight)) {<!-- -->}
                }
            }
        }

    }
}

Usage process

Not much else to say, just use it now

 Row(
            Modifier
                .wrapContentHeight()
                .fillMaxWidth(),
            Arrangement.Center,
            Alignment.CenterVertically
        ) {<!-- -->
            //Year
            var selectYear by remember {<!-- -->
                mutableIntStateOf(selectDate.getYearr())
            }

            val yearData = LinkedList<Int>().apply {<!-- -->
                for (i in 1970..nowDate.getYearr()) {<!-- -->
                    add(i)
                }
            }

            ListNumberPicker(
                data = yearData,
                selectIndex = yearData.indexOf(selectYear),
                visibleCount = 3,
                modifier = Modifier
                    .height(150.dp)
                    .width(89.dp)
                    .background(
                        color = colorResource(id = R.color.home_background).copy(alpha = 0.3f),
                        shape = RoundedCornerShape(10.dp)
                    ),
                onSelect = {<!-- --> _, item ->
                    selectYear = item
                    yearCall(selectYear)
                }
            ) {<!-- -->
                Column(
                    modifier = Modifier.fillMaxWidth(),
                    verticalArrangement = Arrangement.Center,
                    horizontalAlignment = Alignment.CenterHorizontally
                ) {<!-- -->
                //Determine whether it is a selected state, select the style to be displayed and the unselected style
                    if (it == selectYear) {<!-- -->
                        Divider(
                            modifier = Modifier
                                .width(89.dp)
                                .padding(horizontal = 17.5.dp),
                            color = colorResource(id = R.color.color_btn_line).copy(alpha = 0.5f)
                        )
                        Text(
                            text = "$it", color = Color.White,
                            fontWeight = FontWeight.Bold,
                            textAlign = TextAlign.Center,
                            fontSize = 29.sp
                        )
                        Divider(
                            modifier = Modifier
                                .width(89.dp)
                                .padding(horizontal = 17.5.dp),
                            color = colorResource(id = R.color.color_btn_line).copy(alpha = 0.5f)
                        )
                    } else {<!-- -->
                        Text(
                            text = "$it",
                            color = Color.White.copy(alpha = 0.3f),
                            fontWeight = FontWeight.Bold,
                            textAlign = TextAlign.Center,
                            fontSize = 18.sp
                        )
                    }
                }
            }

Conclusion

Up to now, I feel that my writing is very rough, but there are still many shortcomings. This article is also based on the foundation of predecessors. Thanks again to everyone who shared their experience. If you have different ideas while reading the article, please feel free to put them forward and let us make progress together.