car settings: build rows as they scroll into view

Opening Settings built every row of buttons at once, which took a
noticeable moment. Only the ones on screen are built now.
This commit is contained in:
Seb3thehacker committed 2026-10-19 23:41:38 -07:00
1 parent e42cb767c9
commit 99ad0d03d7
1 file changed
+174 -123
@@ -5,6 +5,8 @@ import app.seb3thehacker.gearslip.car.theme.*
import android.os.Build import android.os.Build
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@@ -73,60 +75,76 @@ fun CarSettingsScreen() {
value = withContext(Dispatchers.Default) { CertSummary.read(context) } value = withContext(Dispatchers.Default) { CertSummary.read(context) }
} }
Column( LazyColumn(
Modifier Modifier.fillMaxSize(),
.fillMaxSize() // Built as it scrolls into view rather than all at once: every row here is a set of
.verticalScroll(rememberScrollState()) // buttons, and building the whole screen made opening Settings visibly slow.
.padding(horizontal = 24.dp, vertical = 16.dp), contentPadding = PaddingValues(horizontal = 24.dp, vertical = 16.dp),
verticalArrangement = Arrangement.spacedBy(20.dp), verticalArrangement = Arrangement.spacedBy(20.dp),
) { ) {
ScreenHeader("Settings") item {
ScreenHeader("Settings")
}
ChoiceRow( item {
"Open on connect", ChoiceRow(
listOf(CarSettings.HOME to "Home") + CarApps.all.filter { it.id != "settings" }.map { it.id to it.label }, "Open on connect",
open, listOf(CarSettings.HOME to "Home") + CarApps.all.filter { it.id != "settings" }.map { it.id to it.label },
) { CarSettings.setOpenOnConnect(it) } open,
) { CarSettings.setOpenOnConnect(it) }
}
ChoiceRow( item {
"Size", ChoiceRow(
listOf(1f to "Normal", 1.25f to "Large", 1.5f to "Extra large"), "Size",
scale, listOf(1f to "Normal", 1.25f to "Large", 1.5f to "Extra large"),
) { CarSettings.setScale(it) } scale,
) { CarSettings.setScale(it) }
}
ChoiceRow( item {
"App theme", ChoiceRow(
listOf(AppTheme.PHONE to "Follow phone", AppTheme.LIGHT to "Light", AppTheme.DARK to "Dark"), "App theme",
theme, listOf(AppTheme.PHONE to "Follow phone", AppTheme.LIGHT to "Light", AppTheme.DARK to "Dark"),
) { CarSettings.setAppTheme(it) } theme,
) { CarSettings.setAppTheme(it) }
}
ChoiceRow( item {
"Theme", ChoiceRow(
GsThemes.builtIn.map { it.id to it.name }, "Theme",
uiTheme, GsThemes.builtIn.map { it.id to it.name },
) { CarSettings.setUiTheme(it) } uiTheme,
) { CarSettings.setUiTheme(it) }
}
ChoiceRow( item {
"Map day/night", ChoiceRow(
listOf(NightMode.AUTO to "Auto", NightMode.DAY to "Day", NightMode.NIGHT to "Night"), "Map day/night",
night, listOf(NightMode.AUTO to "Auto", NightMode.DAY to "Day", NightMode.NIGHT to "Night"),
) { CarSettings.setNightMode(it) } night,
) { CarSettings.setNightMode(it) }
}
ChoiceRow( item {
"Units", ChoiceRow(
listOf(Units.AUTO to "Auto", Units.IMPERIAL to "Imperial (mi, ft, °F)", Units.METRIC to "Metric (km, m, °C)"), "Units",
units, listOf(Units.AUTO to "Auto", Units.IMPERIAL to "Imperial (mi, ft, °F)", Units.METRIC to "Metric (km, m, °C)"),
) { CarSettings.setUnits(it) } units,
) { CarSettings.setUnits(it) }
}
// For a head unit that reports the wrong size and stretches the picture (issue #12). // For a head unit that reports the wrong size and stretches the picture (issue #12).
ChoiceRow( item {
"Screen size (next connection)", ChoiceRow(
listOf<ScreenSize?>(null).plus(SCREEN_PRESETS).plus(screenSize).distinct() "Screen size (next connection)",
.map { it to (it?.toString() ?: "From the car") }, listOf<ScreenSize?>(null).plus(SCREEN_PRESETS).plus(screenSize).distinct()
screenSize, .map { it to (it?.toString() ?: "From the car") },
) { CarSettings.setScreenSize(it) } screenSize,
) { CarSettings.setScreenSize(it) }
}
if (screenSize != null) { if (screenSize != null) item {
ChoiceRow( ChoiceRow(
"Fit to the screen", "Fit to the screen",
listOf(ScreenFit.CROP to "Crop the edges", ScreenFit.SQUASH to "Squash to fit"), listOf(ScreenFit.CROP to "Crop the edges", ScreenFit.SQUASH to "Squash to fit"),
@@ -134,105 +152,138 @@ fun CarSettingsScreen() {
) { CarSettings.setScreenFit(it) } ) { CarSettings.setScreenFit(it) }
} }
ChoiceRow( item {
"Nav bar", ChoiceRow(
listOf( "Nav bar",
NavBarMode.SHOW to "Always show", listOf(
NavBarMode.HIDE_NAVIGATING to "Hide while navigating", NavBarMode.SHOW to "Always show",
NavBarMode.HIDE to "Hide until tapped", NavBarMode.HIDE_NAVIGATING to "Hide while navigating",
), NavBarMode.HIDE to "Hide until tapped",
navBar, ),
) { CarSettings.setNavBarMode(it) } navBar,
) { CarSettings.setNavBarMode(it) }
}
ChoiceRow( item {
"Player in the nav bar", ChoiceRow(
listOf(false to "Skip buttons", true to "Song title"), "Player in the nav bar",
playerTitle, listOf(false to "Skip buttons", true to "Song title"),
) { CarSettings.setPlayerTitle(it) } playerTitle,
) { CarSettings.setPlayerTitle(it) }
}
ChoiceRow( item {
"Autoplay on connect", ChoiceRow(
listOf(false to "Off", true to "On"), "Autoplay on connect",
autoplay, listOf(false to "Off", true to "On"),
) { CarSettings.setAutoplay(it) } autoplay,
) { CarSettings.setAutoplay(it) }
}
ChoiceRow( item {
"Car audio", ChoiceRow(
listOf(false to "Bluetooth", true to "USB"), "Car audio",
pipeAudio, listOf(false to "Bluetooth", true to "USB"),
) { CarSettings.setPipeAudio(it) } pipeAudio,
Text( ) { CarSettings.setPipeAudio(it) }
"USB sends the phone's sound through Gearslip. Android asks to share your screen each " + }
"drive, and a few apps block it. Bluetooth plays as the phone normally would, and " +
"Android never asks.", item {
style = MaterialTheme.typography.bodySmall, Text(
color = MaterialTheme.colorScheme.onSurfaceVariant, "USB sends the phone's sound through Gearslip. Android asks to share your screen each " +
) "drive, and a few apps block it. Bluetooth plays as the phone normally would, and " +
if (!pipeAudio) { "Android never asks.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
if (!pipeAudio) item {
ChoiceRow( ChoiceRow(
"Warn when Bluetooth isn't connected", "Warn when Bluetooth isn't connected",
listOf(false to "Off", true to "On"), listOf(false to "Off", true to "On"),
bluetoothWarning, bluetoothWarning,
) { CarSettings.setBluetoothWarning(it) } ) { CarSettings.setBluetoothWarning(it) }
} }
ChoiceRow(
"Warn when location is off",
listOf(false to "Off", true to "On"),
locationWarning,
) { CarSettings.setLocationWarning(it) }
ChoiceRow( item {
"Spoken directions", ChoiceRow(
listOf(false to "Off", true to "On"), "Warn when location is off",
speakDirections, listOf(false to "Off", true to "On"),
) { CarSettings.setSpeakDirections(it) } locationWarning,
Text( ) { CarSettings.setLocationWarning(it) }
"Gearslip reads each turn aloud through the car's speakers, and the music dips while it " + }
"speaks. Turn it off if your map app's own voice already plays in the car.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
ExperimentalFeatureRow( item {
title = "Use the phone's speech recognition", ChoiceRow(
description = "Listens through the phone's own speech service instead of Gearslip's. On " + "Spoken directions",
"most phones that's Google's, which may send your voice to Google. GrapheneOS has " + listOf(false to "Off", true to "On"),
"none, so Gearslip keeps using its own.", speakDirections,
checked = systemSpeech, ) { CarSettings.setSpeakDirections(it) }
) { CarSettings.setSystemSpeech(it) } }
WebPageSetting() item {
Text(
"Gearslip reads each turn aloud through the car's speakers, and the music dips while it " +
"speaks. Turn it off if your map app's own voice already plays in the car.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
ScreenFit(vehicle) item {
ExperimentalFeatureRow(
title = "Use the phone's speech recognition",
description = "Listens through the phone's own speech service instead of Gearslip's. On " +
"most phones that's Google's, which may send your voice to Google. GrapheneOS has " +
"none, so Gearslip keeps using its own.",
checked = systemSpeech,
) { CarSettings.setSystemSpeech(it) }
}
ChoiceRow( item {
"Experimental features", WebPageSetting()
listOf(false to "Hide", true to "Show"), }
experimentalFeatures,
) { CarSettings.setExperimentalFeaturesEnabled(it) } item {
if (experimentalFeatures) { ScreenFit(vehicle)
}
item {
ChoiceRow(
"Experimental features",
listOf(false to "Hide", true to "Show"),
experimentalFeatures,
) { CarSettings.setExperimentalFeaturesEnabled(it) }
}
if (experimentalFeatures) item {
ExperimentalFeaturesList(carSensors) ExperimentalFeaturesList(carSensors)
} }
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { item {
Text("Connection", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary) Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Info("Phone", "${Build.MANUFACTURER} ${Build.MODEL}") Text("Connection", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary)
Info("Vehicle", vehicle.ifEmpty { "-" }) Info("Phone", "${Build.MANUFACTURER} ${Build.MODEL}")
Info("Video", display.ifEmpty { "-" }) Info("Vehicle", vehicle.ifEmpty { "-" })
Info("Certificate", cert?.headline ?: "Checking…") Info("Video", display.ifEmpty { "-" })
Info("Certificate", cert?.headline ?: "Checking…")
}
} }
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { item {
Text("About", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary) Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Info("Version", app.seb3thehacker.gearslip.BuildConfig.VERSION_NAME) Text("About", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary)
Info("Build", app.seb3thehacker.gearslip.BuildConfig.VERSION_CODE.toString()) Info("Version", app.seb3thehacker.gearslip.BuildConfig.VERSION_NAME)
ExperimentalFeatureRow( Info("Build", app.seb3thehacker.gearslip.BuildConfig.VERSION_CODE.toString())
"What's new after updates", ExperimentalFeatureRow(
"Show the list of changes once, the first time the car connects after an update.", "What's new after updates",
whatsNew, "Show the list of changes once, the first time the car connects after an update.",
) { CarSettings.setWhatsNewAfterUpdates(it) } whatsNew,
GsButton(onClick = CarSettings::openWhatsNew, tone = GsTone.Tonal, modifier = Modifier.height(56.dp)) { ) { CarSettings.setWhatsNewAfterUpdates(it) }
Text("What's new", style = MaterialTheme.typography.titleMedium) GsButton(onClick = CarSettings::openWhatsNew, tone = GsTone.Tonal, modifier = Modifier.height(56.dp)) {
Text("What's new", style = MaterialTheme.typography.titleMedium)
}
} }
} }
} }