fix: jump to messages from the proper direction

This commit is contained in:
infi 2026-07-31 01:33:42 +02:00
parent 46987baec9
commit da79c219b7
2 changed files with 80 additions and 4 deletions

View File

@ -154,6 +154,64 @@ class ChannelHistoryScrollTest {
}
}
@Test
fun reversedListCanCenterAnOlderItemFromTheTopEdge() {
val messages = (0 until 100).map { "message-$it" }
lateinit var state: LazyListState
composeRule.setContent {
state = rememberLazyListState()
LazyColumn(
modifier = Modifier
.width(200.dp)
.height(240.dp),
state = state,
reverseLayout = true,
) {
items(messages, key = { it }) { message ->
Text(
text = message,
modifier = Modifier.height(48.dp),
)
}
}
}
composeRule.runOnIdle {
runBlocking {
state.scrollToItem(20)
}
}
composeRule.runOnIdle {
runBlocking {
state.scrollToItem(40)
}
val viewportCenter =
(state.layoutInfo.viewportStartOffset + state.layoutInfo.viewportEndOffset) / 2
val targetCenter = state.itemCenter("message-40")
runBlocking {
state.scrollBy((targetCenter - viewportCenter) * 2f)
}
}
composeRule.runOnIdle {
val viewportCenter =
(state.layoutInfo.viewportStartOffset + state.layoutInfo.viewportEndOffset) / 2
assertTrue(state.itemCenter("message-40") > viewportCenter)
runBlocking {
state.scrollBy(
(state.itemCenter("message-40") - viewportCenter).toFloat()
)
}
}
composeRule.runOnIdle {
val viewportCenter =
(state.layoutInfo.viewportStartOffset + state.layoutInfo.viewportEndOffset) / 2
assertEquals(viewportCenter, state.itemCenter("message-40"))
}
}
private fun LazyListState.currentAnchor(): Pair<Any, Int> {
val firstVisibleItem = layoutInfo.visibleItemsInfo
.first { it.index == firstVisibleItemIndex }

View File

@ -520,22 +520,40 @@ fun ChannelScreen(
val visibleItemsBeforeJump = lazyListState.layoutInfo.visibleItemsInfo
val targetIsVisible = visibleItemsBeforeJump
.any { it.key == request.messageId }
val targetWasAboveViewport =
visibleItemsBeforeJump.isNotEmpty() &&
lazyItemIndex > visibleItemsBeforeJump.maxOf { it.index }
if (!targetIsVisible) {
// Off-screen lazy items must be measured before exact centering
// so we snap the target into the viewport, then animate the centering distance
lazyListState.scrollToItem(lazyItemIndex)
}
val target = checkNotNull(
var target = checkNotNull(
snapshotFlow {
lazyListState.layoutInfo.visibleItemsInfo
.firstOrNull { it.key == request.messageId }
}.first { it != null }
)
val viewportCenter =
var viewportCenter =
(lazyListState.layoutInfo.viewportStartOffset +
lazyListState.layoutInfo.viewportEndOffset) / 2
val targetCenter = target.offset + target.size / 2
val centerOffset = (targetCenter - viewportCenter).toFloat()
var targetCenter = target.offset + target.size / 2
var centerOffset = (targetCenter - viewportCenter).toFloat()
if (request.animated && !targetIsVisible && targetWasAboveViewport) {
// scrollToItem anchors at the bottom in this reversed list. Here we mirror
// an older target to the top so its centering animation comes from the same
// side of the viewport where the message was located.
lazyListState.scrollBy(centerOffset * 2)
target = checkNotNull(
lazyListState.layoutInfo.visibleItemsInfo
.firstOrNull { it.key == request.messageId }
)
viewportCenter =
(lazyListState.layoutInfo.viewportStartOffset +
lazyListState.layoutInfo.viewportEndOffset) / 2
targetCenter = target.offset + target.size / 2
centerOffset = (targetCenter - viewportCenter).toFloat()
}
if (request.animated) {
lazyListState.animateScrollBy(
value = centerOffset,