mirror of
https://github.com/twinksoftware/Gearslip.git
synced 2026-11-10 16:02:25 +01:00
car: each app screen uses its own connection, with a plain title bar and a close button
Back and the header icon in an app opened from the launcher went to the map app. The bar above it showed the template class and draw state. Map buttons rest faintly instead of vanishing, and place cards share the navigation width.
This commit is contained in:
1 parent
8a6c358257
commit
d90c39f50a
4 files changed
+62
-40
No files matched your search
@@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
@@ -38,6 +39,7 @@ import androidx.compose.ui.layout.boundsInWindow
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import app.seb3thehacker.gearslip.GearslipLog
|
||||
import app.seb3thehacker.gearslip.host.CarAppConnection
|
||||
@@ -67,7 +69,7 @@ fun CarAppStage(
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
// The app's own drawing goes underneath; everything else is the host's rendering of
|
||||
// the template it described, laid over the top.
|
||||
androidx.compose.runtime.CompositionLocalProvider(LocalChromeCover provides cover) {
|
||||
androidx.compose.runtime.CompositionLocalProvider(LocalChromeCover provides cover, LocalCarAppConnection provides connection) {
|
||||
if (status.surfaceRequested) {
|
||||
AppSurface(connection, frame, template, Modifier.fillMaxSize())
|
||||
}
|
||||
@@ -108,7 +110,7 @@ private fun AppSurface(
|
||||
LaunchedEffect(template, size, measured, measuredStable) {
|
||||
if (width > 0 && height > 0) {
|
||||
val visible = visibleAreaFor(template, width, height, measured)
|
||||
val stable = if (template is androidx.car.app.navigation.model.NavigationTemplate) {
|
||||
val stable = if (!isContentTemplate(template)) {
|
||||
visibleAreaFor(template, width, height, measuredStable)
|
||||
} else {
|
||||
visible
|
||||
@@ -213,28 +215,31 @@ private fun ChromeBar(
|
||||
onDisconnect: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
// The app's name and a way out, nothing else: which template is showing, or whether the
|
||||
// app has drawn yet, is for the log, not the driver.
|
||||
Surface(
|
||||
color = Color.Black.copy(alpha = 0.55f),
|
||||
contentColor = Color.White,
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
color = MaterialTheme.colorScheme.surfaceContainer,
|
||||
contentColor = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = modifier.fillMaxWidth().height(CHROME_BAR),
|
||||
) {
|
||||
Row(
|
||||
Modifier.padding(horizontal = 16.dp, vertical = 6.dp),
|
||||
Modifier.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Column(Modifier.weight(1f)) {
|
||||
Text(
|
||||
status.app ?: "Car app",
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
val detail = buildList {
|
||||
status.template?.let { add(it) }
|
||||
add(if (status.surfaceAttached) "drawing" else "waiting for the app")
|
||||
}.joinToString(" - ")
|
||||
Text(detail, style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
GsButton(onClick = onDisconnect, tone = GsTone.Tonal) { Text("Stop") }
|
||||
Text(
|
||||
status.app ?: "Car app",
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
GsIconButton(
|
||||
androidx.compose.material.icons.Icons.Filled.Close,
|
||||
"Close ${status.app ?: "the app"}",
|
||||
onDisconnect,
|
||||
size = 36.dp,
|
||||
iconSize = 20.dp,
|
||||
shape = androidx.compose.foundation.shape.CircleShape,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -244,3 +249,6 @@ private val CHROME_BAR = 44.dp
|
||||
|
||||
/** Slower than this when the finger lifts and it was a drag that stopped, not a fling. */
|
||||
private const val FLING_MIN_PX_S = 400f
|
||||
|
||||
/** The app connection a template on screen belongs to: the map app's, or the one opened from the launcher. */
|
||||
internal val LocalCarAppConnection = androidx.compose.runtime.staticCompositionLocalOf { CarServices.nav }
|
||||
@@ -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, measured?.bottom ?: height)
|
||||
-> measured ?: Rect((width * CARD_COLUMN_FRACTION).toInt(), 0, width, height)
|
||||
null -> Rect(0, 0, width, height)
|
||||
else -> Rect(0, 0, 0, 0)
|
||||
}
|
||||
@@ -173,7 +173,8 @@ internal fun Modifier.covers(key: String, edge: CoverEdge): Modifier {
|
||||
|
||||
@Composable
|
||||
private fun NavigationChrome(template: NavigationTemplate, modifier: Modifier) {
|
||||
val pan by CarServices.nav.panMode.collectAsState()
|
||||
val connection = LocalCarAppConnection.current
|
||||
val pan by connection.panMode.collectAsState()
|
||||
// Pan mode clears the map for looking around: only the buttons stay.
|
||||
val routing = (template.navigationInfo as? RoutingInfo)?.takeUnless { pan }
|
||||
val estimate = template.destinationTravelEstimate?.takeUnless { pan }
|
||||
@@ -193,9 +194,10 @@ private fun NavigationBars(
|
||||
estimate: TravelEstimate?,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
val pan by CarServices.nav.panMode.collectAsState()
|
||||
val connection = LocalCarAppConnection.current
|
||||
val pan by connection.panMode.collectAsState()
|
||||
androidx.compose.foundation.layout.BoxWithConstraints(modifier.fillMaxSize()) {
|
||||
val column = (maxWidth * NAV_COLUMN_FRACTION).coerceIn(NAV_COLUMN_MIN, NAV_COLUMN_MAX)
|
||||
val column = cardColumnWidth(maxWidth)
|
||||
Row(Modifier.fillMaxSize()) {
|
||||
if (routing != null || estimate != null) {
|
||||
Column(
|
||||
@@ -373,6 +375,7 @@ private fun LaneDirection?.turnDegrees(): Float = when (this?.shape) {
|
||||
*/
|
||||
@Composable
|
||||
private fun TravelEstimateCard(estimate: TravelEstimate, modifier: Modifier = Modifier) {
|
||||
val connection = LocalCarAppConnection.current
|
||||
val time = remainingTime(estimate.remainingTimeSeconds)
|
||||
val distance = estimate.remainingDistance.display()
|
||||
val arrival = estimate.arrivalTimeAtDestination.clockTime()
|
||||
@@ -411,7 +414,7 @@ private fun TravelEstimateCard(estimate: TravelEstimate, modifier: Modifier = Mo
|
||||
GsIconButton(
|
||||
Icons.Filled.Close,
|
||||
"Stop navigating",
|
||||
{ CarServices.nav.stopNavigating() },
|
||||
{ connection.stopNavigating() },
|
||||
colors = GsColors(MaterialTheme.colorScheme.surfaceContainerHighest, MaterialTheme.colorScheme.onSurface),
|
||||
size = 52.dp,
|
||||
iconSize = 26.dp,
|
||||
@@ -438,16 +441,18 @@ private fun MapPaneLayout(
|
||||
fullHeight: Boolean = false,
|
||||
pane: @Composable () -> Unit,
|
||||
) {
|
||||
val connection = LocalCarAppConnection.current
|
||||
// Pan mode hands the whole screen to the map, leaving only its own controls.
|
||||
val pan by CarServices.nav.panMode.collectAsState()
|
||||
val pan by connection.panMode.collectAsState()
|
||||
val edgeActions = (actionStrip?.actions.orEmpty() + mapActionStrip?.actions.orEmpty()).map { it.identity() }.toSet()
|
||||
androidx.compose.foundation.layout.BoxWithConstraints(modifier.fillMaxWidth()) {
|
||||
if (!pan) {
|
||||
ChromeSurface(
|
||||
Modifier
|
||||
.align(Alignment.TopStart)
|
||||
.covers("map-pane", CoverEdge.START)
|
||||
.padding(12.dp)
|
||||
.width(maxOf(maxWidth * CONTENT_PANE_FRACTION - 12.dp, 232.dp))
|
||||
.width(cardColumnWidth(maxWidth) - 24.dp)
|
||||
.let { if (fullHeight) it.height(maxHeight - 24.dp) else it.heightIn(max = maxHeight - 24.dp) },
|
||||
shape = androidx.compose.foundation.shape.RoundedCornerShape(20.dp),
|
||||
shadowElevation = 8.dp,
|
||||
@@ -568,14 +573,16 @@ private fun ListPane(
|
||||
}
|
||||
}
|
||||
|
||||
/** Fraction of the surface the content pane covers beside a map. */
|
||||
private const val CONTENT_PANE_FRACTION = 0.42f
|
||||
|
||||
|
||||
/** Past this the bars only spread the same words further apart, across a map left with no music beside it. */
|
||||
private val NAV_COLUMN_MIN = 280.dp
|
||||
private val NAV_COLUMN_MAX = 420.dp
|
||||
private const val NAV_COLUMN_FRACTION = 0.38f
|
||||
/**
|
||||
* The width of the cards beside the map, the same for a place card and for navigation so the
|
||||
* map doesn't jump sideways when a route starts.
|
||||
*/
|
||||
internal fun cardColumnWidth(maxWidth: androidx.compose.ui.unit.Dp): androidx.compose.ui.unit.Dp =
|
||||
(maxWidth * CARD_COLUMN_FRACTION).coerceIn(280.dp, 440.dp).coerceAtMost(maxWidth)
|
||||
|
||||
private const val CARD_COLUMN_FRACTION = 0.4f
|
||||
|
||||
/** Search, sign-in and the like are laid out for a whole column, so they keep one. */
|
||||
private fun Template?.needsFullHeight() =
|
||||
|
||||
@@ -517,7 +517,8 @@ private val sectionCache = android.util.LruCache<String, List<Item>>(64)
|
||||
*/
|
||||
@Composable
|
||||
private fun SectionedContent(template: SectionedItemTemplate, modifier: Modifier, showActionsInHeader: Boolean = true, tabId: String = "") {
|
||||
val appPackage = CarServices.nav.appPackage.orEmpty()
|
||||
val connection = LocalCarAppConnection.current
|
||||
val appPackage = connection.appPackage.orEmpty()
|
||||
ContentSurface(modifier) {
|
||||
// The app's own actions (Spotify's search and Liked Songs shortcut, for two) come from
|
||||
// template.actions, not the header - but a bottom-of-content row is wrong for a driver
|
||||
|
||||
@@ -218,6 +218,9 @@ internal fun headerTitle(header: Header?, fallback: String): String =
|
||||
* actions in a row along the top, its map controls stacked under them. Keeping them in one
|
||||
* column is what stops them colliding on a screen only 480px tall.
|
||||
*/
|
||||
/** How visible the map buttons stay once they fade: enough to know they're there. */
|
||||
private const val RESTING_ALPHA = 0.35f
|
||||
|
||||
@Composable
|
||||
internal fun RightEdgeControls(
|
||||
actionStrip: ActionStrip?,
|
||||
@@ -226,22 +229,23 @@ internal fun RightEdgeControls(
|
||||
/** For the stack of map buttons alone, without the row of the app's own above it. */
|
||||
mapActionsModifier: Modifier = Modifier,
|
||||
) {
|
||||
val connection = LocalCarAppConnection.current
|
||||
val actions = actionStrip?.actions.orEmpty().filter { it.isDrawable() }
|
||||
val mapActions = mapActionStrip?.actions.orEmpty().filter { it.isDrawable() }
|
||||
if (actions.isEmpty() && mapActions.isEmpty()) return
|
||||
// A template that stops offering Pan must not leave the screen stuck in pan mode.
|
||||
val hasPan = (actions + mapActions).any { it.type == Action.TYPE_PAN }
|
||||
androidx.compose.runtime.LaunchedEffect(hasPan) { if (!hasPan) CarServices.nav.setPanMode(false) }
|
||||
androidx.compose.runtime.LaunchedEffect(hasPan) { if (!hasPan) connection.setPanMode(false) }
|
||||
|
||||
// Faded out when the screen has been left alone; then they are only a memory of buttons, so
|
||||
// the first touch on one wakes them rather than pressing it.
|
||||
val awake = LocalMapControlsWake.current.awake
|
||||
val pan by CarServices.nav.panMode.collectAsState()
|
||||
val pan by connection.panMode.collectAsState()
|
||||
// Kept up while the knob is on one of them, or the ring would fade out along with the button.
|
||||
var holdsFocus by remember { mutableStateOf(false) }
|
||||
val visible = awake || pan || holdsFocus
|
||||
val alpha by androidx.compose.animation.core.animateFloatAsState(
|
||||
targetValue = if (visible) 1f else 0f,
|
||||
targetValue = if (visible) 1f else RESTING_ALPHA,
|
||||
animationSpec = androidx.compose.animation.core.tween(if (visible) 150 else 500),
|
||||
label = "mapControls",
|
||||
)
|
||||
@@ -299,13 +303,14 @@ private fun CarRow.isBlank(): Boolean =
|
||||
*/
|
||||
@Composable
|
||||
internal fun ActionButton(action: Action, large: Boolean = false, modifier: Modifier = Modifier) {
|
||||
val connection = LocalCarAppConnection.current
|
||||
if (action.type == Action.TYPE_APP_ICON) {
|
||||
AppIconBadge()
|
||||
return
|
||||
}
|
||||
val dark = MaterialTheme.colorScheme.surface.luminanceIsDark()
|
||||
val title = action.title.text()
|
||||
val panOn by CarServices.nav.panMode.collectAsState()
|
||||
val panOn by connection.panMode.collectAsState()
|
||||
val active = action.type == Action.TYPE_PAN && panOn
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
val custom = action.backgroundColor?.type?.let { it != androidx.car.app.model.CarColor.TYPE_DEFAULT } == true
|
||||
@@ -320,8 +325,8 @@ internal fun ActionButton(action: Action, large: Boolean = false, modifier: Modi
|
||||
|
||||
GsIconBox(
|
||||
onClick = {
|
||||
if (action.type == Action.TYPE_BACK) CarServices.nav.backPressed()
|
||||
else if (action.type == Action.TYPE_PAN) CarServices.nav.setPanMode(!panOn)
|
||||
if (action.type == Action.TYPE_BACK) connection.backPressed()
|
||||
else if (action.type == Action.TYPE_PAN) connection.setPanMode(!panOn)
|
||||
else action.onClickDelegate?.click("action")
|
||||
},
|
||||
modifier = modifier,
|
||||
@@ -432,8 +437,9 @@ private const val AUTO_START_MS = 5_000L
|
||||
/** The connected app's own launcher icon, which is what the app-icon action stands for. */
|
||||
@Composable
|
||||
private fun AppIconBadge() {
|
||||
val connection = LocalCarAppConnection.current
|
||||
val context = LocalContext.current
|
||||
val pkg = CarServices.nav.appPackage
|
||||
val pkg = connection.appPackage
|
||||
val icon = remember(pkg) {
|
||||
pkg?.let {
|
||||
runCatching {
|
||||
|
||||
Reference in new issue
Block a user