feat(android): NowPlaying drag-down-to-dismiss + close button
Audit v2 missing UX: full player had no way to escape except system back. Two additions: - Chevron-down close button as the Scaffold topBar navigation icon. Transparent background so the gradient/cover behind shows through. - Vertical drag-down on the whole screen pops back past a 200px threshold, mirroring Flutter's modal-page gesture. Horizontal scrubs on the slider still work because the gesture detector is specifically vertical-only. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -17,8 +17,14 @@ 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.material3.Scaffold
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
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
|
||||
@@ -38,6 +44,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavHostController
|
||||
import coil3.compose.AsyncImage
|
||||
import com.composables.icons.lucide.ChevronDown
|
||||
import com.composables.icons.lucide.ListMusic
|
||||
import com.composables.icons.lucide.Lucide
|
||||
import com.composables.icons.lucide.Music
|
||||
@@ -64,6 +71,12 @@ private const val MS_PER_SECOND = 1000L
|
||||
private const val SECONDS_PER_MINUTE = 60L
|
||||
private const val POP_GRACE_MS = 500L
|
||||
|
||||
// Vertical drag-down threshold (in pixels) past which the gesture
|
||||
// pops the player. Matches Flutter's 80px threshold in spirit;
|
||||
// using pointer-input pixels not dp because gesture velocity also
|
||||
// participates in the dismiss decision.
|
||||
private const val DRAG_DISMISS_THRESHOLD_PX = 200f
|
||||
|
||||
/**
|
||||
* Full-screen player. Shows the cover (placeholder until AlbumRef join
|
||||
* lands), title + artist + album, a scrubber, and a transport row
|
||||
@@ -99,7 +112,8 @@ fun NowPlayingScreen(
|
||||
return
|
||||
}
|
||||
Scaffold(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
modifier = Modifier.fillMaxSize().nowPlayingDragDismiss(navController),
|
||||
topBar = { NowPlayingTopBar(onClose = { navController.popBackStack() }) },
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||
containerColor = MaterialTheme.colorScheme.background,
|
||||
) { inner ->
|
||||
@@ -113,6 +127,50 @@ fun NowPlayingScreen(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.).
|
||||
*/
|
||||
@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()
|
||||
}
|
||||
totalDragY = 0f
|
||||
},
|
||||
onDragCancel = { totalDragY = 0f },
|
||||
onVerticalDrag = { _, delta -> totalDragY += delta },
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun NowPlayingTopBar(onClose: () -> Unit) {
|
||||
TopAppBar(
|
||||
title = {},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onClose) {
|
||||
Icon(
|
||||
imageVector = Lucide.ChevronDown,
|
||||
contentDescription = "Close player",
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = Color.Transparent,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NowPlayingBody(
|
||||
inner: androidx.compose.foundation.layout.PaddingValues,
|
||||
|
||||
Reference in New Issue
Block a user