components: ReorderableColumn resists dragging past the ends

This commit is contained in:
2026-09-23 17:24:36 +02:00
parent 1e49fd5067
commit 8ed9b1dbdc
@@ -84,6 +84,8 @@ fun <T> ReorderableColumn(
// release settle — re-based onto the row's new slot once the order commits
// (see onDragEnd), then eased down to zero.
var dragOffset by remember { mutableFloatStateOf(0f) }
// The finger's own travel; [dragOffset] is this held to the list's ends.
var rawOffset by remember { mutableFloatStateOf(0f) }
// The running release/cancel settle, cancelled if a new drag pre-empts it.
// Purely visual — the order commit (see onDragEnd) never depends on it.
var settleJob by remember { mutableStateOf<Job?>(null) }
@@ -131,10 +133,13 @@ fun <T> ReorderableColumn(
settleJob?.cancel()
draggedKey = key
dragOffset = 0f
rawOffset = 0f
},
onDrag = { change, amount ->
change.consume()
dragOffset += amount.y
rawOffset += amount.y
val from = order.indexOfFirst { keyOf(it) == key }
dragOffset = rubberBand(rawOffset, -from * pitchPx, (order.lastIndex - from) * pitchPx, pitchPx * 0.4f)
},
onDragEnd = {
val from = order.indexOfFirst { keyOf(it) == key }
@@ -206,3 +211,16 @@ private class InsetShape(private val base: Shape, private val inset: Dp) : Shape
}
}
}
/**
* [value] as is between [min] and [max]; past either end it keeps following with
* growing resistance and never gets further than [stretch] beyond it.
*/
private fun rubberBand(value: Float, min: Float, max: Float, stretch: Float): Float {
fun resist(over: Float) = stretch * (1f - 1f / (over / stretch + 1f))
return when {
value > max -> max + resist(value - max)
value < min -> min - resist(min - value)
else -> value
}
}