Created
March 3, 2026 06:42
-
-
Save KlassenKonstantin/d16e9771517fa830e1dc792509a6c90d to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| @file:OptIn(ExperimentalFoundationApi::class) | |
| package de.kuno.snappyswipe.snappyswipe | |
| import androidx.compose.foundation.ExperimentalFoundationApi | |
| import androidx.compose.runtime.Composable | |
| import androidx.compose.runtime.derivedStateOf | |
| import androidx.compose.runtime.getValue | |
| import androidx.compose.runtime.mutableStateMapOf | |
| import androidx.compose.runtime.mutableStateOf | |
| import androidx.compose.runtime.remember | |
| import androidx.compose.runtime.setValue | |
| import kotlinx.coroutines.ExperimentalCoroutinesApi | |
| import kotlin.math.absoluteValue | |
| import kotlin.math.max | |
| import kotlin.math.min | |
| @OptIn(ExperimentalCoroutinesApi::class) | |
| class DragCoordinatorState<T : DraggedItemInfo> internal constructor() { | |
| var dragInfo by mutableStateOf<T?>(null) | |
| val itemOffsets = mutableStateMapOf<Any?, Float>() | |
| val itemInfos = mutableStateOf<List<ItemInfo>>(listOf()) | |
| private val itemLookup by derivedStateOf { | |
| itemInfos.value.associateBy { it.key } | |
| } | |
| fun getItemState(key: Any?): ItemState<T>? { | |
| val index = itemLookup[key]?.index ?: return null | |
| val itemInfos = itemInfos.value | |
| val offsets = Triple( | |
| itemOffsets[key], | |
| itemOffsets[itemInfos.getOrNull(index - 1)?.key], | |
| itemOffsets[itemInfos.getOrNull(index + 1)?.key] | |
| ) | |
| val currentItem = itemInfos.first { it.key == key }.copy( | |
| offset = offsets.first ?: 0f | |
| ) | |
| val topItem = itemInfos.getOrNull(currentItem.index - 1)?.copy( | |
| offset = offsets.second ?: 0f | |
| ) | |
| val bottomItem = itemInfos.getOrNull(currentItem.index + 1)?.copy( | |
| offset = offsets.third ?: 0f | |
| ) | |
| val draggedItemRelation = dragInfo?.let { dragInfo -> | |
| val draggedItemIndex = itemInfos.indexOfFirst { it.key == dragInfo.key } | |
| if (index < 0 || draggedItemIndex < 0) return@let null | |
| val draggedItemInfo = itemInfos[draggedItemIndex] | |
| val distanceToDraggedItem = index - draggedItemIndex | |
| val sameSegmentAsDraggedItem = itemInfos.subList( | |
| min(index, draggedItemIndex), | |
| max(index, draggedItemIndex) | |
| ).all { it.segmentType == draggedItemInfo.segmentType } | |
| DraggedItemRelation( | |
| draggedItemInfo = dragInfo, | |
| indexDelta = distanceToDraggedItem, | |
| sameSegmentAsDraggedItem = sameSegmentAsDraggedItem, | |
| ) | |
| } | |
| return ItemState( | |
| draggedItemRelation = draggedItemRelation, | |
| itemInfo = currentItem, | |
| topItemInfo = topItem, | |
| bottomItemInfo = bottomItem, | |
| ) | |
| } | |
| fun updateOffset(key: Any?, offset: Float) { | |
| itemOffsets[key] = offset | |
| } | |
| } | |
| @Composable | |
| fun <D : DraggedItemInfo, T> rememberDragCoordinatorState( | |
| items: List<T>, | |
| key: (T) -> Any?, | |
| segmentType: (T) -> Any? = { null }, | |
| ): DragCoordinatorState<D> { | |
| return remember { | |
| DragCoordinatorState<D>() | |
| }.apply { | |
| itemInfos.value = items.mapIndexed { index, item -> | |
| val key = key(item) | |
| ItemInfo( | |
| key = key, | |
| index = index, | |
| segmentType = segmentType(item), | |
| offset = 0f | |
| ) | |
| } | |
| } | |
| } | |
| interface DraggedItemInfo { | |
| val key: Any? | |
| val dragOffset: Float | |
| } | |
| data class ItemState<T : DraggedItemInfo>( | |
| val draggedItemRelation: DraggedItemRelation<T>?, | |
| val itemInfo: ItemInfo, | |
| val topItemInfo: ItemInfo?, | |
| val bottomItemInfo: ItemInfo?, | |
| ) { | |
| val sameSegmentAsTopNeighbor: Boolean | |
| get() = itemInfo.segmentType == topItemInfo?.segmentType | |
| val sameSegmentAsBottomNeighbor: Boolean | |
| get() = itemInfo.segmentType == bottomItemInfo?.segmentType | |
| val offsetDeltaTop: Float | |
| get() = (itemInfo.offset - (topItemInfo?.offset ?: 0f)).absoluteValue | |
| val offsetDeltaBottom: Float | |
| get() = (itemInfo.offset - (bottomItemInfo?.offset ?: 0f)).absoluteValue | |
| val isDraggedItem: Boolean | |
| get() = itemInfo.key == draggedItemRelation?.draggedItemInfo?.key | |
| } | |
| data class DraggedItemRelation<T : DraggedItemInfo>( | |
| /** | |
| * Information about the item that is being dragged. | |
| */ | |
| val draggedItemInfo: T, | |
| /** | |
| * The distance between this item and the item that is being dragged. | |
| * A positive value means this item is further down than the item that is being dragged and vice versa. | |
| * An indexDelta of 0 means this item is the one being dragged. | |
| */ | |
| val indexDelta: Int, | |
| /** | |
| * Whether the dragged item is in the same segment as the item that is being dragged. | |
| */ | |
| val sameSegmentAsDraggedItem: Boolean, | |
| ) | |
| data class ItemInfo( | |
| val key: Any?, | |
| val index: Int, | |
| val segmentType: Any?, | |
| val offset: Float, | |
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package de.kuno.snappyswipe.snappyswipe | |
| import androidx.compose.animation.core.Animatable | |
| import androidx.compose.animation.core.AnimationSpec | |
| import androidx.compose.animation.core.VectorConverter | |
| import androidx.compose.foundation.shape.RoundedCornerShape | |
| import androidx.compose.runtime.Composable | |
| import androidx.compose.runtime.DisposableEffect | |
| import androidx.compose.runtime.LaunchedEffect | |
| import androidx.compose.runtime.getValue | |
| import androidx.compose.runtime.mutableStateOf | |
| import androidx.compose.runtime.remember | |
| import androidx.compose.runtime.rememberCoroutineScope | |
| import androidx.compose.runtime.setValue | |
| import androidx.compose.runtime.snapshotFlow | |
| import androidx.compose.ui.graphics.Shape | |
| import androidx.compose.ui.platform.LocalDensity | |
| import androidx.compose.ui.unit.Dp | |
| import androidx.compose.ui.unit.min | |
| import co.touchlab.kermit.Logger.Companion.d | |
| import kotlinx.coroutines.coroutineScope | |
| import kotlinx.coroutines.flow.collect | |
| import kotlinx.coroutines.flow.collectLatest | |
| import kotlinx.coroutines.flow.combine | |
| import kotlinx.coroutines.flow.filterNotNull | |
| import kotlinx.coroutines.launch | |
| import kotlin.experimental.ExperimentalTypeInference | |
| @OptIn(ExperimentalTypeInference::class) | |
| class ShapeHelper( | |
| minCornerRadius: () -> Dp, | |
| maxCornerRadius: () -> Dp, | |
| animationSpec: () -> AnimationSpec<Dp>, | |
| private val maxOffsetDelta: Float, | |
| private val itemState: () -> ItemState<SnappyDraggedItemInfo>?, | |
| ) { | |
| private val topCornerRadiusAnimator = Animatable(calcTopCornerRadius(itemState()!!, minCornerRadius(), maxCornerRadius()), Dp.VectorConverter) | |
| private val bottomCornerRadiusAnimator = Animatable(calcBottomCornerRadius(itemState()!!, minCornerRadius(), maxCornerRadius()), Dp.VectorConverter) | |
| var minCornerRadius by mutableStateOf(minCornerRadius) | |
| var maxCornerRadius by mutableStateOf(maxCornerRadius) | |
| var animationSpec by mutableStateOf(animationSpec) | |
| val shape: Shape | |
| get() { | |
| val minCornerRadius = minCornerRadius() | |
| val maxCornerRadius = maxCornerRadius() | |
| val top = topCornerRadiusAnimator.value.coerceIn(minCornerRadius, maxCornerRadius) | |
| val bottom = bottomCornerRadiusAnimator.value.coerceIn(minCornerRadius, maxCornerRadius) | |
| return RoundedCornerShape( | |
| topStart = top, | |
| topEnd = top, | |
| bottomStart = bottom, | |
| bottomEnd = bottom, | |
| ) | |
| } | |
| suspend fun observeChanges() = coroutineScope { | |
| combine( | |
| snapshotFlow { itemState() }.filterNotNull(), | |
| snapshotFlow { minCornerRadius() }, | |
| snapshotFlow { maxCornerRadius() }, | |
| snapshotFlow { animationSpec() }, | |
| ) { itemState, minCornerRadius, maxCornerRadius, animationSpec -> | |
| launch { topCornerRadiusAnimator.animateTo(calcTopCornerRadius(itemState, minCornerRadius, maxCornerRadius), animationSpec) } | |
| launch { bottomCornerRadiusAnimator.animateTo(calcBottomCornerRadius(itemState, minCornerRadius, maxCornerRadius), animationSpec) } | |
| }.collect() | |
| } | |
| private fun calcRadius(offsetDelta: Float, minCornerRadius: Dp, maxCornerRadius: Dp): Dp { | |
| val progress = (offsetDelta / maxOffsetDelta).coerceIn(0f, 1f) | |
| return (minCornerRadius + (maxCornerRadius - minCornerRadius) * progress) | |
| } | |
| private fun calcTopCornerRadius(itemState: ItemState<SnappyDraggedItemInfo>, minCornerRadius: Dp, maxCornerRadius: Dp): Dp { | |
| return when { | |
| !itemState.sameSegmentAsTopNeighbor || (itemState.isDraggedItem || itemState.draggedItemRelation?.indexDelta == 1) && !itemState.draggedItemRelation!!.draggedItemInfo.stuck -> maxCornerRadius | |
| else -> calcRadius(itemState.offsetDeltaTop, minCornerRadius, maxCornerRadius) | |
| } | |
| } | |
| private fun calcBottomCornerRadius(itemState: ItemState<SnappyDraggedItemInfo>, minCornerRadius: Dp, maxCornerRadius: Dp): Dp { | |
| return when { | |
| !itemState.sameSegmentAsBottomNeighbor || (itemState.isDraggedItem || itemState.draggedItemRelation?.indexDelta == -1) && !itemState.draggedItemRelation!!.draggedItemInfo.stuck -> maxCornerRadius | |
| else -> calcRadius(itemState.offsetDeltaBottom, minCornerRadius, maxCornerRadius) | |
| } | |
| } | |
| } | |
| @Composable | |
| fun rememberShapeHelper( | |
| minCornerRadius: () -> Dp, | |
| maxCornerRadius: () -> Dp, | |
| maxAtOffsetDelta: Dp, | |
| itemState: () -> ItemState<SnappyDraggedItemInfo>?, | |
| animationSpec: () -> AnimationSpec<Dp>, | |
| ): ShapeHelper { | |
| val density = LocalDensity.current | |
| val helper = remember { | |
| ShapeHelper( | |
| minCornerRadius = minCornerRadius, | |
| maxCornerRadius = maxCornerRadius, | |
| maxOffsetDelta = with(density) { maxAtOffsetDelta.toPx() }, | |
| itemState = itemState, | |
| animationSpec = animationSpec | |
| ) | |
| }.apply { | |
| this.minCornerRadius = minCornerRadius | |
| this.maxCornerRadius = maxCornerRadius | |
| this.animationSpec = animationSpec | |
| } | |
| LaunchedEffect(Unit) { | |
| helper.observeChanges() | |
| } | |
| DisposableEffect(Unit) { | |
| onDispose { | |
| d {"BYE"} | |
| } | |
| } | |
| return helper | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package de.kuno.snappyswipe.snappyswipe | |
| import kotlin.math.absoluteValue | |
| class SnappyDragHelper( | |
| private val key: Any?, | |
| private val unstickDistance: Float, | |
| private val restickDistance: Float, | |
| private val onStuck: () -> Unit = { }, | |
| private val onUnstuck: () -> Unit = { }, | |
| ) { | |
| var dragInfo: SnappyDraggedItemInfo? = null | |
| fun onDragStarted( | |
| initialOffset: Float, | |
| ) = SnappyDraggedItemInfo( | |
| key = key, | |
| dragOffset = initialOffset, | |
| stuck = initialOffset.absoluteValue < unstickDistance, | |
| unstuckProgress = (initialOffset.absoluteValue / unstickDistance).coerceAtMost(1f) | |
| ).also { | |
| dragInfo = it | |
| } | |
| fun updateDragInfo(dragDelta: Float): SnappyDraggedItemInfo { | |
| val currentDragInfo = requireNotNull(dragInfo) | |
| val newStuck = if (currentDragInfo.stuck) { | |
| currentDragInfo.dragOffset.absoluteValue < unstickDistance | |
| } else { | |
| currentDragInfo.dragOffset.absoluteValue < restickDistance | |
| } | |
| if (newStuck != currentDragInfo.stuck) { | |
| if (newStuck) { | |
| onStuck() | |
| } else { | |
| onUnstuck() | |
| } | |
| } | |
| val newDragOffset = currentDragInfo.dragOffset + dragDelta | |
| val newUnstuckProgress = if (newStuck) { | |
| (newDragOffset / unstickDistance).coerceIn(0f, 1f) | |
| } else { | |
| (newDragOffset / restickDistance).coerceIn(0f, 1f) | |
| } | |
| return currentDragInfo.copy( | |
| dragOffset = newDragOffset, | |
| stuck = newStuck, | |
| unstuckProgress = newUnstuckProgress | |
| ).also { | |
| dragInfo = it | |
| } | |
| } | |
| fun reset() { | |
| dragInfo = null | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package de.kuno.snappyswipe.snappyswipe | |
| import androidx.compose.animation.core.Animatable | |
| import androidx.compose.animation.core.FiniteAnimationSpec | |
| import androidx.compose.animation.core.spring | |
| import androidx.compose.foundation.gestures.Orientation | |
| import androidx.compose.foundation.gestures.draggable | |
| import androidx.compose.foundation.gestures.rememberDraggableState | |
| import androidx.compose.foundation.layout.Box | |
| import androidx.compose.foundation.layout.BoxScope | |
| import androidx.compose.foundation.layout.fillMaxWidth | |
| import androidx.compose.foundation.layout.offset | |
| import androidx.compose.foundation.layout.padding | |
| import androidx.compose.runtime.Composable | |
| import androidx.compose.runtime.DisposableEffect | |
| import androidx.compose.runtime.LaunchedEffect | |
| import androidx.compose.runtime.getValue | |
| import androidx.compose.runtime.mutableFloatStateOf | |
| import androidx.compose.runtime.mutableIntStateOf | |
| import androidx.compose.runtime.mutableStateOf | |
| import androidx.compose.runtime.remember | |
| import androidx.compose.runtime.setValue | |
| import androidx.compose.runtime.snapshotFlow | |
| import androidx.compose.ui.Modifier | |
| import androidx.compose.ui.graphics.graphicsLayer | |
| import androidx.compose.ui.hapticfeedback.HapticFeedbackType | |
| import androidx.compose.ui.layout.onSizeChanged | |
| import androidx.compose.ui.platform.LocalDensity | |
| import androidx.compose.ui.platform.LocalHapticFeedback | |
| import androidx.compose.ui.unit.Dp | |
| import androidx.compose.ui.unit.IntOffset | |
| import androidx.compose.ui.unit.dp | |
| import kotlinx.coroutines.Dispatchers | |
| import kotlinx.coroutines.flow.collect | |
| import kotlinx.coroutines.flow.combine | |
| import kotlinx.coroutines.flow.filterNotNull | |
| import kotlinx.coroutines.launch | |
| import kotlin.math.absoluteValue | |
| @Composable | |
| fun SnappyItem( | |
| key: Any, | |
| modifier: Modifier = Modifier, | |
| dragCoordinatorState: DragCoordinatorState<SnappyDraggedItemInfo>, | |
| onDismissed: () -> Unit, | |
| settings: SnappyDragSettings, | |
| content: @Composable BoxScope.(() -> ItemState<SnappyDraggedItemInfo>?) -> Unit, | |
| ) { | |
| var dismissing by remember(key) { mutableStateOf(false) } | |
| val offsetAnimatable = remember(key) { Animatable(0f) } | |
| var width by remember { mutableStateOf(0) } | |
| val density = LocalDensity.current | |
| val haptics = LocalHapticFeedback.current | |
| val snappyDragHelper = remember( | |
| key, | |
| settings.unstickDistance, | |
| settings.restickDistance | |
| ) { | |
| density.run { | |
| SnappyDragHelper( | |
| key = key, | |
| unstickDistance = settings.unstickDistance.toPx(), | |
| restickDistance = settings.restickDistance.toPx(), | |
| onStuck = { | |
| haptics.performHapticFeedback(HapticFeedbackType.SegmentTick) | |
| }, | |
| onUnstuck = { | |
| haptics.performHapticFeedback(HapticFeedbackType.Confirm) | |
| } | |
| ) | |
| } | |
| } | |
| LaunchedEffect(key) { | |
| snapshotFlow { offsetAnimatable.value }.collect { | |
| launch { | |
| dragCoordinatorState.updateOffset(key, it) | |
| } | |
| } | |
| } | |
| val itemState = remember(key) { | |
| { dragCoordinatorState.getItemState(key) } | |
| } | |
| val shapeHelper = rememberShapeHelper( | |
| minCornerRadius = { settings.minCornerRadius }, | |
| maxCornerRadius = { settings.maxCornerRadius }, | |
| maxAtOffsetDelta = settings.unstickDistance, | |
| animationSpec = { settings.cornerRadiusAnimationSpec }, | |
| itemState = itemState, | |
| ) | |
| LaunchedEffect(settings.holdDrag) { | |
| if (!settings.holdDrag && dragCoordinatorState.dragInfo?.key == key) { | |
| dragCoordinatorState.dragInfo = null | |
| } | |
| } | |
| LaunchedEffect(key) { | |
| combine( | |
| snapshotFlow { itemState() }.filterNotNull(), | |
| snapshotFlow { settings.affectedNeighbours }, | |
| ) { itemState, affectedNeighbours -> | |
| if (dismissing) return@combine | |
| launch(Dispatchers.Main.immediate) { | |
| if (itemState.draggedItemRelation == null) { | |
| snappyDragHelper.reset() | |
| offsetAnimatable.animateTo( | |
| 0f, | |
| settings.offsetAnimationSpec | |
| ) | |
| } else { | |
| val draggedItemRelation = itemState.draggedItemRelation | |
| val dragOffset = draggedItemRelation.draggedItemInfo.dragOffset | |
| val isAffected = draggedItemRelation.sameSegmentAsDraggedItem && draggedItemRelation.indexDelta.absoluteValue <= affectedNeighbours | |
| val offset = when { | |
| // Follow the drag offset. Add friction if stuck | |
| itemState.isDraggedItem -> dragOffset / if (draggedItemRelation.draggedItemInfo.stuck) settings.friction else 1f | |
| // Is one of the affected neighbours. The higher the distance to the dragged item, the less the offset | |
| draggedItemRelation.draggedItemInfo.stuck && isAffected -> dragOffset / (affectedNeighbours + 1) * ((affectedNeighbours + 1) - draggedItemRelation.indexDelta.absoluteValue) / settings.friction | |
| // Reset | |
| else -> 0f | |
| } | |
| if (itemState.isDraggedItem) { | |
| offsetAnimatable.animateTo( | |
| offset, | |
| spring() | |
| ) | |
| } else { | |
| offsetAnimatable.animateTo( | |
| offset, | |
| settings.offsetAnimationSpec | |
| ) | |
| } | |
| } | |
| } | |
| }.collect() | |
| } | |
| val draggedKey = { | |
| itemState()?.draggedItemRelation?.draggedItemInfo?.key | |
| } | |
| DisposableEffect(key) { | |
| onDispose { | |
| if (draggedKey() == key) { | |
| snappyDragHelper.reset() | |
| dragCoordinatorState.dragInfo = null | |
| } | |
| } | |
| } | |
| Box( | |
| modifier = modifier | |
| .onSizeChanged { | |
| width = it.width | |
| }.draggable( | |
| state = rememberDraggableState { | |
| if (draggedKey() == key) { | |
| snappyDragHelper.updateDragInfo(it) | |
| dragCoordinatorState.dragInfo = snappyDragHelper.dragInfo | |
| } | |
| }, | |
| orientation = Orientation.Horizontal, | |
| onDragStarted = { | |
| if (draggedKey() == null) { | |
| dismissing = false | |
| snappyDragHelper.onDragStarted(offsetAnimatable.value) | |
| dragCoordinatorState.dragInfo = snappyDragHelper.dragInfo | |
| } | |
| }, | |
| onDragStopped = { velocity -> | |
| if (draggedKey() == key) { | |
| if (settings.holdDrag) { | |
| return@draggable | |
| } | |
| val dragInfo = requireNotNull(snappyDragHelper.dragInfo) | |
| dragCoordinatorState.dragInfo = null | |
| val dismissRight = | |
| velocity >= DISMISS_MIN_VELOCITY || velocity >= 0f && !dragInfo.stuck && dragInfo.dragOffset >= 0f | |
| val dismissLeft = | |
| velocity <= -DISMISS_MIN_VELOCITY || velocity <= 0f && !dragInfo.stuck && dragInfo.dragOffset <= 0f | |
| if (dismissRight || dismissLeft) { | |
| if (dragInfo.stuck) { | |
| haptics.performHapticFeedback(HapticFeedbackType.Confirm) | |
| } | |
| dismissing = true | |
| offsetAnimatable.animateTo( | |
| targetValue = (if (dismissRight) width else -width).toFloat(), | |
| initialVelocity = velocity, | |
| ) | |
| onDismissed() | |
| } | |
| } | |
| } | |
| ), | |
| ) { | |
| Box( | |
| modifier = Modifier.padding(horizontal = 16.dp).fillMaxWidth().offset { | |
| IntOffset( | |
| offsetAnimatable.value.toInt(), | |
| 0 | |
| ) | |
| }.graphicsLayer { | |
| shape = shapeHelper.shape | |
| clip = true | |
| }, | |
| ) { | |
| content(itemState) | |
| } | |
| } | |
| } | |
| class SnappyDragSettings( | |
| unstickDistance: Dp = 100.dp, | |
| restickDistance: Dp = 50.dp, | |
| minCornerRadius: Dp = 0.dp, | |
| maxCornerRadius: Dp = 24.dp, | |
| affectedNeighbours: Int = 1, | |
| offsetAnimationSpec: FiniteAnimationSpec<Float> = spring(), | |
| cornerRadiusAnimationSpec: FiniteAnimationSpec<Dp> = spring(), | |
| friction: Float = 2f, | |
| holdDrag: Boolean = false, | |
| ) { | |
| var unstickDistance by mutableStateOf(unstickDistance) | |
| var restickDistance by mutableStateOf(restickDistance) | |
| var minCornerRadius by mutableStateOf(minCornerRadius) | |
| var maxCornerRadius by mutableStateOf(maxCornerRadius) | |
| var affectedNeighbours by mutableIntStateOf(affectedNeighbours) | |
| var friction by mutableFloatStateOf(friction) | |
| var holdDrag by mutableStateOf(holdDrag) | |
| var offsetAnimationSpec by mutableStateOf(offsetAnimationSpec) | |
| var cornerRadiusAnimationSpec by mutableStateOf(cornerRadiusAnimationSpec) | |
| } | |
| @Composable | |
| fun rememberSnappyDragSettings(): SnappyDragSettings { | |
| return remember { SnappyDragSettings() } | |
| } | |
| @Composable | |
| fun <T> rememberSnappyDragCoordinatorState( | |
| items: List<T>, | |
| key: (T) -> Any?, | |
| segmentType: (T) -> Any? = { null } | |
| ) = rememberDragCoordinatorState<SnappyDraggedItemInfo, T>( | |
| items = items, | |
| key = key, | |
| segmentType = segmentType, | |
| ) | |
| data class SnappyDraggedItemInfo( | |
| override val key: Any?, | |
| override val dragOffset: Float, | |
| val unstuckProgress: Float, | |
| val stuck: Boolean, | |
| ) : DraggedItemInfo | |
| private const val DISMISS_MIN_VELOCITY = 4000f |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment