From ad7e57fe66957530b185aad93f702494e641663e Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Tue, 2 Jun 2026 22:48:40 -0400 Subject: [PATCH] =?UTF-8?q?feat(android):=20scrubber=20thumb=20pill=20?= =?UTF-8?q?=E2=80=94=20fixes=20off-center=20perception?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User report: the round dot didn't read as vertically centered on the 4dp track even though geometrically it was (M3's SliderLayout centers the track slot within the thumb's height). A small circle on a thin horizontal bar is a known perceptual offset — the eye expects the bar to bisect the circle, but the circle's mass extends above and below in equal amounts the brain reads as a lift. Swap the 14dp circle for a 4dp x 18dp vertical pill (CircleShape on a non-square Box renders as a stadium). Same width as the track, clearly taller — the bar visibly passes through the pill's horizontal axis with no ambiguity. Also aligns with M3 expressive's new vertical-handle slider direction. Updates the ScrubTrack docstring that still referenced the prior 14dp-on-4dp pairing. --- .../minstrel/player/ui/NowPlayingScreen.kt | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/android/app/src/main/java/com/fabledsword/minstrel/player/ui/NowPlayingScreen.kt b/android/app/src/main/java/com/fabledsword/minstrel/player/ui/NowPlayingScreen.kt index 8002842b..b3149c03 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/player/ui/NowPlayingScreen.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/player/ui/NowPlayingScreen.kt @@ -88,6 +88,8 @@ private const val PLAY_PAUSE_ICON_DP = 56 private const val POP_GRACE_MS = 500L private val SCRUB_TRACK_HEIGHT_DP = 4.dp private val SCRUB_TRACK_CORNER_DP = 2.dp +private val SCRUB_THUMB_WIDTH_DP = 4.dp +private val SCRUB_THUMB_HEIGHT_DP = 18.dp // Vertical drag-down threshold (in pixels) past which the gesture // pops the player. Matches Flutter's 80px threshold in spirit; @@ -478,15 +480,22 @@ private fun ScrubberRow(positionMs: Long, durationMs: Long, onSeek: (Long) -> Un modifier = Modifier.fillMaxWidth(), colors = sliderColors, interactionSource = interactionSource, - // Plain filled circle to match the web client's `` thumb — flat, no state-layer - // ring, no border. M3's SliderDefaults.Thumb paints a state - // layer halo on press; we drop it for visual parity. Slider's - // 48dp hit slop still applies, so tapability is unchanged. + // Thin vertical pill (4dp wide × 18dp tall, fully rounded). + // A small circle on a thin horizontal bar reads as visually + // off-center even when geometrically aligned — the eye + // expects the bar to bisect the thumb but a 14dp circle's + // mass extends above and below in equal amounts that the + // brain perceives as offset. A vertical pill the same width + // as the track removes the ambiguity: the bar passes through + // the pill's horizontal axis cleanly. Also matches M3's + // expressive-slider handle direction. State-layer halo is + // still dropped for visual parity with the web scrubber. + // Slider's 48dp hit slop still applies, so tapability is + // unchanged. thumb = { Box( modifier = Modifier - .size(14.dp) + .size(width = SCRUB_THUMB_WIDTH_DP, height = SCRUB_THUMB_HEIGHT_DP) .clip(CircleShape) .background(accent), ) @@ -513,12 +522,11 @@ private fun ScrubberRow(positionMs: Long, durationMs: Long, onSeek: (Long) -> Un /** * Custom 4dp rounded scrubber track. M3's default Track is 16dp tall - * and reads as a heavy pill rather than a measurement line; making - * the thumb visibly taller than the track (14dp thumb on a 4dp bar) - * restores the "handle on a string" cue your eye reads as "tool, - * draggable." Also drops M3's stop indicator dot, which the web - * scrubber doesn't have. Extracted so [ScrubberRow] stays under - * detekt's LongMethod ceiling. + * and reads as a heavy pill rather than a measurement line; the + * pill-thumb-on-thin-track pairing restores the "handle on a string" + * cue your eye reads as "tool, draggable." Also drops M3's stop + * indicator dot, which the web scrubber doesn't have. Extracted so + * [ScrubberRow] stays under detekt's LongMethod ceiling. */ @Composable private fun ScrubTrack(fraction: Float, accent: Color) {