Rework bottom sheets

This commit is contained in:
MM20 2023-08-15 20:56:18 +02:00
parent e502c18de8
commit 21f263b7d0
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389

View File

@ -3,8 +3,13 @@ package de.mm20.launcher2.ui.component
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
import androidx.compose.foundation.background
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.animateTo
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@ -15,11 +20,15 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.safeContent
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsBottomHeight
@ -56,11 +65,12 @@ import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupProperties
import de.mm20.launcher2.ui.ktx.toDp
import de.mm20.launcher2.ui.ktx.toPixels
import kotlinx.coroutines.launch
import kotlin.math.min
import kotlin.math.roundToInt
@Composable
@ -74,21 +84,29 @@ fun BottomSheetDialog(
content: @Composable (paddingValues: PaddingValues) -> Unit,
) {
val scope = rememberCoroutineScope()
val swipeState = remember {
SwipeableState(
var isOpenAnimationFinished by remember { mutableStateOf(false) }
val draggableState = remember {
AnchoredDraggableState(
initialValue = SwipeState.Dismiss,
confirmStateChange = {
if (it == SwipeState.Dismiss) {
if (dismissible()) {
onDismissRequest()
}
else return@SwipeableState false
}
return@SwipeableState true
},
positionalThreshold = { it * 0.5f },
velocityThreshold = { 200f },
animationSpec = spring(),
confirmValueChange = {
it != SwipeState.Dismiss || dismissible()
}
)
}
LaunchedEffect(draggableState.currentValue) {
if (isOpenAnimationFinished && draggableState.currentValue == SwipeState.Dismiss) {
onDismissRequest()
} else {
isOpenAnimationFinished = true
}
}
val nestedScrollConnection = remember {
object : NestedScrollConnection {
@ -96,7 +114,7 @@ fun BottomSheetDialog(
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val delta = available.toFloat()
return if (delta < 0 && source == NestedScrollSource.Drag) {
swipeState.performDrag(delta).toOffset()
draggableState.dispatchRawDelta(delta).toOffset()
} else {
Offset.Zero
}
@ -108,7 +126,7 @@ fun BottomSheetDialog(
source: NestedScrollSource
): Offset {
return if (source == NestedScrollSource.Drag) {
swipeState.performDrag(available.toFloat()).toOffset()
draggableState.dispatchRawDelta(available.toFloat()).toOffset()
} else {
Offset.Zero
}
@ -116,8 +134,8 @@ fun BottomSheetDialog(
override suspend fun onPreFling(available: Velocity): Velocity {
val toFling = Offset(available.x, available.y).toFloat()
return if (toFling < 0 && swipeState.offset.value > 0) {
swipeState.performFling(velocity = toFling)
return if (toFling < 0 && draggableState.offset > draggableState.anchors.minAnchor()) {
draggableState.settle(velocity = toFling)
// since we go to the anchor with tween settling, consume all for the best UX
available
} else {
@ -126,7 +144,7 @@ fun BottomSheetDialog(
}
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
swipeState.performFling(velocity = Offset(available.x, available.y).toFloat())
draggableState.settle(velocity = Offset(available.x, available.y).toFloat())
return available
}
@ -139,23 +157,25 @@ fun BottomSheetDialog(
CompositionLocalProvider(
LocalAbsoluteTonalElevation provides 0.dp,
) {
Dialog(
properties = DialogProperties(
Popup(
properties = PopupProperties(
dismissOnBackPress = dismissible(),
dismissOnClickOutside = dismissible(),
usePlatformDefaultWidth = false,
decorFitsSystemWindows = false,
focusable = true,
),
onDismissRequest = onDismissRequest,
) {
BoxWithConstraints(
modifier = Modifier.fillMaxSize(),
modifier = Modifier
.fillMaxSize()
.consumeWindowInsets(WindowInsets.systemBars),
propagateMinConstraints = true,
contentAlignment = Alignment.BottomCenter
) {
val maxHeightPx = maxHeight.toPixels()
val maxHeight = maxHeight
val scrimAlpha by animateFloatAsState(
if (swipeState.targetValue == SwipeState.Dismiss) 0f else 0.32f,
if (draggableState.targetValue == SwipeState.Dismiss) 0f else 0.32f,
label = "Scrim alpha"
)
@ -166,7 +186,7 @@ fun BottomSheetDialog(
detectTapGestures {
if (dismissible()) {
scope.launch {
swipeState.animateTo(SwipeState.Dismiss)
draggableState.animateTo(SwipeState.Dismiss)
onDismissRequest()
}
}
@ -174,53 +194,60 @@ fun BottomSheetDialog(
}
)
Column(
Box(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(Alignment.Bottom)
.fillMaxHeight()
.clipToBounds(),
verticalArrangement = Arrangement.Bottom
contentAlignment = Alignment.TopCenter,
) {
var height by remember {
mutableStateOf(maxHeightPx)
var sheetHeight by remember {
mutableStateOf(0f)
}
LaunchedEffect(null) {
swipeState.animateTo(SwipeState.Peek)
}
val heightDp = height.toDp()
val peekHeight = (height - maxHeightPx / 2).coerceAtLeast(0f)
val anchors = mutableMapOf(
peekHeight to SwipeState.Peek,
height to SwipeState.Dismiss,
).also {
if (peekHeight > 0f) {
it[0f] = SwipeState.Full
val maxHeightPx = maxHeight.toPixels()
LaunchedEffect(maxHeightPx, sheetHeight) {
val oldValue = draggableState.currentValue
val hasPeekAnchor = sheetHeight > 0f
val hasFullAnchor = sheetHeight > maxHeightPx * 0.5f
// If the sheet was hidden, move it to peek. Otherwise, try to keep the previous state, if possible.
val newValue = when {
oldValue == SwipeState.Dismiss && hasPeekAnchor -> SwipeState.Peek
oldValue == SwipeState.Peek && hasPeekAnchor -> SwipeState.Peek
oldValue == SwipeState.Full && hasFullAnchor -> SwipeState.Full
oldValue == SwipeState.Full && hasPeekAnchor -> SwipeState.Peek
else -> SwipeState.Dismiss
}
draggableState.updateAnchors(
DraggableAnchors {
SwipeState.Dismiss at 0f
if (hasPeekAnchor) SwipeState.Peek at -min(maxHeightPx * 0.5f, sheetHeight)
if (hasFullAnchor) SwipeState.Full at -min(maxHeightPx, sheetHeight)
},
)
if (newValue != oldValue) {
draggableState.animateTo(newValue)
}
}
Surface(
modifier = Modifier
.nestedScroll(nestedScrollConnection)
.onSizeChanged {
height = it.height.toFloat()
sheetHeight = it.height.toFloat()
}
.offset { IntOffset(0, swipeState.offset.value.roundToInt()) }
.swipeable(
swipeState,
anchors = anchors,
.offset {
IntOffset(0,
maxHeightPx.toInt() +
(draggableState.offset
.takeIf { !it.isNaN() }
?.roundToInt() ?: 0)
)
}
.anchoredDraggable(
state = draggableState,
orientation = Orientation.Vertical,
thresholds = { _, to ->
if (to == SwipeState.Dismiss) {
FixedThreshold(heightDp - 48.dp)
} else {
FractionalThreshold(0.5f)
}
},
resistance = null,
)
.fillMaxWidth()
.weight(1f, false),
.fillMaxWidth(),
shape = MaterialTheme.shapes.extraLarge.copy(
bottomStart = CornerSize(0),
bottomEnd = CornerSize(0),
@ -246,8 +273,12 @@ fun BottomSheetDialog(
Box(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.animateContentSize(),
.then(
if (confirmButton != null || dismissButton != null) Modifier.padding(
bottom = 64.dp
) else Modifier
)
.wrapContentHeight(),
propagateMinConstraints = true,
contentAlignment = Alignment.Center
) {
@ -258,10 +289,20 @@ fun BottomSheetDialog(
}
if (confirmButton != null || dismissButton != null) {
val elevation by animateDpAsState(if (swipeState.offset.value == 0f) 0.dp else 1.dp)
val elevation = 1.dp
val heightPx = -64.dp.toPixels()
Surface(
modifier = Modifier
.wrapContentHeight()
.height(64.dp)
.offset {
IntOffset(
0,
maxHeightPx.toInt() +
(draggableState.offset
.takeIf { !it.isNaN() }
?.roundToInt() ?: 0).coerceAtLeast(heightPx.toInt())
)
}
.fillMaxWidth(),
tonalElevation = elevation,
) {