ui: show three lines of the collapsed artist bio

This commit is contained in:
austinried
2026-10-19 13:47:43 +00:00
parent cac1f6af24
commit 028685d62b
2 changed files with 63 additions and 25 deletions
@@ -584,7 +584,7 @@ private fun ArtistBio(
remember(text, fullWidth, reserve, reserveLines, collapsed, expanded, moreText, lessText) { remember(text, fullWidth, reserve, reserveLines, collapsed, expanded, moreText, lessText) {
when { when {
collapsed -> { collapsed -> {
splitBioCollapsed(measurer, text, style, fullWidth, reserve, moreText) splitBioCollapsed(measurer, text, style, fullWidth, reserve, moreText, maxLines = 3)
} }
expanded -> { expanded -> {
@@ -772,7 +772,7 @@ internal data class BioLines(
val less: Boolean = false, val less: Boolean = false,
) )
/** Collapses text to two lines, appending a bold More only when it does not fit. */ /** Collapses text to [maxLines] lines, appending a bold More only when it does not fit. */
internal fun splitBioCollapsed( internal fun splitBioCollapsed(
measurer: TextMeasurer, measurer: TextMeasurer,
text: String, text: String,
@@ -780,6 +780,7 @@ internal fun splitBioCollapsed(
fullWidth: Int, fullWidth: Int,
reserve: Int, reserve: Int,
moreText: String, moreText: String,
maxLines: Int = 2,
): BioLines { ): BioLines {
val words = val words =
text text
@@ -789,26 +790,33 @@ internal fun splitBioCollapsed(
.filter { it.isNotEmpty() } .filter { it.isNotEmpty() }
if (words.isEmpty()) return BioLines(listOf(AnnotatedString("")), more = false) if (words.isEmpty()) return BioLines(listOf(AnnotatedString("")), more = false)
val available = (fullWidth - reserve).coerceAtLeast(1) val available = (fullWidth - reserve).coerceAtLeast(1)
var first = "" val lines = mutableListOf<AnnotatedString>()
var index = 0 var index = 0
while (index < words.size && lines.size < maxLines - 1) {
var current = ""
while (index < words.size) { while (index < words.size) {
val candidate = if (first.isEmpty()) words[index] else "$first ${words[index]}" val candidate = if (current.isEmpty()) words[index] else "$current ${words[index]}"
if (measurer.measure(AnnotatedString(candidate), style, maxLines = 1).size.width <= available) { if (measurer.measure(AnnotatedString(candidate), style, maxLines = 1).size.width <= available) {
first = candidate current = candidate
index++ index++
} else if (first.isEmpty()) { } else if (current.isEmpty()) {
first = words[index] current = words[index]
index++ index++
break
} else { } else {
break break
} }
} }
if (index >= words.size) return BioLines(listOf(AnnotatedString(first)), more = false) lines += AnnotatedString(current)
}
if (index >= words.size) {
return BioLines(lines, more = false)
}
val secondStart = index val remaining = words.drop(index).joinToString(" ")
val remaining = words.drop(secondStart).joinToString(" ")
if (measurer.measure(AnnotatedString(remaining), style, maxLines = 1).size.width <= available) { if (measurer.measure(AnnotatedString(remaining), style, maxLines = 1).size.width <= available) {
return BioLines(listOf(AnnotatedString(first), AnnotatedString(remaining)), more = false) lines += AnnotatedString(remaining)
return BioLines(lines, more = false)
} }
val more = val more =
@@ -819,25 +827,25 @@ internal fun splitBioCollapsed(
val moreWidth = val moreWidth =
measurer.measure(more, style, maxLines = 1).size.width + measurer.measure(more, style, maxLines = 1).size.width +
measurer.measure(AnnotatedString(" "), style, maxLines = 1).size.width measurer.measure(AnnotatedString(" "), style, maxLines = 1).size.width
val secondAvailable = (available - moreWidth).coerceAtLeast(1) val lastAvailable = (available - moreWidth).coerceAtLeast(1)
var truncated = "" var truncated = ""
var truncatedIndex = secondStart while (index < words.size) {
while (truncatedIndex < words.size) { val candidate = if (truncated.isEmpty()) words[index] else "$truncated ${words[index]}"
val candidate = if (truncated.isEmpty()) words[truncatedIndex] else "$truncated ${words[truncatedIndex]}" if (measurer.measure(AnnotatedString(candidate), style, maxLines = 1).size.width <= lastAvailable) {
if (measurer.measure(AnnotatedString(candidate), style, maxLines = 1).size.width <= secondAvailable) {
truncated = candidate truncated = candidate
truncatedIndex++ index++
} else { } else {
break break
} }
} }
val secondLine = val lastLine =
buildAnnotatedString { buildAnnotatedString {
if (truncated.isNotEmpty()) append("$truncated ") if (truncated.isNotEmpty()) append("$truncated ")
append("… ") append("… ")
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { append(moreText) } withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { append(moreText) }
} }
return BioLines(listOf(AnnotatedString(first), secondLine), more = true) lines += lastLine
return BioLines(lines, more = true)
} }
@Composable @Composable
@@ -257,6 +257,36 @@ class ArtistDetailScreenTest {
) )
} }
@Test
fun theCollapsedPreviewCanShowThreeLines() {
val bio = List(6) { "This is a sufficiently long biography sentence." }.joinToString(" ")
var result: BioLines? = null
composeRule.setContent {
val measurer = rememberTextMeasurer()
result =
splitBioCollapsed(
measurer,
bio,
TextStyle(fontSize = 14.sp),
400,
0,
"More",
maxLines = 3,
)
}
composeRule.waitForIdle()
val lines = requireNotNull(result)
assertEquals(3, lines.lines.size)
assertTrue(lines.more)
assertTrue(
lines.lines
.last()
.text
.endsWith("More"),
)
}
@Test @Test
fun anOversizedFirstWordIsStillShown() { fun anOversizedFirstWordIsStillShown() {
var result: BioLines? = null var result: BioLines? = null