mirror of
https://github.com/twinksoftware/Gearslip.git
synced 2026-11-10 16:02:25 +01:00
car: the nav bar is a full-width strip again, and the player card loses its resize grip
This commit is contained in:
1 parent
ef1b2e1be7
commit
894712c38d
5 files changed
+35
-89
No files matched your search
@@ -27,7 +27,6 @@ import app.seb3thehacker.gearslip.R
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.draggable
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
@@ -106,19 +105,11 @@ fun CarHome() {
|
||||
// The map runs edge to edge underneath; the player floats over one side of it as a card, with
|
||||
// a gap all round. The map's surface keeps its size when the card comes and goes, so the app
|
||||
// only re-centres its map instead of drawing onto a new surface.
|
||||
val savedWidth by CarSettings.playerWidth.collectAsStateWithLifecycle()
|
||||
// The width while the grip is being dragged; null otherwise.
|
||||
var dragging by remember { androidx.compose.runtime.mutableStateOf<androidx.compose.ui.unit.Dp?>(null) }
|
||||
BoxWithConstraints(Modifier.fillMaxSize()) {
|
||||
val widest = maxWidth * PLAYER_MAX_SHARE
|
||||
val settled = (if (savedWidth > 0f) savedWidth.dp else (maxWidth / PLAYER_SHARE).coerceAtMost(PLAYER_DEFAULT_MAX))
|
||||
.coerceIn(PLAYER_MIN_WIDTH, widest)
|
||||
val column = dragging ?: settled
|
||||
val column = (maxWidth / PLAYER_SHARE).coerceIn(PLAYER_MIN_WIDTH, PLAYER_MAX_WIDTH)
|
||||
androidx.compose.runtime.CompositionLocalProvider(
|
||||
LocalCardsAtEnd provides (splitScreen && playerLeft),
|
||||
// The settled width, not the one under the finger: the map app re-centres once, when
|
||||
// the drag ends, rather than on every frame of it.
|
||||
LocalSidePanelInset provides if (splitScreen) settled else 0.dp,
|
||||
LocalSidePanelInset provides if (splitScreen) column else 0.dp,
|
||||
) {
|
||||
MapPane(navStatus, frame)
|
||||
}
|
||||
@@ -139,63 +130,17 @@ fun CarHome() {
|
||||
top = CARD_GAP,
|
||||
bottom = LocalNavBarInset.current + CARD_GAP,
|
||||
)
|
||||
Box(card) {
|
||||
val fill = Modifier.fillMaxSize()
|
||||
if (shown.value == SidePanel.LYRICS) SideLyrics(now, fill) else DockedPlayer(now, fill)
|
||||
ResizeGrip(
|
||||
onDrag = { delta ->
|
||||
val grown = if (playerLeft) delta else -delta
|
||||
dragging = ((dragging ?: settled) + grown).coerceIn(PLAYER_MIN_WIDTH, widest)
|
||||
},
|
||||
onDone = {
|
||||
dragging?.let { CarSettings.setPlayerWidth(it.value) }
|
||||
dragging = null
|
||||
},
|
||||
modifier = Modifier.align(if (playerLeft) Alignment.CenterEnd else Alignment.CenterStart),
|
||||
)
|
||||
}
|
||||
if (shown.value == SidePanel.LYRICS) SideLyrics(now, card) else DockedPlayer(now, card)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Until it's dragged, the player takes one part in this many of the screen's width, up to [PLAYER_DEFAULT_MAX]. */
|
||||
/** The player takes one part in this many of the screen's width. */
|
||||
private const val PLAYER_SHARE = 3.6f
|
||||
|
||||
/** On a wide screen the default card stops growing, so the map gets the extra room. */
|
||||
private val PLAYER_DEFAULT_MAX = 340.dp
|
||||
|
||||
/** How far the grip can take the card: narrow enough for the controls, wide enough to leave a map. */
|
||||
/** Narrow enough to leave the map most of a small screen; on a wide one the map gets the extra room. */
|
||||
private val PLAYER_MIN_WIDTH = 240.dp
|
||||
private const val PLAYER_MAX_SHARE = 0.5f
|
||||
|
||||
/**
|
||||
* The handle along the card's edge facing the map: a short bar to show it's there, in a strip wide
|
||||
* enough for a finger. [onDrag] gets each move in dp, positive to the right.
|
||||
*/
|
||||
@Composable
|
||||
private fun ResizeGrip(onDrag: (androidx.compose.ui.unit.Dp) -> Unit, onDone: () -> Unit, modifier: Modifier) {
|
||||
val density = androidx.compose.ui.platform.LocalDensity.current
|
||||
val state = androidx.compose.foundation.gestures.rememberDraggableState { px -> onDrag(with(density) { px.toDp() }) }
|
||||
Box(
|
||||
// Only the middle of the edge, so it stays clear of the close button in the corner.
|
||||
modifier
|
||||
.height(140.dp)
|
||||
.width(28.dp)
|
||||
.draggable(
|
||||
state,
|
||||
androidx.compose.foundation.gestures.Orientation.Horizontal,
|
||||
onDragStopped = { onDone() },
|
||||
),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.size(width = 5.dp, height = 44.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.45f)),
|
||||
)
|
||||
}
|
||||
}
|
||||
private val PLAYER_MAX_WIDTH = 340.dp
|
||||
|
||||
/** Between the player card and the screen's edges, and the map's own cards beside it. */
|
||||
private val CARD_GAP = 12.dp
|
||||
|
||||
@@ -78,7 +78,7 @@ import app.seb3thehacker.gearslip.car.theme.GsColors
|
||||
import app.seb3thehacker.gearslip.car.theme.withTintedSurfaces
|
||||
import app.seb3thehacker.gearslip.car.theme.GsIconBox
|
||||
import app.seb3thehacker.gearslip.car.theme.GsIconButton
|
||||
import app.seb3thehacker.gearslip.car.theme.gsPanelShadow
|
||||
import app.seb3thehacker.gearslip.car.theme.gsBarShadow
|
||||
import app.seb3thehacker.gearslip.car.theme.GsButton
|
||||
import app.seb3thehacker.gearslip.car.theme.GsTone
|
||||
import app.seb3thehacker.gearslip.car.theme.gsColors
|
||||
@@ -165,13 +165,9 @@ internal fun CarNavBar(navigator: CarNavigator) {
|
||||
val frame by CarEnvironment.frame.collectAsState()
|
||||
val running = rememberRunningApps()
|
||||
|
||||
// Floats like the player card: lifted off the screen's edges, rounded, with the theme's shadow.
|
||||
val shape = RoundedCornerShape(22.dp)
|
||||
Surface(
|
||||
Modifier.padding(start = NAV_BAR_GAP, end = NAV_BAR_GAP, bottom = NAV_BAR_GAP).gsPanelShadow(shape),
|
||||
shape = shape,
|
||||
color = MaterialTheme.colorScheme.surfaceContainer,
|
||||
) {
|
||||
// A full-width strip along the bottom, casting the theme's panel shadow up onto the map so the
|
||||
// bar sits above it rather than beside it.
|
||||
Surface(Modifier.gsBarShadow(), color = MaterialTheme.colorScheme.surfaceContainer) {
|
||||
BoxWithConstraints(Modifier.fillMaxWidth().height(NAV_BAR_HEIGHT).padding(horizontal = 10.dp)) {
|
||||
Row(Modifier.fillMaxSize(), verticalAlignment = Alignment.CenterVertically) {
|
||||
// Home and Apps are two different places now, not a toggle on one button - the
|
||||
|
||||
@@ -64,7 +64,6 @@ private const val KEY_LOCATION_WARNING = "car_location_warning"
|
||||
private const val KEY_AUTOPLAY = "car_autoplay"
|
||||
private const val KEY_PLAYER_TITLE = "car_player_title"
|
||||
private const val KEY_PLAYER_LEFT = "car_player_left"
|
||||
private const val KEY_PLAYER_WIDTH = "car_player_width"
|
||||
private const val KEY_MORE_LYRICS = "car_more_lyrics"
|
||||
private const val KEY_THEME = "car_app_theme"
|
||||
private const val KEY_UI_THEME = "car_ui_theme"
|
||||
@@ -106,7 +105,6 @@ private const val KEY_LOCATION_WARNING = "car_location_warning"
|
||||
private val autoplayFlow = MutableStateFlow(false)
|
||||
private val playerTitleFlow = MutableStateFlow(false)
|
||||
private val playerLeftFlow = MutableStateFlow(false)
|
||||
private val playerWidthFlow = MutableStateFlow(0f)
|
||||
private val moreLyricsFlow = MutableStateFlow(true)
|
||||
private val themeFlow = MutableStateFlow(AppTheme.PHONE)
|
||||
val appTheme: StateFlow<AppTheme> = themeFlow
|
||||
@@ -228,9 +226,6 @@ private const val KEY_LOCATION_WARNING = "car_location_warning"
|
||||
/** The home screen's player sits left of the map instead of right of it. */
|
||||
val playerLeft: StateFlow<Boolean> = playerLeftFlow
|
||||
|
||||
/** The docked player's width in dp, as last dragged; 0 until it has been, for the default. */
|
||||
val playerWidth: StateFlow<Float> = playerWidthFlow
|
||||
|
||||
/** Lyrics from BetterLyrics and KuGou as well as LRCLIB; off sends song details to LRCLIB only. */
|
||||
val moreLyrics: StateFlow<Boolean> = moreLyricsFlow
|
||||
|
||||
@@ -326,7 +321,6 @@ private const val KEY_LOCATION_WARNING = "car_location_warning"
|
||||
autoplayFlow.value = AppSettings.getString(app, KEY_AUTOPLAY, "false") == "true"
|
||||
playerTitleFlow.value = AppSettings.getString(app, KEY_PLAYER_TITLE, "false") == "true"
|
||||
playerLeftFlow.value = AppSettings.getString(app, KEY_PLAYER_LEFT, "false") == "true"
|
||||
playerWidthFlow.value = AppSettings.getString(app, KEY_PLAYER_WIDTH, "0").toFloatOrNull() ?: 0f
|
||||
moreLyricsFlow.value = AppSettings.getString(app, KEY_MORE_LYRICS, "true") == "true"
|
||||
pipeAudioFlow.value = AppSettings.getString(app, KEY_PIPE_AUDIO, "true") == "true"
|
||||
// Until the driver sets it, on for USB audio and off for Bluetooth, where the map app's
|
||||
@@ -405,11 +399,6 @@ private const val KEY_LOCATION_WARNING = "car_location_warning"
|
||||
AppSettings.putString(app, KEY_PLAYER_LEFT, on.toString())
|
||||
}
|
||||
|
||||
fun setPlayerWidth(dp: Float) {
|
||||
playerWidthFlow.value = dp
|
||||
AppSettings.putString(app, KEY_PLAYER_WIDTH, dp.toString())
|
||||
}
|
||||
|
||||
fun setMoreLyrics(on: Boolean) {
|
||||
moreLyricsFlow.value = on
|
||||
AppSettings.putString(app, KEY_MORE_LYRICS, on.toString())
|
||||
|
||||
@@ -234,8 +234,8 @@ fun CarUi() {
|
||||
val screen = navigator.current
|
||||
val safetyWarningSeen by CarSettings.hasSeenSafetyWarning.collectAsState()
|
||||
val keyboardVisible by CarKeyboardVisibility.state
|
||||
// The bar always floats, over the map and every other screen, rather than taking a
|
||||
// row of its own: hiding it then leaves the map's surface the size it was.
|
||||
// The bar always lies over the map and every other screen rather than taking a row
|
||||
// of its own: hiding it then leaves the map's surface the size it was.
|
||||
// Resizing it would hand the map app a new surface each time, and some go black on that.
|
||||
val navBarMode by CarSettings.navBarMode.collectAsState()
|
||||
val appTemplate by CarServices.nav.template.collectAsState()
|
||||
@@ -250,7 +250,7 @@ fun CarUi() {
|
||||
val onCall = CarCalls.state.collectAsState().value is CallUiState.Active
|
||||
val barShown = !keyboardVisible && (!barMayHide || controlsWake.navBarAwake || onCall)
|
||||
// Up to the bar's top edge, so the map's cards and the player end above it.
|
||||
val homeInset = if (barShown) NAV_BAR_HEIGHT + NAV_BAR_GAP else 0.dp
|
||||
val homeInset = if (barShown) NAV_BAR_HEIGHT else 0.dp
|
||||
// Only the screen that asked: going Home or anywhere else ends it.
|
||||
val fullscreen = CarFullscreen.active && screen != CarScreen.Home
|
||||
Box(Modifier.fillMaxSize().padding(pad)) {
|
||||
@@ -281,9 +281,8 @@ fun CarUi() {
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
// The bar never hides here, so keep the screen clear of it, with
|
||||
// the same gap above it as below.
|
||||
.padding(bottom = if (!keyboardVisible && !fullscreen) NAV_BAR_HEIGHT + NAV_BAR_GAP * 2 else 0.dp)
|
||||
// The bar never hides here, so keep the screen clear of it.
|
||||
.padding(bottom = if (!keyboardVisible && !fullscreen) NAV_BAR_HEIGHT else 0.dp)
|
||||
// Keeps touches that miss this screen's controls off the map.
|
||||
.pointerInput(Unit) { detectTapGestures { } },
|
||||
) {
|
||||
@@ -359,9 +358,6 @@ fun CarUi() {
|
||||
|
||||
internal val NAV_BAR_HEIGHT = 56.dp
|
||||
|
||||
/** Between the floating nav bar and the screen's edges. */
|
||||
internal val NAV_BAR_GAP = 12.dp
|
||||
|
||||
/**
|
||||
* How much of the bottom of Home the floating nav bar covers right now: its height while it shows,
|
||||
* zero while it's hidden or when it has a row of its own.
|
||||
|
||||
@@ -183,6 +183,26 @@ fun Modifier.gsPanelShadow(shape: Shape): Modifier {
|
||||
return drawBehind { drawSoftShadow(shape, look.panelShadow.toPx(), look.panelShadowAlpha, 1f) }
|
||||
}
|
||||
|
||||
/** The same shadow cast upwards from the top edge of a bar along the bottom of the screen. */
|
||||
@Composable
|
||||
fun Modifier.gsBarShadow(): Modifier {
|
||||
val look = gsLook()
|
||||
if (look.panelShadow <= 0.dp || look.panelShadowAlpha <= 0f) return this
|
||||
return drawBehind {
|
||||
val reach = look.panelShadow.toPx() * 1.5f
|
||||
drawRect(
|
||||
Brush.verticalGradient(
|
||||
0f to Color.Transparent,
|
||||
1f to Color.Black.copy(alpha = look.panelShadowAlpha * 0.6f),
|
||||
startY = -reach,
|
||||
endY = 0f,
|
||||
),
|
||||
topLeft = androidx.compose.ui.geometry.Offset(0f, -reach),
|
||||
size = Size(size.width, reach),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun DrawScope.drawSoftShadow(shape: Shape, elevation: Float, alpha: Float, lift: Float) {
|
||||
if (alpha <= 0f) return
|
||||
val blur = elevation * 2f
|
||||
|
||||
Reference in new issue
Block a user