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

Class Method, % Branch, % Line, % Instruction, %
WheelPickerKt 0% (0/17) 0% (0/38) 0% (0/98) 0% (0/1111)
WheelPickerKt$rememberSnapFlingBehaviorWithOnFinishedListener$1$1 0% (0/2) 0% (0/3) 0% (0/45)
WheelPickerKt$rememberSnapFlingBehaviorWithOnFinishedListener$1$1$performFling$1
WheelPickerKt$WheelPicker$1$1 0% (0/1) 0% (0/1) 0% (0/16)
WheelPickerKt$WheelPicker$lambda$4$0$$inlined$itemsIndexed$default$1 0% (0/1)
WheelPickerKt$WheelPicker$lambda$4$0$$inlined$itemsIndexed$default$2 0% (0/1)
WheelPickerKt$WheelPicker$lambda$4$0$$inlined$itemsIndexed$default$3 0% (0/1)
Total 0% (0/23) 0% (0/38) 0% (0/102) 0% (0/1172)


 package com.vsevolodganin.clicktrack.ui.piece
 
 import androidx.compose.animation.animateColorAsState
 import androidx.compose.animation.core.EaseOutQuad
 import androidx.compose.foundation.background
 import androidx.compose.foundation.gestures.FlingBehavior
 import androidx.compose.foundation.gestures.ScrollScope
 import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.lazy.LazyItemScope
 import androidx.compose.foundation.lazy.LazyListLayoutInfo
 import androidx.compose.foundation.lazy.LazyListState
 import androidx.compose.foundation.lazy.itemsIndexed
 import androidx.compose.foundation.lazy.rememberLazyListState
 import androidx.compose.material.LocalContentColor
 import androidx.compose.material.MaterialTheme
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.State
 import androidx.compose.runtime.derivedStateOf
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.alpha
 import androidx.compose.ui.draw.scale
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.util.fastSumBy
 import androidx.compose.ui.util.lerp
 import org.jetbrains.compose.ui.tooling.preview.Preview
 import kotlin.math.abs
 
 /**
  * A composable function that displays a vertical scrolling picker allowing users to choose
  * from a list of items.
  *
  * @param T The type of the items to be displayed in the picker.
  * @param selectedIndex The index of the currently selected item. This controls the initial
  * position of the picker and can be updated to animate a selection change.
  * @param items A list of items to display in the picker.
  * @param onItemSelect A callback invoked whenever an item is selected. Provides the selected
  * index and item.
  * @param modifier A [Modifier] for styling and configuring the appearance and behavior
  * of the picker.
  * @param itemContent A composable lambda that describes how to render each item in the list. Provides
  * the item's index, value, whether it is selected, and its closeness to the center of the picker
  * (to animate stuff like alpha, scale, etc.).
  */
 @Composable
 fun <T> WheelPicker(
     selectedIndex: Int,
     items: List<T>,
     onItemSelect: (index: Int, item: T) -> Unit,
     modifier: Modifier = Modifier,
     itemContent: @Composable LazyItemScope.(index: Int, item: T, isSelected: Boolean, closenessToSelection: Float) -> Unit =
         { _, item, isSelected, closenessToSelection ->
             DefaultWheelItemContent(item, isSelected, closenessToSelection)
         },
 ) {
     val lazyListState = rememberLazyListState(selectedIndex)
     val itemIndexInCenter by itemIndexInCenter(lazyListState)
     val contentPadding by contentPadding(lazyListState)
     val snapBehaviour = rememberSnapFlingBehaviorWithOnFinishedListener(lazyListState) {
         onItemSelect(itemIndexInCenter, items[itemIndexInCenter])
     }
 
     LaunchedEffect(selectedIndex) {
         lazyListState.animateScrollToItem(selectedIndex)
     }
 
     LazyColumn(
         modifier = modifier,
         state = lazyListState,
         contentPadding = contentPadding,
         flingBehavior = snapBehaviour,
     ) {
         itemsIndexed(items) { index, item ->
             val closenessToSelection by closenessToSelection(index, lazyListState)
             itemContent(
                 index,
                 item,
                 index == itemIndexInCenter,
                 closenessToSelection,
             )
         }
     }
 }
 
 @Composable
 fun <T> LazyItemScope.DefaultWheelItemContent(item: T, isSelected: Boolean, closenessToSelection: Float) {
     val easedClosenessToSelection = EaseOutQuad.transform(closenessToSelection)
     val scale = lerp(0.0f, 1f, easedClosenessToSelection)
     val color by animateColorAsState(if (isSelected) MaterialTheme.colors.primary else LocalContentColor.current)
 
     Text(
         text = item.toString(),
         modifier = Modifier
             .fillParentMaxWidth()
             .scale(scale)
             .alpha(easedClosenessToSelection),
         color = color,
         textAlign = TextAlign.Center,
     )
 }
 
 @Composable
 private fun rememberSnapFlingBehaviorWithOnFinishedListener(
     lazyListState: LazyListState,
     onSnapFinished: () -> Unit,
 ): FlingBehavior {
     val flingBehaviour = rememberSnapFlingBehavior(lazyListState)
     return remember(flingBehaviour, onSnapFinished) {
         object : FlingBehavior {
             override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
                 return with(flingBehaviour) { performFling(initialVelocity) }
                     .also { onSnapFinished() }
             }
         }
     }
 }
 
 @Composable
 private fun itemIndexInCenter(lazyListState: LazyListState): State<Int> {
     return remember {
         derivedStateOf {
             val layout = lazyListState.layoutInfo
             val viewportStart = layout.viewportStartOffset
             val viewportEnd = layout.viewportEndOffset
             val viewportCenter = (viewportStart + viewportEnd) / 2
 
             layout.visibleItemsInfo.minByOrNull { info ->
                 val itemCenter = info.offset + info.size / 2
                 abs(itemCenter - viewportCenter)
             }?.index ?: 0
         }
     }
 }
 
 @Composable
 private fun contentPadding(lazyListState: LazyListState): State<PaddingValues> {
     val density = LocalDensity.current
     return remember {
         derivedStateOf {
             val layout = lazyListState.layoutInfo
             val verticalPaddingPx = (layout.viewportSize.height - layout.visibleItemsAverageSize()) / 2
             val verticalPaddingDp = with(density) { verticalPaddingPx.toDp() }
 
             PaddingValues(vertical = verticalPaddingDp)
         }
     }
 }
 
 @Composable
 private fun closenessToSelection(itemIndex: Int, lazyListState: LazyListState): State<Float> {
     return remember {
         derivedStateOf {
             val layout = lazyListState.layoutInfo
             val viewportStart = layout.viewportStartOffset
             val viewportEnd = layout.viewportEndOffset
             val viewportCenter = (viewportStart + viewportEnd) / 2
 
             lazyListState.layoutInfo.visibleItemsInfo
                 .firstOrNull { it.index == itemIndex }
                 ?.let { info ->
                     val itemCenter = info.offset + info.size / 2
                     abs(itemCenter - viewportCenter)
                 }
                 ?.let { distanceFromCenter ->
                     1f - distanceFromCenter.toFloat() / layout.viewportSize.height * 2
                 }
                 ?: 0f
         }
     }
 }
 
 private fun LazyListLayoutInfo.visibleItemsAverageSize(): Int {
     return if (visibleItemsInfo.isEmpty()) {
         0
     } else {
         val visibleItems = visibleItemsInfo
         val itemsSum = visibleItems.fastSumBy { it.size }
         itemsSum / visibleItems.size + mainAxisItemSpacing
     }
 }
 
 @Preview
 @Composable
 private fun WheelPickerPreview() {
     Box(
         modifier = Modifier.fillMaxSize(),
         contentAlignment = Alignment.Center,
     ) {
         WheelPicker(
             selectedIndex = 100,
             items = List(300) { index -> "Item $index" },
             onItemSelect = { _, _ -> },
             modifier = Modifier.fillMaxHeight(0.5f),
         )
 
         // Guidelines
         Box(
             modifier = Modifier
                 .height(1.dp)
                 .fillMaxWidth()
                 .background(Color.Gray.copy(alpha = 0.2f)),
         )
         Box(
             modifier = Modifier
                 .width(1.dp)
                 .fillMaxHeight()
                 .background(Color.Gray.copy(alpha = 0.2f)),
         )
     }
 }