components: ReorderableColumn drag shadow follows the card

This commit is contained in:
2026-09-23 17:13:07 +02:00
parent d5f5ad7b9d
commit 1e49fd5067
@@ -11,7 +11,6 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
@@ -21,10 +20,18 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.geometry.translate
import androidx.compose.ui.graphics.Outline
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import kotlinx.coroutines.Job
@@ -53,7 +60,8 @@ private val RowGap: Dp = 2.dp
* [GroupedRow]'s card shaping — pass `gapBelow = false` there, this owns spacing)
* and a `dragHandle` [Modifier] to attach to the element that starts a drag.
* Pass [rowHeight] matching the rows' non-reorder `minHeight`, so they keep their
* size when a screen switches into reordering.
* size when a screen switches into reordering. [rowInset] is the card's horizontal
* inset inside its slot, so the held row's shadow hugs the card, not the slot.
*/
@Composable
fun <T> ReorderableColumn(
@@ -62,6 +70,7 @@ fun <T> ReorderableColumn(
onReorder: (List<T>) -> Unit,
modifier: Modifier = Modifier,
rowHeight: Dp = ReorderableRowHeight,
rowInset: Dp = GroupedListInset,
rowContent: @Composable (item: T, position: Position, dragHandle: Modifier, isDragging: Boolean) -> Unit,
) {
val pitchPx = with(LocalDensity.current) { (rowHeight + RowGap).toPx() }
@@ -174,7 +183,7 @@ fun <T> ReorderableColumn(
scaleX = 1.02f
scaleY = 1.02f
shadowElevation = 8.dp.toPx()
shape = RoundedCornerShape(20.dp)
shape = InsetShape(groupedShape(positionOf(index, order.size), 22.dp, 6.dp), rowInset)
clip = false
}
},
@@ -184,3 +193,16 @@ fun <T> ReorderableColumn(
}
}
}
/** [base] laid out [inset] in from both sides of the slot. */
private class InsetShape(private val base: Shape, private val inset: Dp) : Shape {
override fun createOutline(size: Size, layoutDirection: LayoutDirection, density: Density): Outline {
val dx = with(density) { inset.toPx() }
val inner = Size((size.width - 2 * dx).coerceAtLeast(0f), size.height)
return when (val outline = base.createOutline(inner, layoutDirection, density)) {
is Outline.Rectangle -> Outline.Rectangle(outline.rect.translate(Offset(dx, 0f)))
is Outline.Rounded -> Outline.Rounded(outline.roundRect.translate(Offset(dx, 0f)))
is Outline.Generic -> Outline.Generic(Path().apply { addPath(outline.path, Offset(dx, 0f)) })
}
}
}