diff --git a/app/src/androidTest/java/chat/stoat/screens/chat/views/channel/ChannelHistoryScrollTest.kt b/app/src/androidTest/java/chat/stoat/screens/chat/views/channel/ChannelHistoryScrollTest.kt index 54571fc7..234ad678 100644 --- a/app/src/androidTest/java/chat/stoat/screens/chat/views/channel/ChannelHistoryScrollTest.kt +++ b/app/src/androidTest/java/chat/stoat/screens/chat/views/channel/ChannelHistoryScrollTest.kt @@ -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 { val firstVisibleItem = layoutInfo.visibleItemsInfo .first { it.index == firstVisibleItemIndex } diff --git a/app/src/main/java/chat/stoat/screens/chat/views/channel/ChannelScreen.kt b/app/src/main/java/chat/stoat/screens/chat/views/channel/ChannelScreen.kt index 396d8fb0..65d37ef9 100644 --- a/app/src/main/java/chat/stoat/screens/chat/views/channel/ChannelScreen.kt +++ b/app/src/main/java/chat/stoat/screens/chat/views/channel/ChannelScreen.kt @@ -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,