diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/AdaptiveSheet.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/AdaptiveSheet.kt index d9f90ad54..aab3efcec 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/AdaptiveSheet.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/AdaptiveSheet.kt @@ -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(durationMillis = SheetAnimationDuration) +private val sheetAnimationSpec = tween(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 SwipeableState.preUpPostDownNestedScrollConnection( - enabled: Boolean = true, - anchor: Map, -) = object : NestedScrollConnection { +private fun AnchoredDraggableState.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 SwipeableState.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 SwipeableState.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 }