fix(ui): merge article blocks to fix scroll jank
Link-heavy articles split every line into its own list item, so a fast fling composed a dozen items in one frame and froze. Merge small blocks into multi-paragraph items: indent via em space and a uniform line pitch via LineHeightStyle(Proportional, Trim.Both).
This commit is contained in:
1 parent
71fea4472b
commit
6040ca8db5
1 file changed
+79
-27
@@ -18,6 +18,9 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.semantics.semantics
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.semantics.testTagsAsResourceId
|
import androidx.compose.ui.semantics.testTagsAsResourceId
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.style.LineHeightStyle
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.IntSize
|
import androidx.compose.ui.unit.IntSize
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
@@ -48,6 +51,54 @@ fun ArticleRoute(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const val MERGED_BLOCK_MAX_CHARS = 1500
|
||||||
|
private const val MERGED_BLOCK_MAX_ITEMS = 12
|
||||||
|
|
||||||
|
private fun mergeDslBlocks(blocks: List<DslBlock>): List<DslBlock> {
|
||||||
|
if (blocks.size <= MERGED_BLOCK_MAX_ITEMS) return blocks
|
||||||
|
val merged = ArrayList<DslBlock>(blocks.size / 4)
|
||||||
|
var i = 0
|
||||||
|
while (i < blocks.size) {
|
||||||
|
val builder = AnnotatedString.Builder()
|
||||||
|
val refs = ArrayList<DslAnnotatedParser.LazyRef>()
|
||||||
|
var j = i
|
||||||
|
while (j < blocks.size) {
|
||||||
|
val block = blocks[j]
|
||||||
|
repeat(block.indent) { builder.append(' ') }
|
||||||
|
val paraStart = builder.length
|
||||||
|
builder.append(block.text)
|
||||||
|
block.lazyRefs.forEach { ref ->
|
||||||
|
refs.add(
|
||||||
|
ref.copy(
|
||||||
|
startIndex = ref.startIndex + paraStart,
|
||||||
|
endIndex = ref.endIndex + paraStart
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val more = j + 1 < blocks.size &&
|
||||||
|
(j + 1 - i) < MERGED_BLOCK_MAX_ITEMS &&
|
||||||
|
builder.length < MERGED_BLOCK_MAX_CHARS
|
||||||
|
if (more) {
|
||||||
|
builder.append('\n')
|
||||||
|
}
|
||||||
|
j++
|
||||||
|
if (!more) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
merged.add(
|
||||||
|
DslBlock(
|
||||||
|
text = builder.toAnnotatedString(),
|
||||||
|
indent = 0,
|
||||||
|
lazyRefs = refs,
|
||||||
|
continuesPrevious = blocks[i].continuesPrevious
|
||||||
|
)
|
||||||
|
)
|
||||||
|
i = j
|
||||||
|
}
|
||||||
|
return merged
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ArticleContent(
|
fun ArticleContent(
|
||||||
articleState: ArticleState?,
|
articleState: ArticleState?,
|
||||||
@@ -63,7 +114,11 @@ fun ArticleContent(
|
|||||||
is ArticleState.Success -> {
|
is ArticleState.Success -> {
|
||||||
val articleSaveKey = "${articleState.dictionaryPath}:${articleState.title}"
|
val articleSaveKey = "${articleState.dictionaryPath}:${articleState.title}"
|
||||||
val baseTextStyle = MaterialTheme.typography.bodyLarge.copy(
|
val baseTextStyle = MaterialTheme.typography.bodyLarge.copy(
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
lineHeightStyle = LineHeightStyle(
|
||||||
|
alignment = LineHeightStyle.Alignment.Proportional,
|
||||||
|
trim = LineHeightStyle.Trim.Both,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
val indentStep = 16.dp
|
val indentStep = 16.dp
|
||||||
|
|
||||||
@@ -123,6 +178,12 @@ fun ArticleContent(
|
|||||||
val isHeavyArticle =
|
val isHeavyArticle =
|
||||||
articleState.dslContent.size > HEAVY_ARTICLE_BLOCK_COUNT
|
articleState.dslContent.size > HEAVY_ARTICLE_BLOCK_COUNT
|
||||||
|
|
||||||
|
val displayBlocks = remember(
|
||||||
|
articleState.dslContent,
|
||||||
|
baseTextStyle
|
||||||
|
) {
|
||||||
|
mergeDslBlocks(articleState.dslContent)
|
||||||
|
}
|
||||||
val articleList: @Composable (Modifier) -> Unit = { listModifier ->
|
val articleList: @Composable (Modifier) -> Unit = { listModifier ->
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = listState,
|
state = listState,
|
||||||
@@ -134,39 +195,30 @@ fun ArticleContent(
|
|||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
itemsIndexed(
|
itemsIndexed(
|
||||||
items = articleState.dslContent,
|
items = displayBlocks,
|
||||||
key = { index, _ ->
|
key = { index, _ ->
|
||||||
// Use stable key combining article save key and index
|
|
||||||
"$articleSaveKey:$index"
|
"$articleSaveKey:$index"
|
||||||
},
|
},
|
||||||
contentType = { _, _ -> "article_block" }
|
contentType = { _, _ -> "article_block" }
|
||||||
) { index, block ->
|
) { index, block ->
|
||||||
val followedByContinuation =
|
Box(
|
||||||
articleState.dslContent.getOrNull(index + 1)?.continuesPrevious == true
|
modifier = Modifier
|
||||||
val textContent = @Composable {
|
.padding(start = Spacing.contentStart + indentStep * block.indent)
|
||||||
Box(
|
) {
|
||||||
|
ArticleLinkText(
|
||||||
|
block = block,
|
||||||
|
linksEnabled = true,
|
||||||
|
preferDirectHitTesting = isHeavyArticle,
|
||||||
|
style = baseTextStyle,
|
||||||
|
onRefClick = refClickHandler,
|
||||||
|
onTextLayout = {},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(
|
.testTag("article_block_$index")
|
||||||
start = Spacing.contentStart + indentStep * block.indent
|
.semantics {
|
||||||
)
|
testTagsAsResourceId = true
|
||||||
.padding(bottom = if (followedByContinuation) 0.dp else 8.dp)
|
}
|
||||||
) {
|
)
|
||||||
ArticleLinkText(
|
|
||||||
block = block,
|
|
||||||
linksEnabled = true,
|
|
||||||
preferDirectHitTesting = isHeavyArticle,
|
|
||||||
style = baseTextStyle,
|
|
||||||
onRefClick = refClickHandler,
|
|
||||||
onTextLayout = {},
|
|
||||||
modifier = Modifier
|
|
||||||
.testTag("article_block_$index")
|
|
||||||
.semantics {
|
|
||||||
testTagsAsResourceId = true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
textContent()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user