Advertisement
EnGold

Untitled

Oct 18th, 2023
1,346
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Kotlin 8.65 KB | Source Code | 0 0
  1. package com.chargemap.compose.numberpicker
  2.  
  3. import androidx.compose.animation.core.*
  4. import androidx.compose.foundation.background
  5. import androidx.compose.foundation.gestures.Orientation
  6. import androidx.compose.foundation.gestures.detectTapGestures
  7. import androidx.compose.foundation.gestures.draggable
  8. import androidx.compose.foundation.gestures.rememberDraggableState
  9. import androidx.compose.foundation.layout.*
  10. import androidx.compose.material.LocalTextStyle
  11. import androidx.compose.material.MaterialTheme
  12. import androidx.compose.material.ProvideTextStyle
  13. import androidx.compose.material.Text
  14. import androidx.compose.runtime.*
  15. import androidx.compose.ui.Alignment
  16. import androidx.compose.ui.Modifier
  17. import androidx.compose.ui.draw.alpha
  18. import androidx.compose.ui.graphics.Color
  19. import androidx.compose.ui.input.pointer.pointerInput
  20. import androidx.compose.ui.layout.Layout
  21. import androidx.compose.ui.platform.LocalDensity
  22. import androidx.compose.ui.text.TextStyle
  23. import androidx.compose.ui.text.style.TextAlign
  24. import androidx.compose.ui.unit.IntOffset
  25. import androidx.compose.ui.unit.dp
  26. import kotlinx.coroutines.launch
  27. import kotlin.math.abs
  28. import kotlin.math.roundToInt
  29.  
  30. private fun <T> getItemIndexForOffset(
  31.     range: List<T>,
  32.     value: T,
  33.     offset: Float,
  34.     halfNumbersColumnHeightPx: Float
  35. ): Int {
  36.     val indexOf = range.indexOf(value) - (offset / halfNumbersColumnHeightPx).toInt()
  37.     return maxOf(0, minOf(indexOf, range.count() - 1))
  38. }
  39.  
  40. @Composable
  41. fun <T> ListItemPicker(
  42.     modifier: Modifier = Modifier,
  43.     label: (T) -> String = { it.toString() },
  44.     value: T,
  45.     onValueChange: (T) -> Unit,
  46.     dividersColor: Color = MaterialTheme.colors.primary,
  47.     list: List<T>,
  48.     textStyle: TextStyle = LocalTextStyle.current,
  49. ) {
  50.     val minimumAlpha = 0.3f
  51.     val verticalMargin = 8.dp
  52.     val numbersColumnHeight = 80.dp
  53.     val halfNumbersColumnHeight = numbersColumnHeight / 2
  54.     val halfNumbersColumnHeightPx = with(LocalDensity.current) { halfNumbersColumnHeight.toPx() }
  55.  
  56.     val coroutineScope = rememberCoroutineScope()
  57.  
  58.     val animatedOffset = remember { Animatable(0f) }
  59.         .apply {
  60.             val index = list.indexOf(value)
  61.             val offsetRange = remember(value, list) {
  62.                 -((list.count() - 1) - index) * halfNumbersColumnHeightPx to
  63.                         index * halfNumbersColumnHeightPx
  64.             }
  65.             updateBounds(offsetRange.first, offsetRange.second)
  66.         }
  67.  
  68.     val coercedAnimatedOffset = animatedOffset.value % halfNumbersColumnHeightPx
  69.  
  70.     val indexOfElement = getItemIndexForOffset(list, value, animatedOffset.value, halfNumbersColumnHeightPx)
  71.  
  72.     var dividersWidth by remember { mutableStateOf(0.dp) }
  73.  
  74.     Layout(
  75.         modifier = modifier
  76.             .draggable(
  77.                 orientation = Orientation.Vertical,
  78.                 state = rememberDraggableState { deltaY ->
  79.                     coroutineScope.launch {
  80.                         animatedOffset.snapTo(animatedOffset.value + deltaY)
  81.                     }
  82.                 },
  83.                 onDragStopped = { velocity ->
  84.                     coroutineScope.launch {
  85.                         val endValue = animatedOffset.fling(
  86.                             initialVelocity = velocity,
  87.                             animationSpec = exponentialDecay(frictionMultiplier = 20f),
  88.                             adjustTarget = { target ->
  89.                                 val coercedTarget = target % halfNumbersColumnHeightPx
  90.                                 val coercedAnchors =
  91.                                     listOf(-halfNumbersColumnHeightPx, 0f, halfNumbersColumnHeightPx)
  92.                                 val coercedPoint = coercedAnchors.minByOrNull { abs(it - coercedTarget) }!!
  93.                                 val base = halfNumbersColumnHeightPx * (target / halfNumbersColumnHeightPx).toInt()
  94.                                 coercedPoint + base
  95.                             }
  96.                         ).endState.value
  97.  
  98.                         val result = list.elementAt(
  99.                             getItemIndexForOffset(list, value, endValue, halfNumbersColumnHeightPx)
  100.                         )
  101.                         onValueChange(result)
  102.                         animatedOffset.snapTo(0f)
  103.                     }
  104.                 }
  105.             )
  106.             .padding(vertical = numbersColumnHeight / 3 + verticalMargin * 2),
  107.         content = {
  108.             Box(
  109.                 modifier
  110.                     .width(dividersWidth)
  111.                     .height(2.dp)
  112.                     .background(color = dividersColor)
  113.             )
  114.             Box(
  115.                 modifier = Modifier
  116.                     .padding(vertical = verticalMargin, horizontal = 20.dp)
  117.                     .offset { IntOffset(x = 0, y = coercedAnimatedOffset.roundToInt()) }
  118.             ) {
  119.                 val baseLabelModifier = Modifier.align(Alignment.Center)
  120.                 ProvideTextStyle(textStyle) {
  121.                     if (indexOfElement > 0)
  122.                         Label(
  123.                             text = label(list.elementAt(indexOfElement - 1)),
  124.                             modifier = baseLabelModifier
  125.                                 .offset(y = -halfNumbersColumnHeight)
  126.                                 .alpha(maxOf(minimumAlpha, coercedAnimatedOffset / halfNumbersColumnHeightPx))
  127.                         )
  128.                     Label(
  129.                         text = label(list.elementAt(indexOfElement)),
  130.                         modifier = baseLabelModifier
  131.                             .alpha(
  132.                                 (maxOf(
  133.                                     minimumAlpha,
  134.                                     1 - abs(coercedAnimatedOffset) / halfNumbersColumnHeightPx
  135.                                 ))
  136.                             )
  137.                     )
  138.                     if (indexOfElement < list.count() - 1)
  139.                         Label(
  140.                             text = label(list.elementAt(indexOfElement + 1)),
  141.                             modifier = baseLabelModifier
  142.                                 .offset(y = halfNumbersColumnHeight)
  143.                                 .alpha(maxOf(minimumAlpha, -coercedAnimatedOffset / halfNumbersColumnHeightPx))
  144.                         )
  145.                 }
  146.             }
  147.             Box(
  148.                 modifier
  149.                     .width(dividersWidth)
  150.                     .height(2.dp)
  151.                     .background(color = dividersColor)
  152.             )
  153.         }
  154.     ) { measurables, constraints ->
  155.         // Don't constrain child views further, measure them with given constraints
  156.         // List of measured children
  157.         val placeables = measurables.map { measurable ->
  158.             // Measure each children
  159.             measurable.measure(constraints)
  160.         }
  161.  
  162.         dividersWidth = placeables
  163.             .drop(1)
  164.             .first()
  165.             .width
  166.             .toDp()
  167.  
  168.         // Set the size of the layout as big as it can
  169.         layout(dividersWidth.toPx().toInt(), placeables
  170.             .sumOf {
  171.                 it.height
  172.             }
  173.         ) {
  174.             // Track the y co-ord we have placed children up to
  175.             var yPosition = 0
  176.  
  177.             // Place children in the parent layout
  178.             placeables.forEach { placeable ->
  179.  
  180.                 // Position item on the screen
  181.                 placeable.placeRelative(x = 0, y = yPosition)
  182.  
  183.                 // Record the y co-ord placed up to
  184.                 yPosition += placeable.height
  185.             }
  186.         }
  187.     }
  188. }
  189.  
  190. @Composable
  191. private fun Label(text: String, modifier: Modifier) {
  192.     Text(
  193.         modifier = modifier.pointerInput(Unit) {
  194.             detectTapGestures(onLongPress = {
  195.                 // FIXME: Empty to disable text selection
  196.             })
  197.         },
  198.         text = text,
  199.         textAlign = TextAlign.Center,
  200.     )
  201. }
  202.  
  203. private suspend fun Animatable<Float, AnimationVector1D>.fling(
  204.     initialVelocity: Float,
  205.     animationSpec: DecayAnimationSpec<Float>,
  206.     adjustTarget: ((Float) -> Float)?,
  207.     block: (Animatable<Float, AnimationVector1D>.() -> Unit)? = null,
  208. ): AnimationResult<Float, AnimationVector1D> {
  209.     val targetValue = animationSpec.calculateTargetValue(value, initialVelocity)
  210.     val adjustedTarget = adjustTarget?.invoke(targetValue)
  211.     return if (adjustedTarget != null) {
  212.         animateTo(
  213.             targetValue = adjustedTarget,
  214.             initialVelocity = initialVelocity,
  215.             block = block
  216.         )
  217.     } else {
  218.         animateDecay(
  219.             initialVelocity = initialVelocity,
  220.             animationSpec = animationSpec,
  221.             block = block,
  222.         )
  223.     }
  224. }
  225.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement