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 paragraph spacing via baseline shift.
This commit is contained in:
1 parent
71fea4472b
commit
c9b4a958bf
1 file changed
+83
-3
@@ -12,13 +12,20 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTagsAsResourceId
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.style.BaselineShift
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.isUnspecified
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.example.research.R
|
||||
@@ -48,6 +55,72 @@ fun ArticleRoute(
|
||||
)
|
||||
}
|
||||
|
||||
private const val MERGED_BLOCK_MAX_CHARS = 1500
|
||||
private const val MERGED_BLOCK_MAX_ITEMS = 12
|
||||
|
||||
private fun mergeDslBlocks(
|
||||
blocks: List<DslBlock>,
|
||||
density: Density,
|
||||
baseStyle: TextStyle,
|
||||
): List<DslBlock> {
|
||||
if (blocks.size <= MERGED_BLOCK_MAX_ITEMS) return blocks
|
||||
val gapShift = with(density) {
|
||||
val fontPx =
|
||||
if (baseStyle.fontSize.isUnspecified) 0f else baseStyle.fontSize.toPx()
|
||||
if (fontPx > 0f) 8.dp.toPx() / (0.534f * fontPx) else 0.94f
|
||||
}
|
||||
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 && !blocks[j + 1].continuesPrevious) {
|
||||
val shiftPos = builder.length
|
||||
builder.append('')
|
||||
builder.addStyle(
|
||||
SpanStyle(baselineShift = BaselineShift(-gapShift)),
|
||||
shiftPos,
|
||||
shiftPos + 1
|
||||
)
|
||||
}
|
||||
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
|
||||
fun ArticleContent(
|
||||
articleState: ArticleState?,
|
||||
@@ -123,6 +196,14 @@ fun ArticleContent(
|
||||
val isHeavyArticle =
|
||||
articleState.dslContent.size > HEAVY_ARTICLE_BLOCK_COUNT
|
||||
|
||||
val density = LocalDensity.current
|
||||
val displayBlocks = remember(
|
||||
articleState.dslContent,
|
||||
density,
|
||||
baseTextStyle
|
||||
) {
|
||||
mergeDslBlocks(articleState.dslContent, density, baseTextStyle)
|
||||
}
|
||||
val articleList: @Composable (Modifier) -> Unit = { listModifier ->
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
@@ -134,15 +215,14 @@ fun ArticleContent(
|
||||
}
|
||||
) {
|
||||
itemsIndexed(
|
||||
items = articleState.dslContent,
|
||||
items = displayBlocks,
|
||||
key = { index, _ ->
|
||||
// Use stable key combining article save key and index
|
||||
"$articleSaveKey:$index"
|
||||
},
|
||||
contentType = { _, _ -> "article_block" }
|
||||
) { index, block ->
|
||||
val followedByContinuation =
|
||||
articleState.dslContent.getOrNull(index + 1)?.continuesPrevious == true
|
||||
displayBlocks.getOrNull(index + 1)?.continuesPrevious == true
|
||||
val textContent = @Composable {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
|
||||
Reference in new issue
Block a user