Dziki Arbuz
Dziki Arbuz

Reputation: 187

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

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

Upvotes: 6

Views: 4678

Answers (2)

z.g.y
z.g.y

Reputation: 6187

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

Upvotes: 1

slaviboy
slaviboy

Reputation: 1912

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

Upvotes: 9

Related Questions