Skip to content

Instantly share code, notes, and snippets.

@kokeroulis
Created June 2, 2025 18:28
Show Gist options
  • Save kokeroulis/c7757c205fe3260274a08932b2ed58d0 to your computer and use it in GitHub Desktop.
Save kokeroulis/c7757c205fe3260274a08932b2ed58d0 to your computer and use it in GitHub Desktop.
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.AnchoredDraggableDefaults
import androidx.compose.foundation.gestures.AnchoredDraggableState
import androidx.compose.foundation.gestures.DraggableAnchors
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.anchoredDraggable
import androidx.compose.foundation.gestures.forEach
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import kotlin.math.roundToInt
enum class ExpansionState {
COLLAPSED,
HALF_COLLAPSE,
EXPANDED
}
@Preview
@Composable
fun DraggableAnchorsSample() {
val state = rememberSaveable(saver = AnchoredDraggableState.Saver()) {
AnchoredDraggableState(initialValue = ExpansionState.EXPANDED)
}
val singleDpToPx = with(LocalDensity.current) { 1.dp.toPx() }
SideEffect {
state.updateAnchors(
DraggableAnchors {
ExpansionState.EXPANDED at 300 * singleDpToPx
ExpansionState.HALF_COLLAPSE at 225 * singleDpToPx
ExpansionState.COLLAPSED at 150 * singleDpToPx
},
)
}
Box(
Modifier
.fillMaxWidth()
.visualizeDraggableAnchors(state, Orientation.Vertical),
) {
// Header(
// anchorModifier = Modifier.anchoredDraggable(
// state = state,
// orientation = Orientation.Vertical,
// flingBehavior =
// AnchoredDraggableDefaults.flingBehavior(
// state,
// positionalThreshold = { distance -> distance * 0.25f },
// ),
// ),
// offsetModifier = Modifier
// .offset {
// IntOffset(
// x = 0, y = state.requireOffset().roundToInt(),
// )
// },
// ) {
// val dp = with(LocalDensity.current) {
// state.offset.toDp()
// }
// Text("state $dp ${state.offset} ${state.targetValue} ${state.currentValue}")
// }
val connection = remember { DraggableConnection(state) }
val containerHeight = with(LocalDensity.current) { 150.dp.toPx().roundToInt() }
SampleList(
Modifier
// this is breaking it
.offset { IntOffset(0, 100.dp.roundToPx()) }
//.offset { IntOffset(0, y = containerHeight + state.requireOffset().roundToInt()) }
//.nestedScroll(connection),
)
}
}
@Composable
fun Header(anchorModifier: Modifier, offsetModifier: Modifier, content: @Composable () -> Unit) {
Box(
Modifier
.fillMaxWidth()
.height(450.dp)
// .then(anchorModifier),
)
{
Box(
Modifier
.fillMaxWidth()
// .then(offsetModifier)
.height(150.dp)
.background(Color.Red),
)
Box(
Modifier
.fillMaxWidth()
.height(300.dp)
.background(Color.Blue),
) {
content()
}
}
}
class DraggableConnection(
private val anchorState: AnchoredDraggableState<ExpansionState>
) : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val newFloat = anchorState.dispatchRawDelta(available.y)
return available.copy(y = newFloat)
}
}
@Composable
fun SampleList(modifier: Modifier = Modifier) {
val sampleItems = List(20) { "Item #$it" }
LazyColumn(modifier) {
items(sampleItems) { item ->
Box(
Modifier
.fillMaxWidth()
.height(50.dp)
.background(Color.Cyan),
) {
Text("ITEM: $item")
}
}
}
}
private fun Modifier.visualizeDraggableAnchors(
state: AnchoredDraggableState<*>,
orientation: Orientation,
lineColor: Color = Color.Black,
lineStrokeWidth: Float = 10f,
linePathEffect: PathEffect = PathEffect.dashPathEffect(floatArrayOf(20f, 30f)),
) = drawWithContent {
drawContent()
state.anchors.forEach { _, position ->
val startOffset =
Offset(
x = if (orientation == Orientation.Horizontal) position else 0f,
y = if (orientation == Orientation.Vertical) position else 0f,
)
val endOffset =
Offset(
x = if (orientation == Orientation.Horizontal) startOffset.x else size.height,
y = if (orientation == Orientation.Vertical) startOffset.y else size.width,
)
drawLine(
color = lineColor,
start = startOffset,
end = endOffset,
strokeWidth = lineStrokeWidth,
pathEffect = linePathEffect,
)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment