@@ -64,6 +64,8 @@ import androidx.compose.ui.unit.TextUnit
6464import androidx.compose.ui.unit.dp
6565import androidx.compose.ui.unit.sp
6666import androidx.lifecycle.compose.collectAsStateWithLifecycle
67+ import androidx.compose.foundation.rememberScrollState
68+ import androidx.compose.foundation.verticalScroll
6769import androidx.wear.compose.material.Button
6870import androidx.wear.compose.material.ButtonDefaults
6971import androidx.wear.compose.material.Icon
@@ -245,12 +247,14 @@ private fun MainScreen(state: WatchState, accent: Color) {
245247 // page (DetailsScreen) is where the explicit "Disconnected" hint
246248 // lives. Mirrors the phone dashboard's behaviour.
247249 val sw = maxWidth.value
248- val batteryFontSp = (sw * 0.034f ).coerceIn(9f , 12f ).sp
249- val batteryIconDp = (sw * 0.038f ).coerceIn(10f , 14f ).dp
250+ // Min clamps are the size users actually see on every Wear OS device:
251+ // sw is ~160-225 dp across the device range, so the scale-factor side
252+ // of these formulas never wins. Mins have to be readable on their own.
253+ val batteryFontSp = (sw * 0.034f ).coerceIn(11f , 14f ).sp
254+ val batteryIconDp = (sw * 0.038f ).coerceIn(12f , 15f ).dp
250255 val batterySpacingDp = (sw * 0.018f ).coerceIn(5f , 9f ).dp
251- // ~10% bigger than the previous 0.115f / 0.055f values.
252- val buttonDp = (sw * 0.127f ).coerceIn(37f , 55f ).dp
253- val buttonIconDp = (sw * 0.060f ).coerceIn(18f , 26f ).dp
256+ val buttonDp = (sw * 0.127f ).coerceIn(40f , 56f ).dp
257+ val buttonIconDp = (sw * 0.060f ).coerceIn(20f , 26f ).dp
254258 // "Prioritize PWM" inverts the size hierarchy: when on, speed shrinks
255259 // by ~35% and PWM (bar + number) grows by ~60% so cutout-margin becomes
256260 // the dominant glance signal.
@@ -315,10 +319,10 @@ private fun MainScreen(state: WatchState, accent: Color) {
315319 // The unit is tiny and rendered with an invisible mirror on the left
316320 // so the speed glyph stays visually centred; the row is symmetric
317321 // around the speed even when the unit is shown.
318- val unitSp = (sw * 0.030f ).coerceIn(9f , 12f ).sp
322+ val unitSp = (sw * 0.030f ).coerceIn(11f , 14f ).sp
319323 // Prioritize PWM blows up the number; was 0.075f / 28sp cap, now 0.11f / 40sp cap
320324 // so on a round 454-px Wear face the PWM glyph reads as the dial's headline.
321- val pwmNumberSp = (sw * if (prioritizePwm) 0.110f else 0.038f ).coerceIn(11f , 40f ).sp
325+ val pwmNumberSp = (sw * if (prioritizePwm) 0.110f else 0.038f ).coerceIn(12f , 40f ).sp
322326 val showBar = state.pwmDisplay == " BAR" || state.pwmDisplay == " BOTH"
323327 val showPwmNumber = state.pwmDisplay == " NUMBERS" || state.pwmDisplay == " BOTH"
324328 // In "BOTH" mode the bar shares a row with the percent number, so we shrink the
@@ -826,16 +830,17 @@ private fun DetailsScreen(state: WatchState, accent: Color) {
826830 contentAlignment = Alignment .Center
827831 ) {
828832 val sw = maxWidth.value
829- val labelSp = (sw * 0.034f ).coerceIn(10f , 13f ).sp
830- val valueSp = (sw * 0.038f ).coerceIn(11f , 14f ).sp
831- // Header (wheel name / "Disconnected") is meant to be a quiet caption,
832- // not a heading; it shrinks under the speed which carries the visual
833- // weight on this page.
834- val headerSp = (sw * 0.030f ).coerceIn(9f , 12f ).sp
835- val speedSp = (sw * 0.060f ).coerceIn(18f , 26f ).sp
836- val speedUnitSp = (sw * 0.030f ).coerceIn(9f , 12f ).sp
837- val labelWidth = (sw * 0.22f ).coerceIn(60f , 90f ).dp
838- val valueWidth = (sw * 0.28f ).coerceIn(75f , 110f ).dp
833+ // See MainScreen comment: min clamps drive the rendered size on every
834+ // Wear OS form factor, so bump them to readable values.
835+ val labelSp = (sw * 0.034f ).coerceIn(12f , 15f ).sp
836+ val valueSp = (sw * 0.038f ).coerceIn(13f , 16f ).sp
837+ val headerSp = (sw * 0.030f ).coerceIn(11f , 13f ).sp
838+ val speedSp = (sw * 0.060f ).coerceIn(22f , 30f ).sp
839+ val speedUnitSp = (sw * 0.030f ).coerceIn(11f , 13f ).sp
840+ // Label / value widths grow with the new minimum font sizes so the
841+ // longer labels ("Voltage", "Current") still fit on a single line.
842+ val labelWidth = (sw * 0.22f ).coerceIn(68f , 96f ).dp
843+ val valueWidth = (sw * 0.28f ).coerceIn(84f , 114f ).dp
839844
840845 // Hide all unit labels until the phone has actually told us which unit
841846 // system the rider uses; otherwise an mph user sees km/h on launch and
@@ -856,44 +861,65 @@ private fun DetailsScreen(state: WatchState, accent: Color) {
856861 val speedDisplay = WatchUnits .speed(state.speedKmh, state.speedUnit)
857862 val powerW = state.voltage * state.current
858863
864+ val useAccent = state.accentKey != " default"
865+ val detailMaxSpeed = if (state.maxSpeedKmh > 0f ) state.maxSpeedKmh else 70f
866+ val speedColor = if (useAccent) accent
867+ else speedBandColor(state.speedKmh, detailMaxSpeed, true ,
868+ state.gaugeOrangeThresholdPct, state.gaugeRedThresholdPct, colors)
869+ val live = state.connected && phoneAlive
870+ val cyanColor = accent
871+ val tempColor = when {
872+ useAccent -> accent
873+ ! live -> colors.gaugeFill
874+ state.temperatureC > 60f -> colors.gaugeDanger
875+ state.temperatureC > 45f -> colors.gaugeWarn
876+ else -> colors.gaugeFill
877+ }
878+ val pwmColor = when {
879+ useAccent -> accent
880+ ! live -> colors.gaugeFill
881+ state.pwmPercent > 80f -> colors.gaugeDanger
882+ state.pwmPercent > 60f -> colors.gaugeWarn
883+ else -> colors.gaugeFill
884+ }
885+
886+ // Scrollable column rather than ScalingLazyColumn: the wear-compose
887+ // ScalingLazyColumn currently crashes on Android 15+ trying to read
888+ // the restricted Settings.Global.reduce_motion key when targetSdk
889+ // is above 34 (a known library bug fixed only in newer wear-compose
890+ // versions). Plain Column + verticalScroll gives us scrolling without
891+ // the system-settings dependency; the rider drags to see anything
892+ // pushed off the round display by the bigger fonts. Top + bottom
893+ // padding scale with display width so the round bezel doesn't clip
894+ // the first or last row when scrolled to the extremes.
895+ val edgePad = (sw * 0.14f ).coerceIn(20f , 40f ).dp
859896 Column (
860897 modifier = Modifier
861- .fillMaxWidth()
862- .padding(horizontal = 16 .dp),
898+ .fillMaxSize()
899+ .verticalScroll(rememberScrollState())
900+ .padding(horizontal = 16 .dp, vertical = edgePad),
863901 horizontalAlignment = Alignment .CenterHorizontally ,
864- verticalArrangement = Arrangement .spacedBy(2 .dp)
902+ verticalArrangement = Arrangement .spacedBy(3 .dp)
865903 ) {
866- // Wheel-name and Disconnected header both wear the same muted
867- // caption tint as the phone dashboard's status row; the speed
868- // is the page's visual focus, the header is just context.
869- // Stale phone counts as disconnected from the rider's POV.
870904 if (! state.connected || ! phoneAlive) {
871905 Text (
872906 text = stringResource(R .string.watch_disconnected),
873907 fontSize = headerSp,
874908 color = colors.textSecondary,
875- fontWeight = FontWeight .SemiBold
909+ fontWeight = FontWeight .SemiBold ,
910+ textAlign = TextAlign .Center
876911 )
877912 } else if (state.wheelName.isNotBlank()) {
878913 Text (
879914 text = state.wheelName,
880915 fontSize = headerSp,
881916 color = colors.textPrimary,
882- fontWeight = FontWeight .SemiBold
917+ fontWeight = FontWeight .SemiBold ,
918+ textAlign = TextAlign .Center
883919 )
884920 }
885- // Speed number colours per main-screen rules; the km/h unit
886- // stays the muted grey from the dashboard's small label.
887- val useAccent = state.accentKey != " default"
888- val detailMaxSpeed = if (state.maxSpeedKmh > 0f ) state.maxSpeedKmh else 70f
889- val speedColor = if (useAccent) accent
890- else speedBandColor(state.speedKmh, detailMaxSpeed, true ,
891- state.gaugeOrangeThresholdPct, state.gaugeRedThresholdPct, colors)
892921 Row (verticalAlignment = Alignment .Bottom ) {
893922 Text (
894- // Headline speed dashes out the same way the main-screen
895- // glyph does. Without this the page keeps showing the
896- // last reading forever after the phone app dies.
897923 text = if (phoneAlive) " %.0f" .format(speedDisplay) else DASH ,
898924 fontSize = speedSp,
899925 fontWeight = FontWeight .Bold ,
@@ -909,33 +935,6 @@ private fun DetailsScreen(state: WatchState, accent: Color) {
909935 )
910936 }
911937 Spacer (Modifier .height(4 .dp))
912- // A live row needs BOTH a connected wheel and a phone that's
913- // still pushing; otherwise we dash the whole strip.
914- val live = state.connected && phoneAlive
915- // Per-metric coloring on default accent mirrors the phone
916- // dashboard: voltage / current / power / trip / torque get the
917- // accent (which IS cyan when accent=default), temp and PWM use
918- // tiered green / amber / red thresholds, battery in BatteryRow
919- // is already tier-coloured. Custom accent collapses everything
920- // to the picked accent so the watch wears one identity colour.
921- // Disconnected dashes wear the same colour the live value
922- // would at zero (green safe-tier or accent) so the row reads
923- // continuous with the speed glyph at the top of the page.
924- val cyanColor = accent
925- val tempColor = when {
926- useAccent -> accent
927- ! live -> colors.gaugeFill
928- state.temperatureC > 60f -> colors.gaugeDanger
929- state.temperatureC > 45f -> colors.gaugeWarn
930- else -> colors.gaugeFill
931- }
932- val pwmColor = when {
933- useAccent -> accent
934- ! live -> colors.gaugeFill
935- state.pwmPercent > 80f -> colors.gaugeDanger
936- state.pwmPercent > 60f -> colors.gaugeWarn
937- else -> colors.gaugeFill
938- }
939938 DetailRow (R .string.watch_voltage_label, if (live) " %.1f V" .format(state.voltage) else DASH , labelSp, valueSp, labelWidth, valueWidth, cyanColor)
940939 DetailRow (R .string.watch_current_label, if (live) " %.1f A" .format(state.current) else DASH , labelSp, valueSp, labelWidth, valueWidth, cyanColor)
941940 DetailRow (R .string.watch_power_label, if (live) " %.0f W" .format(powerW) else DASH , labelSp, valueSp, labelWidth, valueWidth, cyanColor)
0 commit comments