From 8348e048b599e0d98254449fa67267f1faf52c26 Mon Sep 17 00:00:00 2001 From: austinried <4966622+austinried@users.noreply.github.com> Date: Mon, 19 Oct 2026 14:01:30 +0000 Subject: [PATCH] ui: keep the artist gradient anchored to the hero image Use symmetric page margins for the artist bio, let lines below the action buttons use the full width, and stop the expanding bio from shifting the hero gradient. --- .../ui/library/ArtistDetailScreen.kt | 24 ++++++++++++------- .../ui/library/ArtistDetailScreenTest.kt | 4 +++- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/app/src/main/kotlin/com/subtracks/ui/library/ArtistDetailScreen.kt b/app/src/main/kotlin/com/subtracks/ui/library/ArtistDetailScreen.kt index 2906082f..5d704966 100644 --- a/app/src/main/kotlin/com/subtracks/ui/library/ArtistDetailScreen.kt +++ b/app/src/main/kotlin/com/subtracks/ui/library/ArtistDetailScreen.kt @@ -134,6 +134,7 @@ private val SHUFFLE_RESERVE = 86.dp private val SHUFFLE_HANG = SHUFFLE_HEIGHT / 2 private val BIO_ACTION_RESERVE = 104.dp private val BIO_ACTION_HEIGHT = 48.dp +private val BIO_ACTION_OFFSET = 8.dp private val BIO_TOP_GAP = 8.dp internal fun estimateScrollPx( @@ -363,7 +364,9 @@ fun ArtistDetailScreen( BoxWithConstraints(modifier.fillMaxSize().background(MaterialTheme.colorScheme.background)) { val screenHeightPx = with(density) { maxHeight.toPx() } val barHeightPx = with(density) { barHeight.toPx() } - val gradientScrollPx = scrollPx - imageHeightPx + // The gradient is anchored to the hero image, not the whole header, whose bio can expand. + val heroHeightPx = with(density) { ART_HEIGHT.toPx() } + val gradientScrollPx = scrollPx - heroHeightPx val barColor = heroBarColor(effectiveArtwork, gradientScrollPx, barHeightPx, screenHeightPx) HeroGradient( colors = effectiveArtwork, @@ -441,7 +444,7 @@ fun ArtistDetailScreen( } } Column( - modifier = Modifier.fillMaxWidth().padding(start = 16.dp, end = 8.dp, top = TITLE_INSET), + modifier = Modifier.fillMaxWidth().padding(start = 16.dp, end = 16.dp, top = TITLE_INSET), ) { Text( text = metaText, @@ -579,12 +582,15 @@ private fun ArtistBio( remember(style) { measurer.measure(AnnotatedString("X"), style, maxLines = 1).size.height }.coerceAtLeast(1) - val reserveLines = ceil(with(density) { BIO_ACTION_HEIGHT.toPx() } / lineHeightPx).toInt().coerceAtLeast(1) + val reserveLines = + ceil(with(density) { (BIO_ACTION_HEIGHT - BIO_ACTION_OFFSET).toPx() } / lineHeightPx) + .toInt() + .coerceAtLeast(1) val bioLines = remember(text, fullWidth, reserve, reserveLines, collapsed, expanded, moreText, lessText) { when { collapsed -> { - splitBioCollapsed(measurer, text, style, fullWidth, reserve, moreText, maxLines = 3) + splitBioCollapsed(measurer, text, style, fullWidth, reserve, reserveLines, moreText, maxLines = 3) } expanded -> { @@ -620,7 +626,7 @@ private fun ArtistBio( ) } } - Row(Modifier.align(Alignment.TopEnd).offset(y = (-8).dp)) { + Row(Modifier.align(Alignment.TopEnd).offset(y = -BIO_ACTION_OFFSET)) { ArtistActions(artist, starred, onToggleStar, onMore) } } @@ -742,6 +748,7 @@ internal fun splitBioCollapsed( style: TextStyle, fullWidth: Int, reserve: Int, + reserveLines: Int, moreText: String, maxLines: Int = 2, ): BioLines { @@ -752,13 +759,13 @@ internal fun splitBioCollapsed( .split(' ') .filter { it.isNotEmpty() } if (words.isEmpty()) return BioLines(listOf(AnnotatedString("")), more = false) - val available = (fullWidth - reserve).coerceAtLeast(1) val lines = mutableListOf() var index = 0 while (index < words.size && lines.size < maxLines - 1) { var current = "" while (index < words.size) { val candidate = if (current.isEmpty()) words[index] else "$current ${words[index]}" + val available = bioLineWidth(lines.size, fullWidth, reserve, reserveLines) if (measurer.measure(AnnotatedString(candidate), style, maxLines = 1).size.width <= available) { current = candidate index++ @@ -776,8 +783,9 @@ internal fun splitBioCollapsed( return BioLines(lines, more = false) } + val remainingAvailable = bioLineWidth(lines.size, fullWidth, reserve, reserveLines) val remaining = words.drop(index).joinToString(" ") - if (measurer.measure(AnnotatedString(remaining), style, maxLines = 1).size.width <= available) { + if (measurer.measure(AnnotatedString(remaining), style, maxLines = 1).size.width <= remainingAvailable) { lines += AnnotatedString(remaining) return BioLines(lines, more = false) } @@ -790,7 +798,7 @@ internal fun splitBioCollapsed( val moreWidth = measurer.measure(more, style, maxLines = 1).size.width + measurer.measure(AnnotatedString(" "), style, maxLines = 1).size.width - val lastAvailable = (available - moreWidth).coerceAtLeast(1) + val lastAvailable = (bioLineWidth(lines.size, fullWidth, reserve, reserveLines) - moreWidth).coerceAtLeast(1) var truncated = "" while (index < words.size) { val candidate = if (truncated.isEmpty()) words[index] else "$truncated ${words[index]}" diff --git a/app/src/test/kotlin/com/subtracks/ui/library/ArtistDetailScreenTest.kt b/app/src/test/kotlin/com/subtracks/ui/library/ArtistDetailScreenTest.kt index de951710..f20aa8a0 100644 --- a/app/src/test/kotlin/com/subtracks/ui/library/ArtistDetailScreenTest.kt +++ b/app/src/test/kotlin/com/subtracks/ui/library/ArtistDetailScreenTest.kt @@ -238,7 +238,7 @@ class ArtistDetailScreenTest { var result: BioLines? = null composeRule.setContent { val measurer = rememberTextMeasurer() - result = splitBioCollapsed(measurer, bio, TextStyle(fontSize = 14.sp), 400, 0, "More") + result = splitBioCollapsed(measurer, bio, TextStyle(fontSize = 14.sp), 400, 0, 0, "More") } composeRule.waitForIdle() @@ -269,6 +269,7 @@ class ArtistDetailScreenTest { TextStyle(fontSize = 14.sp), 400, 0, + 0, "More", maxLines = 3, ) @@ -298,6 +299,7 @@ class ArtistDetailScreenTest { TextStyle(fontSize = 14.sp), 20, 0, + 0, "More", ) }