如何在启用inverted的FlashList中加载历史消息时保留滚动位置?
问题分析与解决方案
你的核心问题出在数据更新顺序和inverted模式下的滚动偏移计算逻辑上,同时依赖估算高度而非真实内容高度也会导致误差。以下是具体修正方案:
一、先修正数据更新逻辑(最关键)
在conversationSlice的reducer中,必须确保新加载的历史消息被追加到messages数组末尾。因为inverted={true}时,FlashList会反转显示顺序:数组第一个元素显示在屏幕底部(最新消息),最后一个元素显示在屏幕顶部(最旧消息)。如果把历史消息加到数组开头,会导致它们被显示在底部,直接触发滚动到底部的问题。
// conversationSlice中的reducer示例 case loadConversation.fulfilled: return { ...state, // 将历史消息追加到数组末尾,而非开头 messages: [...state.messages, ...(action.payload.user_chat || [])], start: action.payload.start, fullyLoaded: action.payload.user_chat?.length === 0, };
二、修正滚动偏移计算与调整逻辑
使用真实内容高度替代估算值,并在内容更新后精准调整滚动位置:
import { loadConversation, selectChatState, } from "@/src/store/features/conversation/conversationSlice"; import { ChatItem } from "@/src/store/features/conversation/types"; import { useAppDispatch, useAppSelector } from "@/src/store/hooks/storeHooks"; import cnStyles from "@/src/styles"; import { FlashList } from "@shopify/flash-list"; import React, { FC, useCallback, useRef } from "react"; import { KeyboardAvoidingView, StyleSheet, View } from "react-native"; import BlockedStrip from "./BlockedStrip"; import InputBox from "./InputBox"; import { MessageBubble } from "./MessageBubble"; import PrevLoader from "./PrevLoader"; type PropType = { SendMessage: (value: string) => void; }; export const ChatList: FC<PropType> = ({ SendMessage }) => { const flatListRef = useRef<FlashList<ChatItem>>(null); const dispatch = useAppDispatch(); const loadingMoreRef = useRef(false); const prevContentHeightRef = useRef(0); // 记录加载前的内容高度 const currentScrollOffsetRef = useRef(0); // 记录加载前的滚动偏移 const isLoadingHistoryRef = useRef(false); // 标记是否正在加载历史消息 const { isLoading, messages, start, userInfo, fullyLoaded, you_id } = useAppSelector(selectChatState); // 加载历史消息 const loadPreviousMessages = useCallback(async () => { if (!fullyLoaded && !isLoading && !loadingMoreRef.current) { loadingMoreRef.current = true; isLoadingHistoryRef.current = true; // 记录加载前的内容高度和滚动偏移 flatListRef.current?.measure((_x, _y, _width, height) => { prevContentHeightRef.current = height; }); currentScrollOffsetRef.current = flatListRef.current?.getScrollableNode()?.scrollOffset || 0; try { const res = await dispatch( loadConversation({ reqBody: { start, conv_id: you_id }, }) ).unwrap(); if (res.success !== "1") { // 处理加载失败逻辑 } } finally { loadingMoreRef.current = false; } } }, [fullyLoaded, isLoading, start, you_id, dispatch]); // 内容高度变化时调整滚动位置 const handleContentSizeChange = useCallback((_width: number, height: number) => { if (isLoadingHistoryRef.current && height > prevContentHeightRef.current) { const heightDiff = height - prevContentHeightRef.current; // inverted模式下,新增顶部内容后,需将偏移量加上高度差以保持原位置 flatListRef.current?.scrollToOffset({ offset: currentScrollOffsetRef.current + heightDiff, animated: false, }); isLoadingHistoryRef.current = false; } prevContentHeightRef.current = height; }, []); const onSubmitMsg = (msg: string) => { SendMessage(msg); setTimeout(() => { flatListRef.current?.scrollToEnd({ animated: true }); }, 10); }; return ( <KeyboardAvoidingView style={cnStyles.container} behavior="padding"> <View style={styles.messagesList}> <FlashList ref={flatListRef} data={messages} keyExtractor={(item) => item.key} renderItem={({ item }) => <MessageBubble item={item} />} estimatedItemSize={100} ListFooterComponent={isLoading ? <PrevLoader /> : null} inverted={true} onScroll={(event) => { currentScrollOffsetRef.current = event.nativeEvent.contentOffset.y; }} scrollEventThrottle={16} onEndReached={loadPreviousMessages} onEndReachedThreshold={0.1} onContentSizeChange={handleContentSizeChange} // 监听内容高度变化 /> </View> {userInfo?.button_status === 4 ? ( <BlockedStrip /> ) : ( <InputBox onSubmit={onSubmitMsg} /> )} </KeyboardAvoidingView> ); }; const styles = StyleSheet.create({ messagesList: { flex: 1, }, });
三、关键说明
- 真实高度计算:通过
onContentSizeChange获取实际内容高度变化,替代估算的estimatedItemHeight,避免因消息高度不一致导致的偏移误差。 - 滚动调整时机:在加载历史消息前记录当前状态,待内容高度更新后立即调整滚动偏移,确保视觉位置完全不变。
- 状态标记:用
isLoadingHistoryRef避免非加载场景下的不必要滚动调整。
额外排查点
- 确保
MessageBubble组件的高度是确定的,避免动态高度导致FlashList重新计算布局时出现跳动。 - 检查
loadConversation返回的user_chat数组顺序是否正确(更早的消息在前,这样追加到messages末尾后,inverted显示时会在顶部)。
内容的提问来源于stack exchange,提问作者sinan
相关产品推荐
相关产品推荐

