feat(ui): swipe-down to dismiss the mini-player
- verticalDragToDismiss: bar follows the finger downward (translationY, capped 120dp) and dismisses past 40% height or on a downward fling; springs back otherwise; static under Reduce Motion - Dismissal is per-source: playback continues, and the bar returns automatically when a different station/song starts - Tabs route now owns its mini-player (root no longer stacks one) - On-device: swipe dismissed bar while PLAYING; station change re-showed it
This commit is contained in:
parent
172e932ef3
commit
918aeaf8d3
3 changed files with 51 additions and 2 deletions
|
|
@ -29,7 +29,7 @@ import com.rada.ui.streams.StreamsScreen
|
|||
fun NavGraph(navController: NavHostController) {
|
||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||
val currentRoute = navBackStackEntry?.destination?.route
|
||||
val showNowPlayingBar = currentRoute != Screen.Player.route
|
||||
val showNowPlayingBar = currentRoute != Screen.Player.route && currentRoute != Screen.Tabs.route
|
||||
val reduce = LocalReduceMotion.current
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import com.rada.ui.theme.metadataEnter
|
|||
import com.rada.ui.theme.metadataExit
|
||||
import com.rada.ui.theme.LocalReduceMotion
|
||||
import com.rada.ui.theme.barEnter
|
||||
import com.rada.ui.theme.verticalDragToDismiss
|
||||
import com.rada.ui.theme.barExit
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
|
|
@ -36,6 +37,8 @@ import androidx.compose.material3.Text
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -62,7 +65,12 @@ fun NowPlayingBar(
|
|||
val isLocal by viewModel.isLocal.collectAsState()
|
||||
val streamMeta by viewModel.streamMeta.collectAsState()
|
||||
val localMedia by viewModel.localMedia.collectAsState()
|
||||
val isVisible = stationName.isNotEmpty()
|
||||
// Swipe-down (or quick drag-down) dismisses the bar until a *new*
|
||||
// source starts playing — a floating thing should answer to gestures.
|
||||
var dismissedFor by remember { mutableStateOf<String?>(null) }
|
||||
val sourceKey = stationName
|
||||
if (dismissedFor != null && dismissedFor != sourceKey) dismissedFor = null
|
||||
val isVisible = stationName.isNotEmpty() && dismissedFor != sourceKey
|
||||
|
||||
// Spotify-style two-line bar: track (or station) + artist (or station).
|
||||
val title: String
|
||||
|
|
@ -93,6 +101,10 @@ fun NowPlayingBar(
|
|||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.verticalDragToDismiss(
|
||||
thresholdFraction = 0.4f,
|
||||
onDismiss = { dismissedFor = sourceKey },
|
||||
)
|
||||
.padding(horizontal = 12.dp)
|
||||
.clip(RoundedCornerShape(SatsAmpGlass.rCard))
|
||||
// glass: textSafe 90% overlay + 6% white + highlight border
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.rada.ui.theme
|
||||
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
||||
|
|
@ -8,6 +9,10 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.foundation.gestures.detectVerticalDragGestures
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.animation.EnterTransition
|
||||
import androidx.compose.animation.ExitTransition
|
||||
import androidx.compose.animation.core.CubicBezierEasing
|
||||
|
|
@ -142,3 +147,35 @@ fun Modifier.pressDip(
|
|||
)
|
||||
this.graphicsLayer { scaleX = scale; scaleY = scale }
|
||||
}
|
||||
|
||||
// ── Dismiss-by-drag ───────────────────────────────────────────────────────
|
||||
// A floating bar should answer to a downward drag: it follows the finger
|
||||
// (translationY only) and dismisses past a threshold or on a downward fling;
|
||||
// otherwise it springs back. Reduce Motion: no follow, threshold-only dismiss.
|
||||
|
||||
fun Modifier.verticalDragToDismiss(
|
||||
thresholdFraction: Float = 0.5f,
|
||||
onDismiss: () -> Unit,
|
||||
): Modifier = composed {
|
||||
val reduce = LocalReduceMotion.current
|
||||
var dragPx by remember { mutableFloatStateOf(0f) }
|
||||
val density = androidx.compose.ui.platform.LocalDensity.current
|
||||
val maxDrag = with(density) { 120.dp.toPx() }
|
||||
this
|
||||
.graphicsLayer { translationY = if (reduce) 0f else dragPx }
|
||||
.pointerInput(Unit) {
|
||||
detectVerticalDragGestures(
|
||||
onDragEnd = {
|
||||
if (dragPx > size.height * thresholdFraction) onDismiss()
|
||||
dragPx = 0f
|
||||
},
|
||||
onDragCancel = { dragPx = 0f },
|
||||
onVerticalDrag = { change, amount ->
|
||||
if (amount > 0) {
|
||||
change.consume()
|
||||
dragPx = (dragPx + amount).coerceIn(0f, maxDrag)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue