Reputation: 187
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.
Upvotes: 6
Views: 4678
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
Reputation: 1912
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