Is there a way to create Scroll Wheel in Jetpack Compose?
Asked Answered
B

2

6

Is there a way to create looped Scroll Wheel that looks like scroll wheel date Pickers on iOS?

Looked everywhere for the answer but didn't find any.

enter image description here

Bouffard answered 27/10, 2021 at 7:27 Comment(0)
E
8

enter image description here

Here is the full example code Gist


You can use the following sample composable InfiniteCircularList:

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun <T> InfiniteCircularList(
    width: Dp,
    itemHeight: Dp,
    numberOfDisplayedItems: Int = 3,
    items: List<T>,
    initialItem: T,
    itemScaleFact: Float = 1.5f,
    textStyle: TextStyle,
    textColor: Color,
    selectedTextColor: Color,
    onItemSelected: (index: Int, item: T) -> Unit = { _, _ -> }
) {
    val itemHalfHeight = LocalDensity.current.run { itemHeight.toPx() / 2f }
    val scrollState = rememberLazyListState(0)
    var lastSelectedIndex by remember {
        mutableStateOf(0)
    }
    var itemsState by remember {
        mutableStateOf(items)
    }
    LaunchedEffect(items) {
        var targetIndex = items.indexOf(initialItem) - 1
        targetIndex += ((Int.MAX_VALUE / 2) / items.size) * items.size
        itemsState = items
        lastSelectedIndex = targetIndex
        scrollState.scrollToItem(targetIndex)
    }
    LazyColumn(
        modifier = Modifier
            .width(width)
            .height(itemHeight * numberOfDisplayedItems),
        state = scrollState,
        flingBehavior = rememberSnapFlingBehavior(
            lazyListState = scrollState
        )
    ) {
        items(
            count = Int.MAX_VALUE,
            itemContent = { i ->
                val item = itemsState[i % itemsState.size]
                Box(
                    modifier = Modifier
                        .height(itemHeight)
                        .fillMaxWidth()
                        .onGloballyPositioned { coordinates ->
                            val y = coordinates.positionInParent().y - itemHalfHeight
                            val parentHalfHeight = (itemHalfHeight * numberOfDisplayedItems)
                            val isSelected = (y > parentHalfHeight - itemHalfHeight && y < parentHalfHeight + itemHalfHeight)
                            val index = i - 1
                            if (isSelected && lastSelectedIndex != index) {
                                onItemSelected(index % itemsState.size, item)
                                lastSelectedIndex = index
                            }
                        },
                    contentAlignment = Alignment.Center
                ) {
                    Text(
                        text = item.toString(),
                        style = textStyle,
                        color = if (lastSelectedIndex == i) {
                            selectedTextColor
                        } else {
                            textColor
                        },
                        fontSize = if (lastSelectedIndex == i) {
                            textStyle.fontSize * itemScaleFact
                        } else {
                            textStyle.fontSize
                        }
                    )
                }
            }
        )
    }
}

You can also change the number of shown items via numberOfDisplayedItems property, using value 3,5,7...

Euterpe answered 14/10, 2023 at 23:54 Comment(1)
Changing numberOfDisplayedItems to 5 for example doesn't work. The alignment is lost.Fusco
C
0

Apologies, but it was difficult to look for a sample of this use-case, so ill leave a rough working sample here for anybody looking for something similar. I managed to create an infinite looking scrolling based on this post Circular Endless Scrolling, and applied a little bit of logic for an Hour Vertical scrollable

This can display both 24 and 12 hour format, though the entirety is not complete yet especially with the 24hr format

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun CircularClock(
    hourSize : Int,
    initialHour: Int
) {
    val height = 90.dp
    val cellSize = height / 3
    val cellTextSize = LocalDensity.current.run { (cellSize / 2f).toSp() }

    // just prepare an offset of 1 hour when format is set to 12hr format
    val hourOffset = if (hourSize == 12) 1 else 0
    val expandedSize = hourSize * 10_000_000
    val initialListPoint = expandedSize / 2
    val targetIndex = initialListPoint + initialHour - 1

    val scrollState = rememberLazyListState(targetIndex)
    val hour by remember { derivedStateOf { (scrollState.firstVisibleItemIndex + 1) % hourSize }}

    if (!scrollState.isScrollInProgress) {
        Log.e("FocusedHour", "${hour + hourOffset}")
    }

    LaunchedEffect(Unit) {
        // subtract the offset upon initial scrolling, otherwise it will look like
        // it moved 1 hour past the initial hour when format is set to 12hr format
        scrollState.scrollToItem(targetIndex - hourOffset)
    }

    Box(
        modifier = Modifier
            .height(height)
            .wrapContentWidth()
    ) {
        LazyColumn(
            modifier = Modifier
                .wrapContentWidth(),
            state = scrollState,
            flingBehavior = rememberSnapFlingBehavior(lazyListState = scrollState)
        ) {
            items(expandedSize, itemContent = {

                // if 12hr format, move 1 hour so instead of displaying 00 -> 11
                // it will display 01 to 12
                val num = (it % hourSize) + hourOffset
                Box(
                    modifier = Modifier
                        .size(cellSize),
                    contentAlignment = Alignment.Center
                ) {
                    Text(
                        text = String.format("%02d", num),
                        style = MaterialTheme.typography.overline.copy(
                        color = Color.Gray,
                        fontSize = cellTextSize
                    )
                )
            }
        })
    }
}

Disclaimer: this implementation doesn't work well (yet) with certain height as I rely with the scrollstate visible items, and any correction would be greatly appreciated.

For the snap effect: I used Chrisbane snapper

Confectionery answered 6/10, 2022 at 3:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.