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

Flutter实现列表项点击动画展开弹窗并返回列表的需求

实现方案

要实现列表项点击动画展开为全屏弹窗、返回时缩回的效果,核心思路是通过状态管理跟踪选中项+动画控制器控制尺寸/位置/透明度变化+Stack叠加布局完成,具体实现步骤如下:


1. 改造为StatefulWidget并添加动画状态变量

先将页面改为StatefulWidget,在状态类中声明动画及选中状态相关变量:

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

  @override
  State<FactsPage> createState() => _FactsPageState();
}

class _FactsPageState extends State<FactsPage> with SingleTickerProviderStateMixin {
  int? _selectedIndex; // 记录选中的列表项索引
  final Map<int, GlobalKey> _itemKeys = {}; // 存储每个列表项的GlobalKey,用于获取位置
  late AnimationController _animationController;
  late Animation<double> _scaleAnim;
  late Animation<double> _opacityAnim;
  Rect? _itemRect; // 选中项的初始位置矩形

  @override
  void initState() {
    super.initState();
    // 初始化动画控制器,时长可按需调整
    _animationController = AnimationController(
      duration: const Duration(milliseconds: 300),
      vsync: this,
    );
    // 缩放动画:从初始项尺寸过渡到全屏
    _scaleAnim = Tween<double>(begin: 0, end: 1).animate(
      CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
    );
    // 透明度动画:避免弹窗突兀出现
    _opacityAnim = Tween<double>(begin: 0, end: 1).animate(
      CurvedAnimation(parent: _animationController, curve: Curves.easeIn),
    );
    // 监听反向动画结束,重置选中状态
    _animationController.addStatusListener((status) {
      if (status == AnimationStatus.dismissed) {
        setState(() => _selectedIndex = null);
      }
    });
  }

  @override
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }

2. 给列表项绑定点击事件与位置追踪

修改列表项的构建逻辑,添加GlobalKey和点击触发动画的逻辑:

Widget _buildFactItem(int index) {
  // 为每个列表项生成唯一GlobalKey
  _itemKeys.putIfAbsent(index, () => GlobalKey());
  final factWidget = widgetsList[index];

  return GestureDetector(
    key: _itemKeys[index],
    onTap: () {
      setState(() => _selectedIndex = index);
      // 获取列表项在屏幕上的精确位置
      final renderBox = _itemKeys[index].currentContext?.findRenderObject() as RenderBox?;
      if (renderBox != null) {
        _itemRect = renderBox.localToGlobal(Offset.zero) & renderBox.size;
      }
      // 启动正向展开动画
      _animationController.forward();
    },
    child: factWidget,
  );
}

3. 用Stack布局实现动画弹窗

修改原页面的body结构,用Stack包裹原列表和动画弹窗:

@override
Widget build(BuildContext context) {
  final screenSize = MediaQuery.of(context).size;

  return Scaffold(
    appBar: const PreferredSize(
      preferredSize: Size.fromHeight(50), 
      child: FactsAppBar()
    ),
    drawer: const FactsAppDrawer(),
    body: Stack(
      children: [
        // 原列表视图
        GlowingOverscrollIndicator(
          axisDirection: AxisDirection.down,
          color: Theme.of(context).primaryColor,
          child: ListView.separated(
            itemCount: widgetsList.length,
            physics: const AlwaysScrollableScrollPhysics(),
            itemBuilder: (_, int index) => _buildFactItem(index),
            separatorBuilder: (_, div) => const Divider(thickness: 1.5),
          ),
        ),
        // 动画弹窗(仅选中项时显示)
        if (_selectedIndex != null && _itemRect != null)
          AnimatedBuilder(
            animation: _animationController,
            builder: (context, child) {
              // 计算动画过渡中的位置与尺寸
              final targetRect = Rect.fromLTWH(0, 0, screenSize.width, screenSize.height);
              final currentRect = Rect.lerp(_itemRect, targetRect, _scaleAnim.value);

              return Positioned.fromRect(
                rect: currentRect!,
                child: Opacity(
                  opacity: _opacityAnim.value,
                  child: Container(
                    decoration: BoxDecoration(
                      color: Theme.of(context).scaffoldBackgroundColor,
                      borderRadius: BorderRadius.lerp(
                        // 从列表项圆角过渡到全屏直角
                        BorderRadius.circular(8),
                        BorderRadius.zero,
                        _scaleAnim.value,
                      ),
                    ),
                    child: Column(
                      children: [
                        // 返回按钮栏
                        AppBar(
                          leading: IconButton(
                            icon: const Icon(Icons.arrow_back),
                            onPressed: () => _animationController.reverse(), // 启动缩回动画
                          ),
                          backgroundColor: Theme.of(context).primaryColor,
                        ),
                        // 事实详情文本
                        Expanded(
                          child: Padding(
                            padding: const EdgeInsets.all(16.0),
                            child: SingleChildScrollView(
                              child: Text(
                                // 替换为对应事实的实际文本内容
                                "第${_selectedIndex! + 1}条事实的详细内容",
                                style: const TextStyle(fontSize: 18),
                              ),
                            ),
                          ),
                        ),
                        // 底部操作按钮
                        Padding(
                          padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
                          child: Row(
                            mainAxisAlignment: MainAxisAlignment.end,
                            children: [
                              TextButton(
                                onPressed: () {}, // 保存逻辑自行实现
                                child: const Text("保存"),
                              ),
                              const SizedBox(width: 8),
                              TextButton(
                                onPressed: () {}, // 分享逻辑自行实现
                                child: const Text("分享"),
                              ),
                            ],
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
              );
            },
          ),
      ],
    ),
  );
}

关键细节说明

  • GlobalKey作用:精准获取列表项的屏幕位置,保证动画从原位置平滑展开。
  • 动画曲线:使用Curves.easeInOut让过渡更自然,避免生硬跳转。
  • 圆角过渡:如果你的列表项本身带有圆角,通过BorderRadius.lerp实现圆角到直角的平滑变化,提升视觉连贯性。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.05 16:05:25