From e04995a84d3134ce91ef52e3463bd06a5e434758 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Tue, 8 Sep 2026 21:16:49 +0800 Subject: [PATCH] =?UTF-8?q?fix(chat):=20=E6=96=B0=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E8=B7=9F=E9=9A=8F=E5=BA=95=E9=83=A8=EF=BC=88?= =?UTF-8?q?=E5=BE=AE=E4=BF=A1/QQ=20=E5=BC=8F=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit reverseLayout 会锚定原可见项,导致新消息插到视口外。改为:滚动结束时记录是否贴底(容差 64px),最新一项变化且处于跟随状态时 animateScrollToItem(0);用户上滑查看历史时不再强制滚动。 --- .../top/yeij/cyrene/ui/chat/ChatScreen.kt | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/app/src/main/kotlin/top/yeij/cyrene/ui/chat/ChatScreen.kt b/app/src/main/kotlin/top/yeij/cyrene/ui/chat/ChatScreen.kt index 7f4bec4..47de3f3 100644 --- a/app/src/main/kotlin/top/yeij/cyrene/ui/chat/ChatScreen.kt +++ b/app/src/main/kotlin/top/yeij/cyrene/ui/chat/ChatScreen.kt @@ -18,6 +18,9 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState 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.ui.Alignment 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.theme.MiuixTheme +/** 判定「贴着底部」的容差(px)。 */ +private const val BOTTOM_TOLERANCE = 64 + @Composable fun ChatScreen( container: AppContainer, @@ -52,6 +58,28 @@ fun ChatScreen( val state by viewModel.state.collectAsState() 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 下「列表末尾」就是屏幕顶部;接近末尾时加载更早的一页。 LaunchedEffect(listState) { snapshotFlow {