Coverage Summary for Class: NumberPickerKt (com.vsevolodganin.clicktrack.ui.piece)

Class Method, % Branch, % Line, % Instruction, %
NumberPickerKt 0% (0/9) 0% (0/34) 0% (0/94) 0% (0/1141)
NumberPickerKt$Label$1$1 0% (0/2) 0% (0/2) 0% (0/15)
NumberPickerKt$NumberPicker$3$1$1 0% (0/1) 0% (0/1) 0% (0/20)
NumberPickerKt$NumberPicker$4$1 0% (0/1) 0% (0/1) 0% (0/22)
NumberPickerKt$NumberPicker$4$1$1 0% (0/2) 0% (0/11) 0% (0/113)
Total 0% (0/15) 0% (0/34) 0% (0/109) 0% (0/1311)


 package com.vsevolodganin.clicktrack.ui.piece
 
 import androidx.compose.animation.core.Animatable
 import androidx.compose.animation.core.AnimationResult
 import androidx.compose.animation.core.AnimationVector1D
 import androidx.compose.animation.core.DecayAnimationSpec
 import androidx.compose.animation.core.calculateTargetValue
 import androidx.compose.animation.core.exponentialDecay
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.gestures.detectTapGestures
 import androidx.compose.foundation.gestures.draggable
 import androidx.compose.foundation.gestures.rememberDraggableState
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.offset
 import androidx.compose.foundation.layout.wrapContentSize
 import androidx.compose.material.ContentAlpha
 import androidx.compose.material.LocalTextStyle
 import androidx.compose.material.MaterialTheme
 import androidx.compose.material.ProvideTextStyle
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.alpha
 import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.dp
 import com.vsevolodganin.clicktrack.utils.compose.Preview
 import kotlinx.coroutines.launch
 import kotlin.math.abs
 import kotlin.math.roundToInt
 
 @Composable
 fun NumberPicker(
     state: MutableState<Int>,
     modifier: Modifier = Modifier,
     range: IntRange? = null,
     textStyle: TextStyle = LocalTextStyle.current,
 ) {
     NumberPicker(
         value = state.value,
         onValueChange = { state.value = it },
         modifier = modifier,
         range = range,
         textStyle = textStyle,
     )
 }
 
 @Composable
 fun NumberPicker(
     value: Int,
     onValueChange: (Int) -> Unit,
     modifier: Modifier = Modifier,
     range: IntRange? = null,
     textStyle: TextStyle = LocalTextStyle.current,
 ) {
     val coroutineScope = rememberCoroutineScope()
     val numbersColumnHeight = 36.dp
     val halvedNumbersColumnHeight = numbersColumnHeight / 2
     val halvedNumbersColumnHeightPx = with(LocalDensity.current) { halvedNumbersColumnHeight.toPx() }
 
     fun animatedStateValue(offset: Float): Int = value - (offset / halvedNumbersColumnHeightPx).toInt()
 
     val animatedOffset = remember { Animatable(0f) }
         .apply {
             if (range != null) {
                 val first = -(range.last - value) * halvedNumbersColumnHeightPx
                 val last = -(range.first - value) * halvedNumbersColumnHeightPx
                 val offsetRange = first..last
                 updateBounds(offsetRange.start, offsetRange.endInclusive)
             }
         }
     val coercedAnimatedOffset = animatedOffset.value % halvedNumbersColumnHeightPx
     val animatedStateValue = animatedStateValue(animatedOffset.value)
 
     Column(
         modifier = modifier
             .wrapContentSize()
             .draggable(
                 orientation = Orientation.Vertical,
                 state = rememberDraggableState { deltaY ->
                     coroutineScope.launch {
                         animatedOffset.snapTo(animatedOffset.value + deltaY)
                     }
                 },
                 onDragStopped = { velocity ->
                     coroutineScope.launch {
                         val endValue = animatedOffset.fling(
                             initialVelocity = velocity,
                             animationSpec = 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()
                                 coercedPoint + base
                             },
                         ).endState.value
 
                         onValueChange(animatedStateValue(endValue))
                         animatedOffset.snapTo(0f)
                     }
                 },
             ),
     ) {
         val spacing = 4.dp
 
         val arrowColor = MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.disabled)
 
         Arrow(direction = ArrowDirection.UP, tint = arrowColor)
 
         Spacer(modifier = Modifier.height(spacing))
 
         Box(
             modifier = Modifier
                 .align(Alignment.CenterHorizontally)
                 .offset { IntOffset(x = 0, y = coercedAnimatedOffset.roundToInt()) },
         ) {
             val baseLabelModifier = Modifier.align(Alignment.Center)
             ProvideTextStyle(textStyle) {
                 Label(
                     text = (animatedStateValue - 1).toString(),
                     modifier = baseLabelModifier
                         .offset(y = -halvedNumbersColumnHeight)
                         .alpha(coercedAnimatedOffset / halvedNumbersColumnHeightPx),
                 )
                 Label(
                     text = animatedStateValue.toString(),
                     modifier = baseLabelModifier
                         .alpha(1 - abs(coercedAnimatedOffset) / halvedNumbersColumnHeightPx),
                 )
                 Label(
                     text = (animatedStateValue + 1).toString(),
                     modifier = baseLabelModifier
                         .offset(y = halvedNumbersColumnHeight)
                         .alpha(-coercedAnimatedOffset / halvedNumbersColumnHeightPx),
                 )
             }
         }
 
         Spacer(modifier = Modifier.height(spacing))
 
         Arrow(direction = ArrowDirection.DOWN, tint = arrowColor)
     }
 }
 
 @Composable
 private fun Label(text: String, modifier: Modifier) {
     Text(
         text = text,
         modifier = modifier.pointerInput(Unit) {
             detectTapGestures(onLongPress = {
                 // FIXME: Empty to disable text selection
             })
         },
     )
 }
 
 private suspend fun Animatable<Float, AnimationVector1D>.fling(
     initialVelocity: Float,
     animationSpec: DecayAnimationSpec<Float>,
     adjustTarget: ((Float) -> Float)?,
     block: (Animatable<Float, AnimationVector1D>.() -> Unit)? = null,
 ): AnimationResult<Float, AnimationVector1D> {
     val targetValue = animationSpec.calculateTargetValue(value, initialVelocity)
     val adjustedTarget = adjustTarget?.invoke(targetValue)
 
     return if (adjustedTarget != null) {
         animateTo(
             targetValue = adjustedTarget,
             initialVelocity = initialVelocity,
             block = block,
         )
     } else {
         animateDecay(
             initialVelocity = initialVelocity,
             animationSpec = animationSpec,
             block = block,
         )
     }
 }
 
 @Preview
 @Composable
 private fun Preview() {
     Box(modifier = Modifier.fillMaxSize()) {
         NumberPicker(
             state = remember { mutableStateOf(9) },
             range = 0..10,
             modifier = Modifier.align(Alignment.Center),
         )
     }
 }