One navigation card: turn, lanes, a clearer next turn and the trip, with the app's own stop button when it has one

This commit is contained in:
Seb3thehacker committed 2026-11-05 16:44:22 -08:00
1 parent 5d1d2337f0
commit c389c1c586
3 files changed
+270 -191

No files matched your search

@@ -17,7 +17,6 @@ import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.StrokeJoin
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.lerp
import androidx.compose.ui.platform.LocalContext
@@ -112,54 +111,73 @@ internal fun LaneRoad(lanes: List<Lane>, modifier: Modifier, fill: Color, onFill
drawLine(lerp(ground, line, 0.7f), Offset(x(i.toFloat(), 0f), 0f), Offset(x(i.toFloat(), h), h), edge, pathEffect = dashes)
}
val top = h * 0.1f
val top = h * 0.12f
val bottom = h * 0.9f
val half = minOf(bottomWidth / n * 0.34f, h * 0.4f)
// Half the room across a lane the arrows may use, as at the bottom: well inside the lines
// even where the lane narrows at the top.
val room = bottomWidth / n * 0.31f
lanes.forEachIndexed { i, lane ->
// A point [u] across from the lane's middle, measured as at the bottom, at height [y].
val onRoad = { u: Float, y: Float -> Offset(x(i + 0.5f, y) + u * width(y) / bottomWidth, y) }
val stroke = half * 0.24f * width((top + bottom) / 2) / bottomWidth
val directions = lane.directions.orEmpty().ifEmpty { listOf(null) }
// Faded first, so the one to take draws on top where they share a stem.
val arrows = directions.sortedBy { it?.isRecommended == true }
.map { it to laneArrow(it?.shape ?: LaneDirection.SHAPE_STRAIGHT, top, bottom) }
// The lane's arrows as one shape: centred in the lane, and shrunk toward the stem's
// foot when they'd reach past [room].
val xs = arrows.flatMap { (_, a) -> a.line.map { it.x } + a.head.map { it.x } }
val shift = -(xs.min() + xs.max()) / 2
val scale = minOf(1f, room / ((xs.max() - xs.min()) / 2))
val lift = (bottom - top) * (1 - scale) / 2
// Flat to the road: centred, scaled, then narrowed with the lane toward the top.
val onRoad = { p: Offset ->
val y = bottom - (bottom - p.y) * scale - lift
Offset(x(i + 0.5f, y) + (p.x + shift) * scale * width(y) / bottomWidth, y)
}
val stroke = (bottom - top) * 0.085f * scale * width((top + bottom) / 2) / bottomWidth
val behind = if (lane.recommended()) fill else ground
val on = if (lane.recommended()) onFill else line
// Solid colours, faded by mixing rather than transparency, so where a stem and a head
// overlap nothing shows through twice. Faded first, so the one to take draws on top.
directions.sortedBy { it?.isRecommended == true }.forEach { d ->
arrows.forEach { (d, arrow) ->
// Solid colours, faded by mixing rather than transparency, so where a stem runs
// into its head nothing shows through twice.
val color = if (d?.isRecommended == true) on else lerp(behind, on, 0.38f)
laneArrow(d?.shape ?: LaneDirection.SHAPE_STRAIGHT, onRoad, top, bottom, half, stroke, color)
val points = arrow.line.map(onRoad)
drawPath(
Path().apply {
moveTo(points[0].x, points[0].y)
points.drop(1).forEach { lineTo(it.x, it.y) }
},
color,
style = Stroke(stroke, cap = StrokeCap.Round, join = StrokeJoin.Round),
)
val corners = arrow.head.map(onRoad)
drawPath(
Path().apply {
moveTo(corners[0].x, corners[0].y)
lineTo(corners[1].x, corners[1].y)
lineTo(corners[2].x, corners[2].y)
close()
},
color,
)
}
}
}
}
/** An arrow laid out flat: the stem as points, and the head's tip and two back corners. */
private class LaneArrow(val line: List<Offset>, val head: List<Offset>)
/**
* One direction's arrow inside a lane: a stem up from [bottom] that carries on straight, bends
* off to a side, or loops back for a U-turn, with a head at the end. Laid out flat, [half] being
* half the room across the lane, then put on the road by [onRoad]. A lane's arrows share a stem.
* One direction's arrow, flat, its stem rising from x 0 at [bottom]: on straight to [top], bending
* off to a side from a little above the middle, or looping back for a U-turn. A lane's arrows
* share the stem, so a straight-and-right lane reads as one fork.
*/
private fun DrawScope.laneArrow(
shape: Int,
onRoad: (Float, Float) -> Offset,
top: Float,
bottom: Float,
half: Float,
stroke: Float,
color: Color,
) {
val head = half * 0.24f * 1.9f
val path = Path()
fun Path.to(p: Offset) = onRoad(p.x, p.y).let { lineTo(it.x, it.y) }
fun Path.curve(c: Offset, p: Offset) {
val a = onRoad(c.x, c.y)
val b = onRoad(p.x, p.y)
quadraticTo(a.x, a.y, b.x, b.y)
}
onRoad(0f, bottom).let { path.moveTo(it.x, it.y) }
val tip: Offset
val dir: Offset
private fun laneArrow(shape: Int, top: Float, bottom: Float): LaneArrow {
val tall = bottom - top
val head = tall * 0.17f
val start = Offset(0f, bottom)
val degrees = when (shape) {
LaneDirection.SHAPE_SLIGHT_LEFT -> -40f
LaneDirection.SHAPE_SLIGHT_RIGHT -> 40f
LaneDirection.SHAPE_SLIGHT_LEFT -> -45f
LaneDirection.SHAPE_SLIGHT_RIGHT -> 45f
LaneDirection.SHAPE_NORMAL_LEFT -> -90f
LaneDirection.SHAPE_NORMAL_RIGHT -> 90f
LaneDirection.SHAPE_SHARP_LEFT -> -135f
@@ -169,50 +187,47 @@ private fun DrawScope.laneArrow(
else -> 0f
}
val side = if (degrees < 0) -1f else 1f
val line = mutableListOf(start)
val end: Offset
val dir: Offset
when {
degrees == 0f -> {
path.to(Offset(0f, top + head))
tip = Offset(0f, top)
end = Offset(0f, top + head)
line += end
dir = Offset(0f, -1f)
}
abs(degrees) == 180f -> {
// Up, over the top and back down beside the stem.
val r = half * 0.45f
val back = side * 2 * r
path.to(Offset(0f, top + r))
path.curve(Offset(0f, top), Offset(side * r, top))
path.curve(Offset(back, top), Offset(back, top + r))
val end = top + (bottom - top) * 0.55f
path.to(Offset(back, end))
tip = Offset(back, end + head)
val r = tall * 0.2f
line += Offset(0f, top + r)
line += curve(Offset(0f, top + r), Offset(0f, top), Offset(side * r, top))
line += curve(Offset(side * r, top), Offset(side * 2 * r, top), Offset(side * 2 * r, top + r))
end = Offset(side * 2 * r, top + tall * 0.5f)
line += end
dir = Offset(0f, 1f)
}
else -> {
val rad = degrees / 180f * PI.toFloat()
val d = Offset(sin(rad), -cos(rad))
val fork = top + (bottom - top) * 0.5f
val length = minOf((bottom - top) * 0.42f, (half - head) / abs(d.x))
val bend = Offset(0f, fork - length * 0.45f)
val end = bend + d * length
path.to(Offset(0f, fork))
path.curve(bend, end)
tip = end + d * head
val length = tall * 0.32f
val fork = Offset(0f, top + tall * 0.6f)
val bend = Offset(0f, fork.y - length * 0.45f)
end = bend + d * length
line += fork
line += curve(fork, bend, end)
dir = d
}
}
drawPath(path, color, style = Stroke(stroke, cap = StrokeCap.Round, join = StrokeJoin.Round))
val across = Offset(-dir.y, dir.x) * (head * 0.85f)
val base = tip - dir * (head * 1.6f)
val corners = listOf(tip, base + across, base - across).map { onRoad(it.x, it.y) }
drawPath(
Path().apply {
moveTo(corners[0].x, corners[0].y)
lineTo(corners[1].x, corners[1].y)
lineTo(corners[2].x, corners[2].y)
close()
},
color,
)
val tip = end + dir * head
val base = tip - dir * (head * 1.5f)
val across = Offset(-dir.y, dir.x) * (head * 0.9f)
return LaneArrow(line, listOf(tip, base + across, base - across))
}
/** Points along a quadratic curve from [a] to [c] bending toward [b], [a] itself left out. */
private fun curve(a: Offset, b: Offset, c: Offset): List<Offset> = (1..10).map { k ->
val t = k / 10f
a * ((1 - t) * (1 - t)) + b * (2 * (1 - t) * t) + c * (t * t)
}
/**
@@ -38,6 +38,9 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
import androidx.compose.runtime.setValue
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
@@ -200,8 +203,7 @@ private fun NavigationChrome(template: NavigationTemplate, modifier: Modifier) {
}
/**
* Cards down the left, map on the right: the turn on top, the trip along the bottom, the map
* buttons at the right edge. The whole column counts as covered, so the app centres its map in
* The drive's card down the left, map on the right, the map buttons at the right edge. The whole column counts as covered, so the app centres its map in
* what is left over and the car sits right of the cards rather than under them. Pan mode drops
* the cards and gives the map the whole width.
*/
@@ -221,16 +223,14 @@ private fun NavigationBars(
val column = cardColumnWidth(maxWidth)
val cards = @Composable {
if (routing != null || estimate != null) {
Column(
Modifier.width(column).fillMaxHeight().covers("nav-cards", cardEdge).padding(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
// The estimate always gets its full height; the turn card has what's left and
// keeps its top, the part that matters, if it ever runs out of room.
Box(Modifier.weight(1f).fillMaxWidth().clip(RoundedCornerShape(16.dp))) {
routing?.let { RoutingCard(it, Modifier.fillMaxWidth()) }
}
estimate?.let { TravelEstimateCard(it, Modifier.fillMaxWidth()) }
Column(Modifier.width(column).fillMaxHeight().covers("nav-cards", cardEdge).padding(12.dp)) {
// As tall as what it holds, up to the column's height.
NavigationCard(
routing,
estimate,
showStop = !template.actionStrip.hasStop(),
Modifier.fillMaxWidth().weight(1f, fill = false),
)
}
}
}
@@ -252,84 +252,112 @@ private fun NavigationBars(
}
/**
* The manoeuvre, big enough to read at a glance: the arrow and the distance to it, what to do
* and onto which road, the lanes to be in, and the turn after it along the bottom.
* The drive in one card, top to bottom: the manoeuvre big enough to read at a glance, the lanes
* to be in, the turn after it, and the trip. On a short screen parts drop out whole, the lanes'
* words first, then the next turn, then the lanes; the manoeuvre and the trip always stay.
*/
@Composable
private fun RoutingCard(info: RoutingInfo, modifier: Modifier = Modifier) {
if (info.isLoading) {
ChromeSurface(modifier) {
Text(stringResource(R.string.nav_finding_route), Modifier.padding(horizontal = 18.dp, vertical = 16.dp), style = ChromeType.title)
}
return
}
val step = info.currentStep ?: return
val scheme = MaterialTheme.colorScheme
private fun NavigationCard(info: RoutingInfo?, estimate: TravelEstimate?, showStop: Boolean, modifier: Modifier) {
val step = info?.currentStep?.takeUnless { info.isLoading }
ChromeSurface(modifier) {
// On a short screen the hint goes first, then the next turn, then the lanes; never the turn.
FitColumn {
Column(
Modifier.layoutId(0).fillMaxWidth().background(scheme.primaryContainer).padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
androidx.compose.runtime.CompositionLocalProvider(
androidx.compose.material3.LocalContentColor provides scheme.onPrimaryContainer,
) {
Row(verticalAlignment = Alignment.CenterVertically) {
CarGlyph(step.maneuver?.icon, Modifier.size(64.dp))
Spacer(Modifier.width(14.dp))
val distance = info.currentDistance.display()
if (distance.isNotEmpty()) {
Text(
distance,
style = MaterialTheme.typography.displaySmall.copy(fontWeight = androidx.compose.ui.text.font.FontWeight.Bold),
maxLines = 1,
)
}
}
val cue = step.cue.text()
if (cue.isNotEmpty()) {
Text(cue, style = ChromeType.title, maxLines = 2, overflow = TextOverflow.Ellipsis)
}
val road = step.road.text()
// "Turn left onto Queensberry Drive" already says the road; saying it twice is noise.
if (road.isNotEmpty() && !cue.contains(road, ignoreCase = true)) {
Text(
road,
style = ChromeType.body,
color = scheme.onPrimaryContainer.copy(alpha = 0.8f),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
// An app that sends only its own picture of the lanes gets it shown as it is.
if (step.lanes.orEmpty().isEmpty()) {
step.lanesImage?.let { JunctionImage(it, Modifier.heightIn(max = 56.dp)) }
}
// Rare, and only where an arrow can't explain the junction: worth the card growing for.
info.junctionImage?.let { JunctionImage(it) }
if (info?.isLoading == true) {
Text(
stringResource(R.string.nav_finding_route),
Modifier.layoutId(0).padding(horizontal = 18.dp, vertical = 16.dp),
style = ChromeType.title,
)
}
if (info != null && step != null) {
ManeuverHeader(info, step, Modifier.layoutId(0).fillMaxWidth())
// Drawn from the lane data whenever there is some, even beside the app's own picture.
step.lanes.orEmpty().takeIf { it.isNotEmpty() }?.let {
LaneBand(it, Modifier.layoutId(2).fillMaxWidth())
LaneHintLine(it, Modifier.layoutId(4).fillMaxWidth())
}
info.nextStep?.let { NextTurn(it, Modifier.layoutId(3).fillMaxWidth()) }
}
estimate?.let { TripRow(it, showStop, Modifier.layoutId(1).fillMaxWidth()) }
}
}
}
/** The manoeuvre on the theme's colour: the arrow and the distance to it, what to do, onto which road. */
@Composable
private fun ManeuverHeader(info: RoutingInfo, step: Step, modifier: Modifier) {
val scheme = MaterialTheme.colorScheme
Column(
modifier.background(scheme.primaryContainer).padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
androidx.compose.runtime.CompositionLocalProvider(
androidx.compose.material3.LocalContentColor provides scheme.onPrimaryContainer,
) {
Row(verticalAlignment = Alignment.CenterVertically) {
CarGlyph(step.maneuver?.icon, Modifier.size(64.dp))
Spacer(Modifier.width(14.dp))
val distance = info.currentDistance.display()
if (distance.isNotEmpty()) {
Text(
distance,
style = MaterialTheme.typography.displaySmall.copy(fontWeight = FontWeight.Bold),
maxLines = 1,
)
}
}
// Drawn from the lane data whenever there is some, even beside the app's own picture.
step.lanes.orEmpty().takeIf { it.isNotEmpty() }?.let {
LaneBand(it, Modifier.layoutId(1).fillMaxWidth())
LaneHintLine(it, Modifier.layoutId(3).fillMaxWidth())
val cue = step.cue.text()
if (cue.isNotEmpty()) {
Text(cue, style = ChromeType.title, maxLines = 2, overflow = TextOverflow.Ellipsis)
}
info.nextStep?.let { next ->
Row(
Modifier.layoutId(2).fillMaxWidth().padding(horizontal = 16.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(stringResource(R.string.nav_then), style = ChromeType.label, color = scheme.onSurfaceVariant)
Spacer(Modifier.width(10.dp))
CarGlyph(next.maneuver?.icon, Modifier.size(28.dp))
val nextText = next.cue.text().ifEmpty { next.road.text() }
if (nextText.isNotEmpty()) {
Spacer(Modifier.width(10.dp))
Text(nextText, style = ChromeType.body, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
}
val road = step.road.text()
// "Turn left onto Queensberry Drive" already says the road; saying it twice is noise.
if (road.isNotEmpty() && !cue.contains(road, ignoreCase = true)) {
Text(
road,
style = ChromeType.body,
color = scheme.onPrimaryContainer.copy(alpha = 0.8f),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
// An app that sends only its own picture of the lanes gets it shown as it is.
if (step.lanes.orEmpty().isEmpty()) {
step.lanesImage?.let { JunctionImage(it, Modifier.heightIn(max = 56.dp)) }
}
// Rare, and only where an arrow can't explain the junction: worth the card growing for.
info.junctionImage?.let { JunctionImage(it) }
}
}
}
/**
* The turn after this one, as large as the instruction above it and wrapping rather than cut
* short: leaving a motorway, the next move comes quickly and has to read in a glance.
*/
@Composable
private fun NextTurn(next: Step, modifier: Modifier) {
val scheme = MaterialTheme.colorScheme
// A shade apart from the trip below, so the two never read as one block of text.
Row(
modifier.background(scheme.surfaceContainerHighest).padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
CarGlyph(next.maneuver?.icon, Modifier.size(40.dp))
Spacer(Modifier.width(14.dp))
Column {
Text(
stringResource(R.string.nav_then).uppercase(java.util.Locale.getDefault()),
style = ChromeType.label.copy(fontWeight = FontWeight.Bold, letterSpacing = 1.2.sp),
color = scheme.primary,
)
val text = next.cue.text().ifEmpty { next.road.text() }
if (text.isNotEmpty()) {
Text(
text,
style = ChromeType.body.copy(fontWeight = FontWeight.SemiBold),
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
}
}
}
@@ -381,63 +409,95 @@ private fun JunctionImage(icon: CarIcon, modifier: Modifier = Modifier) {
}
/**
* The trip at a glance, stacked: time left on top, then the clock time it ends at and the
* distance, with Gearslip's own stop button beside them. The close button is Gearslip's, not the
* app's: an actionStrip's actions carry no flag saying which one (if any) means "stop
* navigating" - the first button turned out to be a mute toggle, not an exit - so this ends
* navigation the way going Home already does rather than guess at the app's own controls.
* The trip along the bottom of the card: time left, coloured as the app colours it (by traffic,
* in Vela), then the arrival and the distance, each with what it is under it. Gearslip's stop
* button sits at the end unless the app shows its own ([hasStop]). It ends navigation the way
* going Home does: the connection stays up and the app decides its own next screen.
*/
@Composable
private fun TravelEstimateCard(estimate: TravelEstimate, modifier: Modifier = Modifier) {
private fun TripRow(estimate: TravelEstimate, showStop: Boolean, modifier: Modifier) {
val connection = LocalCarAppConnection.current
val scheme = MaterialTheme.colorScheme
val time = remainingTime(estimate.remainingTimeSeconds)
val distance = estimate.remainingDistance.display()
val arrival = estimate.arrivalTimeAtDestination.clockTime()
val trip = estimate.tripText.text()
if (time.isEmpty() && distance.isEmpty() && arrival.isEmpty() && trip.isEmpty()) return
ChromeSurface(modifier) {
val dark = MaterialTheme.colorScheme.surface.luminanceIsDark()
Row(
Modifier.padding(start = 18.dp, end = 12.dp, top = 12.dp, bottom = 12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Column(Modifier.weight(1f)) {
if (time.isNotEmpty()) {
Text(
time,
style = ChromeType.headline,
color = estimate.remainingTimeColor.color(dark, MaterialTheme.colorScheme.onSurface),
maxLines = 1,
)
}
val detail = listOfNotNull(
arrival.takeIf { it.isNotEmpty() }?.let { stringResource(R.string.nav_arrive, it) },
distance.takeIf { it.isNotEmpty() },
).joinToString(" • ")
if (detail.isNotEmpty()) {
Text(detail, style = ChromeType.body, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
if (trip.isNotEmpty()) {
Text(trip, style = ChromeType.body, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
val dark = scheme.surface.luminanceIsDark()
Column(modifier.padding(start = 18.dp, end = 12.dp, top = 12.dp, bottom = 12.dp)) {
Row(Modifier.height(androidx.compose.foundation.layout.IntrinsicSize.Min), verticalAlignment = Alignment.CenterVertically) {
// The time leads, large and coloured; arrival and distance are smaller, each in its
// own column behind a thin rule, with what it is under it.
TripFigure(time, null, estimate.remainingTimeColor.color(dark, scheme.onSurface), ChromeType.headline, Modifier.weight(1.1f))
TripRule()
TripFigure(arrival, stringResource(R.string.nav_arrival), scheme.onSurface, ChromeType.body, Modifier.weight(1f))
TripRule()
TripFigure(distance, stringResource(R.string.nav_to_go), scheme.onSurface, ChromeType.body, Modifier.weight(1f))
if (showStop) {
GsIconButton(
Icons.Filled.Close,
stringResource(R.string.nav_stop),
{ connection.stopNavigating() },
colors = GsColors(scheme.surfaceContainerHighest, scheme.onSurface),
size = 52.dp,
iconSize = 26.dp,
shape = CircleShape,
)
}
// Not a disconnect: the connection stays up and the app decides its own next screen.
// Ending navigation is its own call, separate from a back press - a back press only
// pops the on-screen card, leaving the app still believing it is navigating.
GsIconButton(
Icons.Filled.Close,
stringResource(R.string.nav_stop),
{ connection.stopNavigating() },
colors = GsColors(MaterialTheme.colorScheme.surfaceContainerHighest, MaterialTheme.colorScheme.onSurface),
size = 52.dp,
iconSize = 26.dp,
shape = CircleShape,
}
if (trip.isNotEmpty()) {
Text(
trip,
Modifier.padding(top = 6.dp),
style = ChromeType.body,
color = scheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
}
@Composable
private fun TripRule() {
Box(
Modifier.padding(horizontal = 12.dp, vertical = 4.dp).width(1.dp).fillMaxHeight()
.background(MaterialTheme.colorScheme.outlineVariant),
)
}
/** One of the trip's figures with its label under it, shrinking to stay on one line in a narrow column. */
@Composable
private fun TripFigure(value: String, label: String?, color: Color, style: androidx.compose.ui.text.TextStyle, modifier: Modifier) {
Column(modifier) {
if (value.isEmpty()) return@Column
var size by androidx.compose.runtime.remember(value, style) { androidx.compose.runtime.mutableStateOf(style.fontSize) }
Text(
value,
style = style.copy(fontSize = size, fontWeight = FontWeight.Bold),
color = color,
maxLines = 1,
softWrap = false,
onTextLayout = { if (it.hasVisualOverflow && size.value > 12f) size *= 0.9f },
)
if (label != null) {
Text(label, style = ChromeType.label, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
}
}
/**
* Whether the app's own buttons include one that ends the drive: a red primary button, as
* Vela's is, or one named for it. Actions carry no flag that says so, so this is the best sign.
*/
private fun androidx.car.app.model.ActionStrip?.hasStop(): Boolean = this?.actions.orEmpty().any { action ->
val red = action.flags and androidx.car.app.model.Action.FLAG_PRIMARY != 0 &&
action.backgroundColor?.type == androidx.car.app.model.CarColor.TYPE_RED
red || action.title?.toString()?.let { STOP_WORDS.containsMatchIn(it) } == true
}
private val STOP_WORDS = Regex("\\b(stop|end|exit|quit|cancel)\\b", RegexOption.IGNORE_CASE)
// --- map with a side pane ----------------------------------------------------------------------
/**
+4
View File
@@ -751,6 +751,10 @@
<string name="nav_lane_from_right">Use lane %1$d from the right</string>
<string name="nav_lane_any">Any lane</string>
<string name="nav_arrive">Arrive %1$s</string>
<!-- Under the arrival time on the trip line: "4:30 p.m. / arrival" -->
<string name="nav_arrival">arrival</string>
<!-- Under the distance left on the trip line: "2.7 km / to go" -->
<string name="nav_to_go">to go</string>
<!-- Car app screens -->
<string name="template_nothing_playing">Nothing playing yet.</string>