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.
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.
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...
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
© 2022 - 2024 — McMap. All rights reserved.
numberOfDisplayedItems
to 5 for example doesn't work. The alignment is lost. – Fusco