You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在启用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,
  },
});

三、关键说明

  1. 真实高度计算:通过onContentSizeChange获取实际内容高度变化,替代估算的estimatedItemHeight,避免因消息高度不一致导致的偏移误差。
  2. 滚动调整时机:在加载历史消息前记录当前状态,待内容高度更新后立即调整滚动偏移,确保视觉位置完全不变。
  3. 状态标记:用isLoadingHistoryRef避免非加载场景下的不必要滚动调整。

额外排查点

  • 确保MessageBubble组件的高度是确定的,避免动态高度导致FlashList重新计算布局时出现跳动。
  • 检查loadConversation返回的user_chat数组顺序是否正确(更早的消息在前,这样追加到messages末尾后,inverted显示时会在顶部)。

内容的提问来源于stack exchange,提问作者sinan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 09:28:10