Skip to content

Commit e3e1762

Browse files
eriedclaude
andcommitted
fix(wear): bump font/touch-target minimums, scroll the details page
Users reported the wear text was very small. The per-sw scale factors clamp into their minimums on every real Wear OS device (sw is 160-225 dp across the form-factor range), so the clamps ARE the size every rider sees. The previous mins (9-11 sp) were too small for a wrist glance — bumped to 11-16 sp with proportional bumps on button / icon / battery-row sizes. Verified on both AVDs: - WearOS5_round / Samsung-class 480x480 @ 432 dpi (emulator-5558) - WearOS4_round 384x384 @ 320 dpi (emulator-5560) The details page (header + speed + 7 metric rows) overflows a round 480-dp face once the fonts are bigger. Wrapped the Column in a verticalScroll Column so the bottom rows are reachable by drag / rotary crown without clipping. Tried ScalingLazyColumn first (the Wear-native pattern) but it crashes on Android 15+ reading the restricted Settings.Global.reduce_motion key — a known wear-compose bug — so the simpler scrollable Column is the right call for now. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent acfe093 commit e3e1762

1 file changed

Lines changed: 62 additions & 63 deletions

File tree

  • wear/src/main/java/com/eried/eucplanet/wear/ui

wear/src/main/java/com/eried/eucplanet/wear/ui/WatchApp.kt

Lines changed: 62 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ import androidx.compose.ui.unit.TextUnit
6464
import androidx.compose.ui.unit.dp
6565
import androidx.compose.ui.unit.sp
6666
import androidx.lifecycle.compose.collectAsStateWithLifecycle
67+
import androidx.compose.foundation.rememberScrollState
68+
import androidx.compose.foundation.verticalScroll
6769
import androidx.wear.compose.material.Button
6870
import androidx.wear.compose.material.ButtonDefaults
6971
import 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

Comments
 (0)