Instantly share code, notes, and snippets.
Created
September 27, 2023 09:20
-
Star
(0)
0
You must be signed in to star a gist -
Fork
(0)
0
You must be signed in to fork a gist
-
Save chriswiesner/2117196ac854a5747cbcbd058c67f456 to your computer and use it in GitHub Desktop.
AnchoredDraggable Menu with List
This file contains 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
@OptIn(ExperimentalFoundationApi::class) | |
@Composable | |
fun AnchoredMenuListDemo() { | |
val density = LocalDensity.current | |
val state = remember { | |
AnchoredDraggableState( | |
initialValue = DragAnchors.Start, | |
anchors = DraggableAnchors { | |
DragAnchors.Start at 0f | |
DragAnchors.End at 0f | |
}, | |
positionalThreshold = { distance: Float -> distance * 0.3f }, | |
velocityThreshold = { with(density) { 100.dp.toPx() } }, | |
animationSpec = tween(), | |
) | |
} | |
var menuHeight by remember { mutableIntStateOf(0) } | |
var menuHeightDp by remember { mutableStateOf(0.dp) } | |
val coroutineScope = rememberCoroutineScope() | |
val localDensity = LocalDensity.current | |
val listState = rememberLazyListState() | |
val connection = remember { | |
object : NestedScrollConnection { | |
override fun onPostScroll( | |
consumed: Offset, | |
available: Offset, | |
source: NestedScrollSource | |
): Offset { | |
val delta = available.y | |
// if remaining velocity on bottom scroll -> forward to anchoredDraggable | |
if (delta < 0) { | |
return state.dispatchRawDelta(-delta).toYOffset() | |
} | |
return super.onPostScroll(consumed, available, source) | |
} | |
override fun onPreScroll( | |
available: Offset, | |
source: NestedScrollSource | |
): Offset { | |
val end = listState.isScrolledToEnd() | |
// prevent showing dummy list item | |
// consume remaining scroll if we are past last real item | |
if (available.y < 0 && end) { | |
state.dispatchRawDelta(-available.y) | |
return available | |
} | |
// pass scroll if list is on it's end - or sheet is expanded | |
if (end || state.currentValue == DragAnchors.End) { | |
val consumed = state.dispatchRawDelta(-available.y) | |
// return the consumed scroll (need to flip value again as we use reverse in AnchoredDraggable) | |
return consumed.absoluteValue.toYOffset() | |
} | |
return super.onPreScroll(available, source) | |
} | |
override suspend fun onPostFling( | |
consumed: androidx.compose.ui.unit.Velocity, | |
available: androidx.compose.ui.unit.Velocity | |
): androidx.compose.ui.unit.Velocity { | |
state.settle(-available.y) | |
return super.onPostFling(consumed, available) | |
} | |
private fun Float.toYOffset() = Offset(0f, this) | |
} | |
} | |
Box(Modifier.fillMaxSize()) { | |
Menu({ | |
(menuHeight - state.requireOffset().roundToInt()) | |
}, onHeightChange = { height -> | |
menuHeight = height | |
menuHeightDp = localDensity.run { height.toDp() } | |
state.updateAnchors( | |
DraggableAnchors { | |
DragAnchors.Start at 0f | |
DragAnchors.End at height.toFloat() | |
} | |
) | |
}) | |
LazyColumn( | |
state = listState, | |
verticalArrangement = Arrangement.spacedBy(6.dp), modifier = Modifier | |
.fillMaxWidth() | |
.nestedScroll(connection) | |
.anchoredDraggable(state, Orientation.Vertical, reverseDirection = true) | |
.offset { | |
IntOffset( | |
y = -state | |
.requireOffset() | |
.roundToInt(), | |
x = 0, | |
) | |
}) { | |
items(20) { | |
Box( | |
contentAlignment = Alignment.Center, modifier = | |
Modifier | |
.height(70.dp) | |
.fillMaxWidth() | |
.background(Color.LightGray) | |
) { | |
Text(it.toString()) | |
} | |
} | |
item { | |
Box( | |
Modifier | |
.height(10.dp) | |
.background(Color.Red) | |
.fillMaxWidth()) | |
} | |
} | |
} | |
} | |
@Composable | |
fun BoxScope.Menu(getYOffset: () -> Int, onHeightChange: (Int) -> Unit) { | |
var height by remember { mutableIntStateOf(0) } | |
Box( | |
Modifier | |
.offset { | |
IntOffset( | |
y = getYOffset(), | |
x = 0, | |
) | |
} | |
.onSizeChanged { | |
height = it.height | |
onHeightChange(it.height) | |
} | |
.fillMaxWidth() | |
.align(Alignment.BottomCenter) | |
) { | |
Column( | |
horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier | |
.fillMaxWidth() | |
.padding(16.dp) | |
) { | |
Text( | |
"Menu", | |
style = MaterialTheme.typography.headlineLarge.copy(textAlign = TextAlign.Center), | |
modifier = Modifier.fillMaxWidth() | |
) | |
Spacer(Modifier.height(16.dp)) | |
Text( | |
"Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. ", | |
style = MaterialTheme.typography.bodyLarge | |
) | |
Button(onClick = {}) { | |
Text("Button") | |
} | |
} | |
} | |
} | |
@Preview(showBackground = true) | |
@Composable | |
private fun MenuPreview() { | |
Box { | |
Menu({ 0 }, {}) | |
} | |
} | |
enum class DragAnchors { | |
Start, | |
End, | |
} | |
fun LazyListState.isScrolledToEnd() = layoutInfo.visibleItemsInfo.lastOrNull()?.index == layoutInfo.totalItemsCount - 1 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment