fix(chat): 新消息自动跟随底部(微信/QQ 式)

reverseLayout 会锚定原可见项,导致新消息插到视口外。改为:滚动结束时记录是否贴底(容差 64px),最新一项变化且处于跟随状态时 animateScrollToItem(0);用户上滑查看历史时不再强制滚动。
This commit is contained in:
AskaEth committed 2026-09-08 21:16:49 +08:00
1 parent 67ed0479e4
commit e04995a84d
1 file changed
+28
@@ -18,6 +18,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -40,6 +43,9 @@ import top.yukonga.miuix.kmp.icon.MiuixIcons
import top.yukonga.miuix.kmp.icon.basic.Sidebar import top.yukonga.miuix.kmp.icon.basic.Sidebar
import top.yukonga.miuix.kmp.theme.MiuixTheme import top.yukonga.miuix.kmp.theme.MiuixTheme
/** 判定「贴着底部」的容差(px)。 */
private const val BOTTOM_TOLERANCE = 64
@Composable @Composable
fun ChatScreen( fun ChatScreen(
container: AppContainer, container: AppContainer,
@@ -52,6 +58,28 @@ fun ChatScreen(
val state by viewModel.state.collectAsState() val state by viewModel.state.collectAsState()
val listState = rememberLazyListState() val listState = rememberLazyListState()
// 是否跟随底部(微信/QQ 式):只有用户主动滚到别处时才停止跟随。
// 只在「滚动结束」时更新,避免新消息插入导致的锚点位移把状态带偏。
var followBottom by remember { mutableStateOf(true) }
LaunchedEffect(listState) {
snapshotFlow { listState.isScrollInProgress }
.distinctUntilChanged()
.collect { scrolling ->
if (!scrolling) {
followBottom = listState.firstVisibleItemIndex == 0 &&
listState.firstVisibleItemScrollOffset < BOTTOM_TOLERANCE
}
}
}
// 最新一项变化(新消息 / 乐观气泡 / 流式气泡 / 落库归一)时,跟随则滚到底。
val newestKey = state.items.lastOrNull()?.key
LaunchedEffect(newestKey) {
if (newestKey != null && followBottom) {
listState.animateScrollToItem(0)
}
}
// reverseLayout 下「列表末尾」就是屏幕顶部;接近末尾时加载更早的一页。 // reverseLayout 下「列表末尾」就是屏幕顶部;接近末尾时加载更早的一页。
LaunchedEffect(listState) { LaunchedEffect(listState) {
snapshotFlow { snapshotFlow {