* fix(#583): align last message bottom to viewport on scroll-to-bottom animateScrollToItem(lastIndex) only top-aligns the last item, so when the last message is taller than the viewport its bottom is clipped below the fold. Replace the 4 scroll-to-bottom call sites (send auto-scroll, session-switch, auto-scroll-on-new-message, and the FAB) with a scrollToBottom() helper that top-aligns then scrolls the exact remaining delta so the last item's bottom edge is visible. Using the remaining delta (not scrollOffset=Int.MAX_VALUE) avoids integer overflow in the internal scroll-position clamp, which would wrap the offset to 0 and scroll back to the top for tall items. * fix(#583): keep smooth animation on scroll-to-bottom when animated agy review (PR #590) flagged that the animated path did an instant scrollToItem then only animated the small remaining delta, causing a hard jump on FAB / send clicks instead of a smooth scroll. Use animateScrollToItem when animated, scrollToItem when instant. --------- Co-authored-by: M57 <m57@hermes-control.app>
This commit is contained in:
1 parent
c446715608
commit
e5e4231183
1 file changed
+41
-4
@@ -20,6 +20,7 @@ import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.gestures.animateScrollBy
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -479,7 +480,7 @@ fun ChatScreen(
|
||||
(if (streamingState.streamingMessage != null) 1 else 0) +
|
||||
(if (streamingState.isThinking) 1 else 0)
|
||||
if (totalItems > 0) {
|
||||
listState.animateScrollToItem(totalItems - 1)
|
||||
listState.scrollToBottom(animated = true)
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1469,6 +1470,42 @@ private fun LazyListState.isAtBottom(threshold: Int = 3): Boolean {
|
||||
return lastVisibleItem.index >= layoutInfo.totalItemsCount - threshold
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll so the bottom edge of the last item is aligned to the bottom of the
|
||||
* viewport (issue #583).
|
||||
*
|
||||
* `animateScrollToItem(lastIndex)` only top-aligns the last item, so when the
|
||||
* last item is taller than the viewport its bottom is clipped below the fold.
|
||||
* We first top-align (instant), then scroll the exact remaining gap so the
|
||||
* bottom is visible. Using the remaining delta (not `scrollOffset = Int.MAX_VALUE`)
|
||||
* avoids integer overflow in the internal scroll-position clamp, which would
|
||||
* otherwise wrap the offset to 0 and scroll back to the top.
|
||||
*/
|
||||
private suspend fun LazyListState.scrollToBottom(animated: Boolean) {
|
||||
val layoutInfo = this.layoutInfo
|
||||
if (layoutInfo.totalItemsCount == 0) return
|
||||
val lastIndex = layoutInfo.totalItemsCount - 1
|
||||
// Top-align the last item first so layoutInfo reflects the last item's offset.
|
||||
// When animated, use the animated variant so the FAB / send clicks keep a
|
||||
// smooth scroll instead of an instant jump followed by a tiny animation.
|
||||
if (animated) {
|
||||
animateScrollToItem(lastIndex)
|
||||
} else {
|
||||
scrollToItem(lastIndex)
|
||||
}
|
||||
val info = this.layoutInfo
|
||||
val lastItem = info.visibleItemsInfo.lastOrNull { it.index == lastIndex } ?: return
|
||||
val remaining =
|
||||
(lastItem.offset + lastItem.size + info.afterContentPadding) - info.viewportEndOffset
|
||||
if (remaining > 0) {
|
||||
if (animated) {
|
||||
animateScrollBy(remaining.toFloat())
|
||||
} else {
|
||||
scroll { scrollBy(remaining.toFloat()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChatLifecycleEffects(
|
||||
sessionId: String?,
|
||||
@@ -1561,9 +1598,9 @@ private fun ChatLifecycleEffects(
|
||||
val isSessionSwitch = currentSessionId != lastSessionId
|
||||
if (isSessionSwitch) {
|
||||
lastSessionId = currentSessionId
|
||||
listState.scrollToItem(totalItems - 1)
|
||||
listState.scrollToBottom(animated = false)
|
||||
} else if (listState.isAtBottom()) {
|
||||
listState.animateScrollToItem(totalItems - 1)
|
||||
listState.scrollToBottom(animated = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1957,7 +1994,7 @@ private fun BoxScope.ChatScrollToBottomFab(
|
||||
(if (streamingMessage != null) 1 else 0) +
|
||||
(if (isThinking) 1 else 0)
|
||||
if (totalItems > 0) {
|
||||
listState.animateScrollToItem(totalItems - 1)
|
||||
listState.scrollToBottom(animated = true)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user