diff --git a/android/app/src/main/java/com/fabledsword/minstrel/player/ui/NowPlayingScreen.kt b/android/app/src/main/java/com/fabledsword/minstrel/player/ui/NowPlayingScreen.kt index a1a7af98..074a8e3e 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/player/ui/NowPlayingScreen.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/player/ui/NowPlayingScreen.kt @@ -22,7 +22,11 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.foundation.gestures.detectVerticalDragGestures +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.unit.Velocity import androidx.compose.material3.Scaffold import androidx.compose.material3.Slider import androidx.compose.material3.TopAppBar @@ -30,7 +34,6 @@ import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color -import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.material3.SliderDefaults import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState @@ -122,8 +125,11 @@ fun NowPlayingScreen( return } val dominant = rememberDominantColor(track.coverUrl) + val dismissConnection = rememberDragDismissConnection( + onDismiss = { navController.popBackStack() }, + ) Scaffold( - modifier = Modifier.fillMaxSize().nowPlayingDragDismiss(navController), + modifier = Modifier.fillMaxSize().nestedScroll(dismissConnection), topBar = { NowPlayingTopBar(onClose = { navController.popBackStack() }) }, snackbarHost = { SnackbarHost(snackbarHostState) }, containerColor = Color.Transparent, @@ -161,27 +167,50 @@ private const val DOMINANT_TINT_MID_STOP = 0.45f /** * Drag-down to dismiss. Mirrors Flutter's modal-page gesture: a - * downward drag past the threshold pops the screen, returning the - * user to whichever shell route launched NowPlaying. Tap, swipe-up, - * and horizontal drags pass through to the underlying content - * (slider scrub, etc.). + * downward over-scroll past the threshold pops the screen. + * + * Implementation note: the body Column uses verticalScroll, which + * eats every vertical drag delta before a top-level + * detectVerticalDragGestures can see it. NestedScroll is the + * right primitive here — verticalScroll offers its over-scroll + * deltas to the nearest ancestor connection, so we accumulate + * downward over-scroll (available.y > 0 means the scrollable + * couldn't consume it because it's at the top) and pop on + * threshold. The Slider keeps its own pointerInput and is + * unaffected. */ @Composable -private fun Modifier.nowPlayingDragDismiss(navController: NavHostController): Modifier = - this.pointerInput(Unit) { - var totalDragY = 0f - detectVerticalDragGestures( - onDragStart = { totalDragY = 0f }, - onDragEnd = { - if (totalDragY > DRAG_DISMISS_THRESHOLD_PX) { - navController.popBackStack() +private fun rememberDragDismissConnection( + onDismiss: () -> Unit, + thresholdPx: Float = DRAG_DISMISS_THRESHOLD_PX, +): NestedScrollConnection = remember(onDismiss, thresholdPx) { + object : NestedScrollConnection { + private var accumulated = 0f + + override fun onPostScroll( + consumed: Offset, + available: Offset, + source: NestedScrollSource, + ): Offset { + if (source != NestedScrollSource.UserInput) return Offset.Zero + if (available.y > 0f) { + accumulated += available.y + if (accumulated >= thresholdPx) { + accumulated = 0f + onDismiss() } - totalDragY = 0f - }, - onDragCancel = { totalDragY = 0f }, - onVerticalDrag = { _, delta -> totalDragY += delta }, - ) + return Offset(0f, available.y) + } + if (consumed.y != 0f || available.y < 0f) accumulated = 0f + return Offset.Zero + } + + override suspend fun onPreFling(available: Velocity): Velocity { + accumulated = 0f + return Velocity.Zero + } } +} @OptIn(ExperimentalMaterial3Api::class) @Composable