mirror of
https://github.com/twinksoftware/Gearslip.git
synced 2026-11-10 16:02:25 +01:00
nav bar: can hide while navigating, or until the screen is tapped
Set in car Settings. Off "Always show", the bar floats over the map, so hiding it never resizes the map app's surface; the app is told the bar covers the bottom while it shows. The arrival card no longer counts as covering the map, so Waze's speedometer sits level with it.
This commit is contained in:
1 parent
08ce09ad61
commit
325e115752
7 files changed
+98
-15
No files matched your search
@@ -62,6 +62,7 @@ fun CarAppStage(
|
||||
val template by connection.template.collectAsStateWithLifecycle()
|
||||
|
||||
val cover = remember { ChromeCover() }
|
||||
val navBar = LocalNavBarInset.current
|
||||
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
// The app's own drawing goes underneath; everything else is the host's rendering of
|
||||
@@ -70,7 +71,11 @@ fun CarAppStage(
|
||||
if (status.surfaceRequested) {
|
||||
AppSurface(connection, frame, template, Modifier.fillMaxSize())
|
||||
}
|
||||
TemplateChrome(template, Modifier.fillMaxSize().padding(top = if (embedded) 0.dp else CHROME_BAR))
|
||||
TemplateChrome(
|
||||
template,
|
||||
Modifier.fillMaxSize().padding(top = if (embedded) 0.dp else CHROME_BAR, bottom = navBar),
|
||||
)
|
||||
BottomCover(navBar, Modifier.align(Alignment.BottomCenter))
|
||||
}
|
||||
if (!embedded) ChromeBar(status, onDisconnect, Modifier.align(Alignment.TopCenter))
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ fun CarHome() {
|
||||
MapPane(navStatus, frame)
|
||||
}
|
||||
if (splitScreen) {
|
||||
val side = Modifier.weight(1f).fillMaxHeight()
|
||||
val side = Modifier.weight(1f).fillMaxHeight().padding(bottom = LocalNavBarInset.current)
|
||||
if (panel == SidePanel.LYRICS) SideLyrics(now, side) else DockedPlayer(now, side)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,9 @@ enum class Units { AUTO, IMPERIAL, METRIC }
|
||||
/** The car UI's own colours. Separate from [NightMode], which is about the light outside (the map). */
|
||||
enum class AppTheme { PHONE, LIGHT, DARK }
|
||||
|
||||
/** When the nav bar gets out of the way: never, during a route, or whenever the map is up. */
|
||||
enum class NavBarMode { SHOW, HIDE_NAVIGATING, HIDE }
|
||||
|
||||
/**
|
||||
* Settings changed from the car screen itself. Held as flows so the car UI reacts the moment a
|
||||
* button is tapped, and persisted through [AppSettings] so they survive the next connection.
|
||||
@@ -45,6 +48,7 @@ private const val KEY_LOCATION_WARNING = "car_location_warning"
|
||||
private const val KEY_CAR_SENSORS = "car_sensors_enabled"
|
||||
private const val KEY_SEEN_SAFETY_WARNING = "car_seen_safety_warning"
|
||||
private const val KEY_UNITS = "car_units"
|
||||
private const val KEY_NAV_BAR = "car_nav_bar"
|
||||
private const val KEY_WHATS_NEW = "car_whats_new_after_updates"
|
||||
private const val KEY_WHATS_NEW_SEEN = "car_whats_new_seen_code"
|
||||
|
||||
@@ -84,6 +88,14 @@ private const val KEY_LOCATION_WARNING = "car_location_warning"
|
||||
|
||||
val units: StateFlow<Units> = unitsFlow
|
||||
|
||||
private val navBarFlow = MutableStateFlow(NavBarMode.SHOW)
|
||||
val navBarMode: StateFlow<NavBarMode> = navBarFlow
|
||||
|
||||
fun setNavBarMode(mode: NavBarMode) {
|
||||
navBarFlow.value = mode
|
||||
AppSettings.putString(app, KEY_NAV_BAR, mode.name)
|
||||
}
|
||||
|
||||
|
||||
private val whatsNewFlow = MutableStateFlow(true)
|
||||
private val whatsNewSeenFlow = MutableStateFlow(0)
|
||||
@@ -243,6 +255,9 @@ private const val KEY_LOCATION_WARNING = "car_location_warning"
|
||||
unitsFlow.value = runCatching {
|
||||
Units.valueOf(AppSettings.getString(app, KEY_UNITS, Units.AUTO.name))
|
||||
}.getOrDefault(Units.AUTO)
|
||||
navBarFlow.value = runCatching {
|
||||
NavBarMode.valueOf(AppSettings.getString(app, KEY_NAV_BAR, NavBarMode.SHOW.name))
|
||||
}.getOrDefault(NavBarMode.SHOW)
|
||||
whatsNewFlow.value = AppSettings.getString(app, KEY_WHATS_NEW, "true") == "true"
|
||||
whatsNewSeenFlow.value = AppSettings.getString(app, KEY_WHATS_NEW_SEEN, "0").toIntOrNull() ?: 0
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ fun CarSettingsScreen() {
|
||||
val open by CarSettings.openOnConnect.collectAsState()
|
||||
val night by CarSettings.nightMode.collectAsState()
|
||||
val units by CarSettings.units.collectAsState()
|
||||
val navBar by CarSettings.navBarMode.collectAsState()
|
||||
val whatsNew by CarSettings.whatsNewAfterUpdates.collectAsState()
|
||||
val pipeAudio by CarSettings.pipeAudio.collectAsState()
|
||||
val speakDirections by CarSettings.speakDirections.collectAsState()
|
||||
@@ -115,6 +116,16 @@ fun CarSettingsScreen() {
|
||||
units,
|
||||
) { CarSettings.setUnits(it) }
|
||||
|
||||
ChoiceRow(
|
||||
"Nav bar",
|
||||
listOf(
|
||||
NavBarMode.SHOW to "Always show",
|
||||
NavBarMode.HIDE_NAVIGATING to "Hide while navigating",
|
||||
NavBarMode.HIDE to "Hide until tapped",
|
||||
),
|
||||
navBar,
|
||||
) { CarSettings.setNavBarMode(it) }
|
||||
|
||||
ChoiceRow(
|
||||
"Player in the nav bar",
|
||||
listOf(false to "Skip buttons", true to "Song title"),
|
||||
|
||||
@@ -211,6 +211,22 @@ fun CarUi() {
|
||||
}
|
||||
val screen = navigator.current
|
||||
val safetyWarningSeen by CarSettings.hasSeenSafetyWarning.collectAsState()
|
||||
val keyboardVisible by CarKeyboardVisibility.state
|
||||
// Off "Always show", the bar floats over the map 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()
|
||||
val floatingBar = navBarMode != NavBarMode.SHOW
|
||||
val barMayHide = screen == CarScreen.Home && when (navBarMode) {
|
||||
NavBarMode.SHOW -> false
|
||||
NavBarMode.HIDE_NAVIGATING -> appTemplate is androidx.car.app.navigation.model.NavigationTemplate
|
||||
NavBarMode.HIDE -> true
|
||||
}
|
||||
// A full wait from when hiding becomes possible, not from whenever the last touch was.
|
||||
LaunchedEffect(barMayHide) { if (barMayHide) controlsWake.touch() }
|
||||
val barShown = !keyboardVisible && (!barMayHide || controlsWake.navBarAwake)
|
||||
val homeInset = if (floatingBar && barShown) NAV_BAR_HEIGHT else 0.dp
|
||||
Box(Modifier.fillMaxSize().padding(pad)) {
|
||||
Column(Modifier.fillMaxSize()) {
|
||||
if (!safetyWarningSeen) {
|
||||
@@ -223,12 +239,14 @@ fun CarUi() {
|
||||
// Home never leaves the composition; other screens cover it. Dropping it
|
||||
// would tear down the map's surface, and coming back would then show black
|
||||
// while the nav app redrew on a new one.
|
||||
CarHome()
|
||||
CompositionLocalProvider(LocalNavBarInset provides homeInset) { CarHome() }
|
||||
if (screen != CarScreen.Home) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
// The floating bar never hides here, so keep the screen clear of it.
|
||||
.padding(bottom = if (floatingBar && !keyboardVisible) NAV_BAR_HEIGHT else 0.dp)
|
||||
// Keeps touches that miss this screen's controls off the map.
|
||||
.pointerInput(Unit) { detectTapGestures { } },
|
||||
) {
|
||||
@@ -247,7 +265,7 @@ fun CarUi() {
|
||||
}
|
||||
}
|
||||
}
|
||||
CarToastOverlay(Modifier.align(Alignment.BottomCenter))
|
||||
CarToastOverlay(Modifier.align(Alignment.BottomCenter).padding(bottom = homeInset))
|
||||
CallPromptOverlay()
|
||||
BluetoothPairOverlay()
|
||||
PopupTimer()
|
||||
@@ -259,20 +277,27 @@ fun CarUi() {
|
||||
// Above the popup: a call is more urgent than any notification.
|
||||
VoiceHelpOverlay(Modifier.align(Alignment.Center))
|
||||
CallOverlay(Modifier.align(Alignment.TopCenter))
|
||||
AssistantOverlay(Modifier.align(Alignment.BottomCenter))
|
||||
AssistantOverlay(Modifier.align(Alignment.BottomCenter).padding(bottom = homeInset))
|
||||
if (app.seb3thehacker.gearslip.BuildConfig.DEBUG) {
|
||||
// Bottom corner, not top: a screen's own header almost always puts
|
||||
// something in the top-right (settings, search, a Liked Songs
|
||||
// shortcut) and this badge used to sit right on top of it, hiding
|
||||
// whatever the screen drew there.
|
||||
DebugBuildBadge(Modifier.align(Alignment.BottomEnd))
|
||||
DebugBuildBadge(Modifier.align(Alignment.BottomEnd).padding(bottom = homeInset))
|
||||
}
|
||||
if (floatingBar) {
|
||||
androidx.compose.animation.AnimatedVisibility(
|
||||
barShown,
|
||||
Modifier.align(Alignment.BottomCenter),
|
||||
enter = androidx.compose.animation.slideInVertically { it },
|
||||
exit = androidx.compose.animation.slideOutVertically { it },
|
||||
) { CarNavBar(navigator) }
|
||||
}
|
||||
}
|
||||
// The keyboard takes the nav bar's row instead of squeezing in above it -
|
||||
// nothing on the nav bar is useful mid-type, and the screen is too small to
|
||||
// show both.
|
||||
val keyboardVisible by CarKeyboardVisibility.state
|
||||
if (!keyboardVisible) CarNavBar(navigator)
|
||||
if (!floatingBar && !keyboardVisible) CarNavBar(navigator)
|
||||
}
|
||||
// Over everything, nav bar included: a long press on an app, anywhere.
|
||||
AppMenu(navigator, rememberRunningApps())
|
||||
@@ -284,6 +309,14 @@ fun CarUi() {
|
||||
}
|
||||
}
|
||||
|
||||
private val NAV_BAR_HEIGHT = 56.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.
|
||||
*/
|
||||
internal val LocalNavBarInset = androidx.compose.runtime.compositionLocalOf { 0.dp }
|
||||
|
||||
/**
|
||||
* A single blocking screen, shown once per install before anything else the car screen can show -
|
||||
* Home included. The phone's own setup guide carries the same warning, but a driver who plugs in
|
||||
@@ -462,7 +495,7 @@ private fun CarNavBar(navigator: CarNavigator) {
|
||||
val running = rememberRunningApps()
|
||||
|
||||
Surface(color = MaterialTheme.colorScheme.surfaceContainer) {
|
||||
BoxWithConstraints(Modifier.fillMaxWidth().height(56.dp).padding(horizontal = 10.dp)) {
|
||||
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
|
||||
// breadcrumb trail is how you retrace your steps, but reaching for either of
|
||||
|
||||
@@ -11,27 +11,35 @@ import android.os.SystemClock
|
||||
*
|
||||
* They fade out after [IDLE_MS] with no touch anywhere on the car screen, to leave the map clear,
|
||||
* and any touch brings them back. [touch] is called for every pointer event and is cheap: it only
|
||||
* writes state when the buttons were actually hidden.
|
||||
* writes state when the buttons were actually hidden. The nav bar, when set to hide, goes the
|
||||
* same way but waits longer ([NAV_BAR_IDLE_MS]).
|
||||
*/
|
||||
class MapControlsWake {
|
||||
|
||||
var awake by mutableStateOf(true)
|
||||
private set
|
||||
|
||||
var navBarAwake by mutableStateOf(true)
|
||||
private set
|
||||
|
||||
private var lastTouch = SystemClock.uptimeMillis()
|
||||
|
||||
fun touch() {
|
||||
lastTouch = SystemClock.uptimeMillis()
|
||||
if (!awake) awake = true
|
||||
if (!navBarAwake) navBarAwake = true
|
||||
}
|
||||
|
||||
/** Called on a timer; hides the buttons once nothing has touched the screen for [IDLE_MS]. */
|
||||
fun tick() {
|
||||
if (awake && SystemClock.uptimeMillis() - lastTouch >= IDLE_MS) awake = false
|
||||
val idle = SystemClock.uptimeMillis() - lastTouch
|
||||
if (awake && idle >= IDLE_MS) awake = false
|
||||
if (navBarAwake && idle >= NAV_BAR_IDLE_MS) navBarAwake = false
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val IDLE_MS = 4_000L
|
||||
const val NAV_BAR_IDLE_MS = 8_000L
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ fun visibleAreaFor(template: Template?, width: Int, height: Int, measured: Rect?
|
||||
is NavigationTemplate -> measured ?: Rect(0, 0, width, height)
|
||||
is MapWithContentTemplate, is MapTemplate, is PlaceListNavigationTemplate,
|
||||
is RoutePreviewNavigationTemplate, is PlaceListMapTemplate,
|
||||
-> Rect((width * CONTENT_PANE_FRACTION).toInt(), 0, width, height)
|
||||
-> Rect((width * CONTENT_PANE_FRACTION).toInt(), 0, width, measured?.bottom ?: height)
|
||||
null -> Rect(0, 0, width, height)
|
||||
else -> Rect(0, 0, 0, 0)
|
||||
}
|
||||
@@ -150,9 +150,18 @@ internal class ChromeCover {
|
||||
|
||||
internal val LocalChromeCover = compositionLocalOf<ChromeCover?> { null }
|
||||
|
||||
/**
|
||||
* The floating nav bar, as far as the app's map is concerned. Its full [height] at once rather
|
||||
* than following the slide, so the app is told once instead of every frame.
|
||||
*/
|
||||
@Composable
|
||||
internal fun BottomCover(height: androidx.compose.ui.unit.Dp, modifier: Modifier = Modifier) {
|
||||
Box(modifier.fillMaxWidth().height(height).covers("nav-bar", CoverEdge.BOTTOM))
|
||||
}
|
||||
|
||||
/** Records where this lands, padding included, until it leaves the screen. */
|
||||
@Composable
|
||||
private fun Modifier.covers(key: String, edge: CoverEdge): Modifier {
|
||||
internal fun Modifier.covers(key: String, edge: CoverEdge): Modifier {
|
||||
val cover = LocalChromeCover.current ?: return this
|
||||
DisposableEffect(cover, key) { onDispose { cover.remove(key) } }
|
||||
return onGloballyPositioned { cover.put(key, edge, it.boundsInWindow()) }
|
||||
@@ -171,7 +180,9 @@ private fun NavigationChrome(template: NavigationTemplate, modifier: Modifier) {
|
||||
|
||||
/**
|
||||
* The turn in one short bar across the top, the arrival time in another along the bottom, so the
|
||||
* map keeps its full width. Neither grows past [NAV_BAR_MAX_WIDTH]. The map buttons sit between them on the right.
|
||||
* map keeps its full width. Neither grows past [NAV_BAR_MAX_WIDTH]. The bottom one is short and
|
||||
* left out of the visible area on purpose, so a speedometer in the app's bottom corner sits level
|
||||
* with it rather than stacked above it. The map buttons sit between them on the right.
|
||||
*/
|
||||
@Composable
|
||||
private fun NavigationBars(
|
||||
@@ -194,7 +205,7 @@ private fun NavigationBars(
|
||||
estimate?.let {
|
||||
TravelEstimateBar(
|
||||
it,
|
||||
Modifier.align(Alignment.BottomStart).covers("nav-bottom", CoverEdge.BOTTOM).padding(12.dp)
|
||||
Modifier.align(Alignment.BottomStart).padding(12.dp)
|
||||
.widthIn(max = NAV_BAR_MAX_WIDTH),
|
||||
)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user