こんな感じのNumber Pickerを作りました。
State
まずは無難にStateの定義。
State経由でPositionの変化を受け取ります。
@Composable public fun rememberPickerState(): PickerState { return remember { PickerState() } } public class PickerState { public var currentPosition: Int by mutableStateOf(0) internal set }
本体
DividerはColumnのweightで3等分しました。
LazyColumnの要素には3等分した高さを設定し、先頭と末尾に空の要素を1つ設定しています。
問題は後述のflingBehaviorです。
@OptIn(ExperimentalFoundationApi::class) @Composable public fun <T> Picker( modifier: Modifier, state: PickerState = rememberPickerState(), items: ImmutableList<T>, content: @Composable (item: T) -> Unit, ) { BoxWithConstraints( modifier = modifier, ) { val itemHeight = remember(maxHeight) { maxHeight / 3 } Column(Modifier.fillMaxSize()) { val spacerModifier = Modifier .height(1.dp) .fillMaxWidth() .background(Color.Gray) Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = spacerModifier) Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = spacerModifier) Spacer(modifier = Modifier.weight(1f)) } val lazyListState = rememberLazyListState() val itemInfo by remember { derivedStateOf { lazyListState.layoutInfo.visibleItemsInfo } } LaunchedEffect(itemInfo) { val item = itemInfo.getOrNull(0) state.currentPosition = item?.index ?: 0 } LazyColumn( modifier = Modifier.fillMaxHeight(), state = lazyListState, flingBehavior = rememberPickerSnapFlingBehavior( lazyListState = lazyListState ), ) { item { Spacer(modifier = Modifier.height(itemHeight)) } items(items) { item -> Box(modifier = Modifier.height(itemHeight)) { content(item) } } item { Spacer(modifier = Modifier.height(itemHeight)) } } } }
FlingBehavior
普通のLazyListと違うのは以下のことです。これを FlingBehavior
で実現します。
- スクロールが遅い事
- 中途半端な部分に止まった時にSnapして止まること
SnapFlingBehavior
というものがあるのですが、これはスクロールがすぐに止まってしまうので使えなさそうでした。うまく設定すればいけるのかな?
@ExperimentalFoundationApi @Composable private fun rememberPickerSnapFlingBehavior( lazyListState: LazyListState ): FlingBehavior { val snappingLayout = remember(lazyListState) { SnapLayoutInfoProvider(lazyListState) } val flingSpec = remember { exponentialDecay<Float>(frictionMultiplier = 2.0f) // default=1.0f. ここの値でスクロールが遅くなる。 } val snapFlingBehavior = rememberSnapFlingBehavior(snappingLayout) // 元からある return remember(flingSpec, snappingLayout) { SnapFlingBehavior( // 今回定義したもの decayAnimationSpec = flingSpec, snapFlingBehavior = snapFlingBehavior, ) } }
ScrollableDefaults.flingBehavior()
から取得できる DefaultFlingBehavior
のコードを一部コピーしてきます。変更点は以下の点です。
- 戻り値は0にするのでその部分のコードを削除
- スクロールが止まり気味になったら、
SnapFlingBehavior
に処理を移動する
foundationのDefaultFlingBehavior
DefaultFlingBehavior.kt
private class DefaultFlingBehavior( private val flingDecay: DecayAnimationSpec<Float> ) : FlingBehavior { override suspend fun ScrollScope.performFling(initialVelocity: Float): Float { // come up with the better threshold, but we need it since spline curve gives us NaNs return if (abs(initialVelocity) > 1f) { var velocityLeft = initialVelocity var lastValue = 0f AnimationState( initialValue = 0f, initialVelocity = initialVelocity, ).animateDecay(flingDecay) { val delta = value - lastValue val consumed = scrollBy(delta) lastValue = value velocityLeft = this.velocity // avoid rounding errors and stop if anything is unconsumed if (abs(delta - consumed) > 0.5f) this.cancelAnimation() } velocityLeft } else { initialVelocity } } }
SnapFlingBehavior
こちらがカスタマイズしたものです。
@OptIn(ExperimentalFoundationApi::class) private class SnapFlingBehavior( private val decayAnimationSpec: DecayAnimationSpec<Float>, private val snapFlingBehavior: SnapFlingBehavior, ) : FlingBehavior { override suspend fun ScrollScope.performFling(initialVelocity: Float): Float { return if (abs(initialVelocity) > 1f) { var lastValue = 0f var lastVelocity = 0f // 止まりそうな最後のvelocityを保持して、snapFlingBehaviorに渡す。 AnimationState( initialValue = 0f, initialVelocity = initialVelocity, ).animateDecay(decayAnimationSpec) { val delta = value - lastValue val consumed = scrollBy(delta) lastValue = value lastVelocity = velocity if (abs(velocity) < 5) { // ここで止まり気味を判別してcancelする this.cancelAnimation() } if (abs(delta - consumed) > 0.5f) this.cancelAnimation() } with(snapFlingBehavior) { // snapFlingBehaviorでSnapさせる。 performFling(lastVelocity) } 0f } else { with(snapFlingBehavior) { performFling(initialVelocity) } 0f } } }
Previewコード
Previewコード
@Preview(showBackground = true) @Composable private fun Preview() { val items = remember { (0..10).toList().toImmutableList() } Column( Modifier .height(200.dp) .width(200.dp) .padding(horizontal = 12.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { val state = rememberPickerState() Picker( modifier = Modifier .weight(1f) .fillMaxWidth(), items = items, state = state, ) { Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center, ) { Text( text = it.toString(), ) } } Spacer( modifier = Modifier .height(1.dp) .fillMaxWidth() .background(Color.Cyan) ) Text( modifier = Modifier.padding(8.dp), text = "selected = ${state.currentPosition}", ) } }