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

如何在Flutter中构建支持动画的类Twitter信息流?

Flutter实现带顶部插入动画的Twitter式信息流

要实现下拉刷新时在顶部添加新推文并保留动画,同时保持原有焦点项的屏幕位置,不需要更换AnimatedList的GlobalKey,可以通过手动管理滚动偏移结合AnimatedList的原生动画能力来实现,具体步骤如下:

核心思路

  1. 保留AnimatedList的全局Key,用于触发项的插入动画;
  2. 下拉刷新获取新数据后,先通过AnimatedListState.insertItem()在顶部插入新项,触发入场动画;
  3. 计算新插入项的总高度,调整滚动控制器的偏移量,让原有可见项保持在屏幕原位置。

代码实现示例

1. 定义核心变量

import 'package:flutter/material.dart';

class Post {
  final String id;
  final String content;
  Post({required this.id, required this.content});
}

class TwitterFeedPage extends StatefulWidget {
  const TwitterFeedPage({super.key});

  @override
  State<TwitterFeedPage> createState() => _TwitterFeedPageState();
}

class _TwitterFeedPageState extends State<TwitterFeedPage> {
  // 保留AnimatedList的全局Key,用于控制动画
  final GlobalKey<AnimatedListState> _animatedListKey = GlobalKey<AnimatedListState>();
  final ScrollController _scrollController = ScrollController();
  List<Post> _posts = [];

  @override
  void initState() {
    super.initState();
    // 初始化模拟数据
    _posts = List.generate(20, (index) => Post(id: '$index', content: '推文 $index'));
  }

2. 下拉刷新逻辑

Future<void> _onRefresh() async {
    // 模拟获取新推文数据
    await Future.delayed(const Duration(seconds: 1));
    final List<Post> newPosts = List.generate(3, (index) => Post(id: 'new_$index', content: '新推文 $index'));

    // 记录当前滚动偏移
    final double currentOffset = _scrollController.offset;
    // 计算新项总高度(如果是固定高度可直接计算,动态高度需后续获取)
    final double fixedItemHeight = 100.0;
    double totalNewHeight = newPosts.length * fixedItemHeight;

    // 插入新项到列表头部,触发动画
    setState(() {
      _posts.insertAll(0, newPosts);
    });
    for (int i = 0; i < newPosts.length; i++) {
      _animatedListKey.currentState?.insertItem(0);
    }

    // 调整滚动偏移,保持原有项的位置
    if (currentOffset > 0) {
      _scrollController.jumpTo(currentOffset + totalNewHeight);
    }
  }

3. 构建AnimatedList

@override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Twitter式信息流')),
      body: RefreshIndicator(
        onRefresh: _onRefresh,
        child: AnimatedList(
          key: _animatedListKey,
          controller: _scrollController,
          itemBuilder: (context, index, animation) {
            // 自定义入场动画
            return SlideTransition(
              position: animation.drive(
                Tween<Offset>(
                  begin: const Offset(-1.0, 0.0),
                  end: Offset.zero,
                ).chain(CurveTween(curve: Curves.easeOut)),
              ),
              child: _buildPostItem(_posts[index]),
            );
          },
          initialItemCount: _posts.length,
        ),
      ),
    );
  }

  Widget _buildPostItem(Post post) {
    return Container(
      height: 100.0,
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      decoration: const BoxDecoration(
        border: Border(bottom: BorderSide(color: Colors.grey.shade200)),
      ),
      child: Text(post.content, style: const TextStyle(fontSize: 16)),
    );
  }
}

动态高度项的处理

如果推文项高度不固定,可在插入新项后,通过WidgetsBinding的addPostFrameCallback获取新项的实际高度:

// 在_onRefresh的插入项后添加
WidgetsBinding.instance.addPostFrameCallback((_) {
  double totalNewHeight = 0;
  for (int i = 0; i < newPosts.length; i++) {
    // 通过项的Key查找RenderBox
    final key = ValueKey(_posts[i].id);
    final RenderBox? renderBox = context.findRenderObjectForKey(key) as RenderBox?;
    if (renderBox != null) {
      totalNewHeight += renderBox.size.height;
    }
  }
  _scrollController.jumpTo(_scrollController.offset + totalNewHeight);
});

注意需要给每个PostItem设置唯一Key:

Widget _buildPostItem(Post post) {
  return Container(
    key: ValueKey(post.id), // 添加唯一Key
    // ... 其他代码
  );
}

关键优势

  • 无需更换AnimatedList的全局Key,完美保留插入动画;
  • 通过滚动偏移调整,精准保持原有焦点项的屏幕位置,和Twitter的交互逻辑一致;
  • 支持固定/动态高度的列表项,适配不同场景。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.26 13:45:33