keep the artist targets narrow and clear of the controls

Both targets were too greedy. They now hug the text instead of spanning the row, so a tap
beside the line does nothing, and the album detail target stops short of the controls
instead of reaching down to them.

Album detail: the line plus its 4dp lead-in and 8dp below (32dp), with the original
spacing kept, so the layout is exactly where it was. Now playing keeps the title+artist
block (58dp) but is no longer full width.

Both are shorter than the 48dp recommendation on purpose, to stay clear of the slider and
the play/shuffle buttons. Tests assert the measured geometry: no full-width target, a
12dp gap above the controls, and the now playing block staying at its own 58dp.
This commit is contained in:
austinried
2026-10-04 00:30:01 +00:00
parent 17a41fd579
commit 07fb07762c
4 changed files with 61 additions and 9 deletions
@@ -246,14 +246,13 @@ fun HeroHeader(
)
val subtitleIsTarget = onSubtitleClick != null && subtitle.isNotEmpty() && comment.isNullOrBlank()
if (subtitleIsTarget) {
// The target spans the 4dp lead-in and the 20dp gap that would sit below the
// line, so the text stays put and the row clears 48dp; without those the
// subtitle alone is a single 20dp line.
// The line plus the 4dp lead-in above and 8dp below: shorter than the 48dp
// recommendation, deliberately, so it stays 12dp clear of the controls rather
// than reaching down to them. Hugs the text instead of the row.
Box(
modifier =
Modifier
.fillMaxWidth()
.heightIn(min = 48.dp)
.heightIn(min = 32.dp)
.clickable(onClickLabel = "Open artist") { onSubtitleClick?.invoke() },
contentAlignment = Alignment.TopCenter,
) {
@@ -267,7 +266,7 @@ fun HeroHeader(
Spacer(Modifier.height(4.dp))
HeroSubtitle(comment)
}
if (!subtitleIsTarget) Spacer(Modifier.height(20.dp))
if (!subtitleIsTarget) Spacer(Modifier.height(20.dp)) else Spacer(Modifier.height(12.dp))
Row(
modifier = controlsModifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
@@ -293,7 +293,6 @@ fun NowPlayingScreen(
verticalArrangement = Arrangement.spacedBy(6.dp),
modifier =
Modifier
.fillMaxWidth()
.padding(top = 12.dp)
.height(titleHeight + 6.dp + subtitleHeight)
.clickable(
@@ -7,7 +7,9 @@ import androidx.compose.ui.test.filter
import androidx.compose.ui.test.hasClickAction
import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.onRoot
import androidx.compose.ui.test.performClick
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -68,10 +70,39 @@ class AlbumDetailTapsTest {
}
@Test
fun theArtistTargetIsAtLeastTheMinimumTouchSize() {
fun theArtistTargetCoversTheLineAndItsGaps() {
render(onArtistClick = {})
composeRule.onNodeWithText("Radiohead \u00B7 2000").assertHeightIsAtLeast(48.dp)
composeRule.onNodeWithText("Radiohead \u00B7 2000").assertHeightIsAtLeast(32.dp)
}
@Test
fun theArtistTargetHugsTheTextRatherThanTheRow() {
render(onArtistClick = {})
val target =
composeRule
.onNodeWithText("Radiohead \u00B7 2000")
.fetchSemanticsNode()
.boundsInRoot.width
val screen =
composeRule
.onRoot()
.fetchSemanticsNode()
.boundsInRoot.width
assertTrue("the target should not span the row", target < screen)
}
@Test
fun theArtistTargetKeepsClearOfTheControls() {
render(onArtistClick = {})
val target = composeRule.onNodeWithText("Radiohead \u00B7 2000").fetchSemanticsNode().boundsInRoot
val controls = composeRule.onNodeWithContentDescription("Download").fetchSemanticsNode().boundsInRoot
val gap = controls.top - target.bottom
assertTrue("expected a clear gap above the controls, was ${gap}px", gap >= with(composeRule.density) { 12.dp.roundToPx() })
}
@Test
@@ -2,11 +2,13 @@ package com.subtracks.ui.playback
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.assertHeightIsAtLeast
import androidx.compose.ui.test.assertHeightIsEqualTo
import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.assertWidthIsAtLeast
import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.onRoot
import androidx.compose.ui.test.performClick
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -92,10 +94,31 @@ class NowPlayingTapsTest {
fun theArtistTargetIsAtLeastTheMinimumTouchSize() {
render(onArtistClick = {})
// The block is the title line (32dp) plus the gap (6dp) plus the artist line
// (20dp); it must not absorb the slider's own top padding.
composeRule.onNodeWithText("Radiohead").assertHeightIsAtLeast(48.dp)
composeRule.onNodeWithText("Radiohead").assertHeightIsEqualTo(58.dp)
composeRule.onNodeWithText("Radiohead").assertWidthIsAtLeast(48.dp)
}
@Test
fun theArtistTargetHugsTheTextRatherThanTheRow() {
render(onArtistClick = {})
val target =
composeRule
.onNodeWithText("Radiohead")
.fetchSemanticsNode()
.boundsInRoot.width
val screen =
composeRule
.onRoot()
.fetchSemanticsNode()
.boundsInRoot.width
assertTrue("the target should not span the row", target < screen)
}
@Test
fun theTapTargetsStayInertWithoutADestination() {
render()