Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Jetpack Compose implementation of simple integer picker. Supports fling gestures!
@Composable
fun NumberPicker(
state: MutableState<Int> = remember { mutableStateOf(0) },
modifier: Modifier = Modifier
) {
val numbersColumnHeight = 36.dp
val halvedNumbersColumnHeight = numbersColumnHeight / 2
val halvedNumbersColumnHeightPx = with(DensityAmbient.current) { halvedNumbersColumnHeight.toPx() }
fun animatedStateValue(offset: Float): Int = state.value - (offset / halvedNumbersColumnHeightPx).toInt()
val animatedOffset = animatedFloat(initVal = 0f)
val coercedAnimatedOffset = animatedOffset.value % halvedNumbersColumnHeightPx
val animatedStateValue = animatedStateValue(animatedOffset.value)
Column(
modifier = modifier
.wrapContentSize()
.draggable(
orientation = Orientation.Vertical,
onDrag = { deltaY ->
animatedOffset.snapTo(animatedOffset.value + deltaY)
},
onDragStopped = { velocity ->
val config = FlingConfig(
decayAnimation = ExponentialDecay(
frictionMultiplier = 20f
),
adjustTarget = { target ->
val coercedTarget = target % halvedNumbersColumnHeightPx
val coercedAnchors = listOf(-halvedNumbersColumnHeightPx, 0f, halvedNumbersColumnHeightPx)
val coercedPoint = coercedAnchors.minByOrNull { abs(it - coercedTarget) }!!
val base = halvedNumbersColumnHeightPx * (target / halvedNumbersColumnHeightPx).toInt()
val adjusted = coercedPoint + base
TargetAnimation(adjusted, SpringSpec())
}
)
animatedOffset.fling(velocity, config) { _, endValue, _ ->
state.value = animatedStateValue(endValue)
animatedOffset.snapTo(0f)
}
}
)
) {
val spacing = 4.dp
Arrow(ArrowDirection.UP)
Spacer(modifier = Modifier.height(spacing))
Stack(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.offsetPx(y = mutableStateOf(coercedAnimatedOffset))
) {
Label(
text = (animatedStateValue - 1).toString(),
modifier = Modifier
.offset(y = -halvedNumbersColumnHeight)
.drawOpacity(coercedAnimatedOffset / halvedNumbersColumnHeightPx)
)
Label(
text = animatedStateValue.toString(),
modifier = Modifier
.drawOpacity(1 - abs(coercedAnimatedOffset) / halvedNumbersColumnHeightPx)
)
Label(
text = (animatedStateValue + 1).toString(),
modifier = Modifier
.offset(y = halvedNumbersColumnHeight)
.drawOpacity(-coercedAnimatedOffset / halvedNumbersColumnHeightPx)
)
}
Spacer(modifier = Modifier.height(spacing))
Arrow(ArrowDirection.DOWN)
}
}
@Composable
private fun Label(text: String, modifier: Modifier) {
Text(
text = text,
modifier = modifier.longPressGestureFilter {
/* Empty to disable text selection */
}
)
}
private enum class ArrowDirection {
UP, DOWN
}
@Composable
private fun Arrow(direction: ArrowDirection) {
val vectorAsset = remember(direction) {
VectorAssetBuilder(defaultWidth = 24.dp, defaultHeight = 12.dp, viewportWidth = 2f, viewportHeight = 1f)
.addPath(
pathData = when (direction) {
ArrowDirection.UP -> PathBuilder()
.moveTo(0f, 1f)
.lineTo(1f, 0f)
.lineTo(2f, 1f)
.close()
.getNodes()
ArrowDirection.DOWN -> PathBuilder()
.moveTo(0f, 0f)
.lineTo(2f, 0f)
.lineTo(1f, 1f)
.close()
.getNodes()
},
fill = SolidColor(Color.Black)
)
.build()
}
Image(asset = vectorAsset)
}
@Preview
@Composable
fun PreviewNumberPicker() {
Stack(modifier = Modifier.fillMaxSize()) {
NumberPicker(modifier = Modifier.fillMaxSize().align(Alignment.Center))
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.