|
@@ -4,8 +4,13 @@ import androidx.activity.compose.BackHandler
|
|
|
import androidx.compose.animation.core.animateFloatAsState
|
|
|
import androidx.compose.animation.core.tween
|
|
|
import androidx.compose.foundation.clickable
|
|
|
+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.interaction.MutableInteractionSource
|
|
|
+import androidx.compose.foundation.layout.Box
|
|
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
|
|
import androidx.compose.foundation.layout.WindowInsets
|
|
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
|
@@ -18,9 +23,6 @@ import androidx.compose.foundation.layout.systemBars
|
|
|
import androidx.compose.foundation.layout.systemBarsPadding
|
|
|
import androidx.compose.foundation.layout.widthIn
|
|
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
|
|
-import androidx.compose.material.SwipeableState
|
|
|
-import androidx.compose.material.rememberSwipeableState
|
|
|
-import androidx.compose.material.swipeable
|
|
|
import androidx.compose.material3.MaterialTheme
|
|
|
import androidx.compose.material3.Surface
|
|
|
import androidx.compose.runtime.Composable
|
|
@@ -38,6 +40,8 @@ import androidx.compose.ui.geometry.Offset
|
|
|
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.layout.onSizeChanged
|
|
|
+import androidx.compose.ui.platform.LocalDensity
|
|
|
import androidx.compose.ui.unit.Dp
|
|
|
import androidx.compose.ui.unit.IntOffset
|
|
|
import androidx.compose.ui.unit.Velocity
|
|
@@ -48,8 +52,7 @@ import kotlinx.coroutines.flow.filter
|
|
|
import kotlinx.coroutines.launch
|
|
|
import kotlin.math.roundToInt
|
|
|
|
|
|
-private const val SheetAnimationDuration = 350
|
|
|
-private val SheetAnimationSpec = tween<Float>(durationMillis = SheetAnimationDuration)
|
|
|
+private val sheetAnimationSpec = tween<Float>(durationMillis = 350)
|
|
|
|
|
|
@Composable
|
|
|
fun AdaptiveSheet(
|
|
@@ -59,12 +62,13 @@ fun AdaptiveSheet(
|
|
|
onDismissRequest: () -> Unit,
|
|
|
content: @Composable () -> Unit,
|
|
|
) {
|
|
|
+ val density = LocalDensity.current
|
|
|
val scope = rememberCoroutineScope()
|
|
|
if (isTabletUi) {
|
|
|
var targetAlpha by remember { mutableFloatStateOf(0f) }
|
|
|
val alpha by animateFloatAsState(
|
|
|
targetValue = targetAlpha,
|
|
|
- animationSpec = SheetAnimationSpec,
|
|
|
+ animationSpec = sheetAnimationSpec,
|
|
|
)
|
|
|
val internalOnDismissRequest: () -> Unit = {
|
|
|
scope.launch {
|
|
@@ -107,23 +111,36 @@ fun AdaptiveSheet(
|
|
|
}
|
|
|
}
|
|
|
} else {
|
|
|
- val swipeState = rememberSwipeableState(
|
|
|
- initialValue = 1,
|
|
|
- animationSpec = SheetAnimationSpec,
|
|
|
- )
|
|
|
- val internalOnDismissRequest: () -> Unit = { if (swipeState.currentValue == 0) scope.launch { swipeState.animateTo(1) } }
|
|
|
- BoxWithConstraints(
|
|
|
+ val anchoredDraggableState = remember {
|
|
|
+ AnchoredDraggableState(
|
|
|
+ initialValue = 1,
|
|
|
+ animationSpec = sheetAnimationSpec,
|
|
|
+ positionalThreshold = { with(density) { 56.dp.toPx() } },
|
|
|
+ velocityThreshold = { with(density) { 125.dp.toPx() } },
|
|
|
+ )
|
|
|
+ }
|
|
|
+ val internalOnDismissRequest = {
|
|
|
+ if (anchoredDraggableState.currentValue == 0) {
|
|
|
+ scope.launch { anchoredDraggableState.animateTo(1) }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ Box(
|
|
|
modifier = Modifier
|
|
|
.clickable(
|
|
|
interactionSource = remember { MutableInteractionSource() },
|
|
|
indication = null,
|
|
|
onClick = internalOnDismissRequest,
|
|
|
)
|
|
|
- .fillMaxSize(),
|
|
|
+ .fillMaxSize()
|
|
|
+ .onSizeChanged {
|
|
|
+ val anchors = DraggableAnchors {
|
|
|
+ 0 at 0f
|
|
|
+ 1 at it.height.toFloat()
|
|
|
+ }
|
|
|
+ anchoredDraggableState.updateAnchors(anchors)
|
|
|
+ },
|
|
|
contentAlignment = Alignment.BottomCenter,
|
|
|
) {
|
|
|
- val fullHeight = constraints.maxHeight.toFloat()
|
|
|
- val anchors = mapOf(0f to 0, fullHeight to 1)
|
|
|
Surface(
|
|
|
modifier = Modifier
|
|
|
.widthIn(max = 460.dp)
|
|
@@ -132,26 +149,27 @@ fun AdaptiveSheet(
|
|
|
indication = null,
|
|
|
onClick = {},
|
|
|
)
|
|
|
- .nestedScroll(
|
|
|
- remember(enableSwipeDismiss, anchors) {
|
|
|
- swipeState.preUpPostDownNestedScrollConnection(
|
|
|
- enabled = enableSwipeDismiss,
|
|
|
- anchor = anchors,
|
|
|
+ .then(
|
|
|
+ if (enableSwipeDismiss) {
|
|
|
+ Modifier.nestedScroll(
|
|
|
+ remember(anchoredDraggableState) {
|
|
|
+ anchoredDraggableState.preUpPostDownNestedScrollConnection()
|
|
|
+ },
|
|
|
)
|
|
|
+ } else {
|
|
|
+ Modifier
|
|
|
},
|
|
|
)
|
|
|
.offset {
|
|
|
IntOffset(
|
|
|
0,
|
|
|
- swipeState.offset.value.roundToInt(),
|
|
|
+ anchoredDraggableState.offset.takeIf { it.isFinite() }?.roundToInt() ?: 0,
|
|
|
)
|
|
|
}
|
|
|
- .swipeable(
|
|
|
- enabled = enableSwipeDismiss,
|
|
|
- state = swipeState,
|
|
|
- anchors = anchors,
|
|
|
+ .anchoredDraggable(
|
|
|
+ state = anchoredDraggableState,
|
|
|
orientation = Orientation.Vertical,
|
|
|
- resistance = null,
|
|
|
+ enabled = enableSwipeDismiss,
|
|
|
)
|
|
|
.windowInsetsPadding(
|
|
|
WindowInsets.systemBars
|
|
@@ -160,14 +178,14 @@ fun AdaptiveSheet(
|
|
|
shape = MaterialTheme.shapes.extraLarge,
|
|
|
tonalElevation = tonalElevation,
|
|
|
content = {
|
|
|
- BackHandler(enabled = swipeState.targetValue == 0, onBack = internalOnDismissRequest)
|
|
|
+ BackHandler(enabled = anchoredDraggableState.targetValue == 0, onBack = internalOnDismissRequest)
|
|
|
content()
|
|
|
},
|
|
|
)
|
|
|
|
|
|
- LaunchedEffect(swipeState) {
|
|
|
- scope.launch { swipeState.animateTo(0) }
|
|
|
- snapshotFlow { swipeState.currentValue }
|
|
|
+ LaunchedEffect(anchoredDraggableState) {
|
|
|
+ scope.launch { anchoredDraggableState.animateTo(0) }
|
|
|
+ snapshotFlow { anchoredDraggableState.currentValue }
|
|
|
.drop(1)
|
|
|
.filter { it == 1 }
|
|
|
.collectLatest {
|
|
@@ -178,17 +196,11 @@ fun AdaptiveSheet(
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-/**
|
|
|
- * Yoinked from Swipeable.kt with modifications to disable
|
|
|
- */
|
|
|
-private fun <T> SwipeableState<T>.preUpPostDownNestedScrollConnection(
|
|
|
- enabled: Boolean = true,
|
|
|
- anchor: Map<Float, T>,
|
|
|
-) = object : NestedScrollConnection {
|
|
|
+private fun <T> AnchoredDraggableState<T>.preUpPostDownNestedScrollConnection() = object : NestedScrollConnection {
|
|
|
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
|
|
val delta = available.toFloat()
|
|
|
- return if (enabled && delta < 0 && source == NestedScrollSource.Drag) {
|
|
|
- performDrag(delta).toOffset()
|
|
|
+ return if (delta < 0 && source == NestedScrollSource.Drag) {
|
|
|
+ dispatchRawDelta(delta).toOffset()
|
|
|
} else {
|
|
|
Offset.Zero
|
|
|
}
|
|
@@ -199,17 +211,17 @@ private fun <T> SwipeableState<T>.preUpPostDownNestedScrollConnection(
|
|
|
available: Offset,
|
|
|
source: NestedScrollSource,
|
|
|
): Offset {
|
|
|
- return if (enabled && source == NestedScrollSource.Drag) {
|
|
|
- performDrag(available.toFloat()).toOffset()
|
|
|
+ return if (source == NestedScrollSource.Drag) {
|
|
|
+ dispatchRawDelta(available.toFloat()).toOffset()
|
|
|
} else {
|
|
|
Offset.Zero
|
|
|
}
|
|
|
}
|
|
|
|
|
|
override suspend fun onPreFling(available: Velocity): Velocity {
|
|
|
- val toFling = Offset(available.x, available.y).toFloat()
|
|
|
- return if (enabled && toFling < 0 && offset.value > anchor.keys.minOrNull()!!) {
|
|
|
- performFling(velocity = toFling)
|
|
|
+ val toFling = available.toFloat()
|
|
|
+ return if (toFling < 0 && offset > anchors.minAnchor()) {
|
|
|
+ settle(toFling)
|
|
|
// since we go to the anchor with tween settling, consume all for the best UX
|
|
|
available
|
|
|
} else {
|
|
@@ -218,15 +230,15 @@ private fun <T> SwipeableState<T>.preUpPostDownNestedScrollConnection(
|
|
|
}
|
|
|
|
|
|
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
|
|
|
- return if (enabled) {
|
|
|
- performFling(velocity = Offset(available.x, available.y).toFloat())
|
|
|
- available
|
|
|
- } else {
|
|
|
- Velocity.Zero
|
|
|
- }
|
|
|
+ settle(velocity = available.toFloat())
|
|
|
+ return available
|
|
|
}
|
|
|
|
|
|
private fun Float.toOffset(): Offset = Offset(0f, this)
|
|
|
|
|
|
+ @JvmName("velocityToFloat")
|
|
|
+ private fun Velocity.toFloat() = this.y
|
|
|
+
|
|
|
+ @JvmName("offsetToFloat")
|
|
|
private fun Offset.toFloat(): Float = this.y
|
|
|
}
|