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

Flutter实现卡片顶部层叠与点击带动画展开的技术求助

实现重叠卡片点击展开动画的解决方案

需求:卡片按顺序顶部重叠排列,点击选中卡片时,该卡片带动画展开,其余卡片下移。现有代码能实现重叠效果,但无法触发预期的展开动画,以下是修正方案。

原始尝试代码

import 'dart:math' as math;
import 'package:flutter/material.dart';

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

  @override
  State<MerchantCards> createState() => _MerchantCardsState();
}

class _MerchantCardsState extends State<MerchantCards> {
  int? _selectedCardIndex;

  void _onCardTap(int index) {
    setState(() {
      _selectedCardIndex = index; // Toggle selection
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Overlapping Animated Cards"),
      ),
      body: ListView.builder(
        itemCount: 3, // Number of cards
        itemBuilder: (context, index) {
          return Align(
              heightFactor: _selectedCardIndex == index ? 2 : 0.3,
              child: GestureDetector(
                  onTap: () => _onCardTap(index),
                  child: SizedBox(
                    height: 200,
                    width: 300,
                    child: Card(
                      color: Color((math.Random().nextDouble() * 0xFFFFFF).toInt())
                              .withOpacity(1.0),
                      child: Center(
                        child: Text("Card ${index + 1}"),
                      ),
                    ),
                  )));
        },
      ),
    );
  }
}

问题分析

原代码使用ListView.builder配合Align的heightFactor控制卡片高度,这种方式没有内置动画过渡,且ListView的线性布局逻辑难以精准控制卡片间的重叠位移动画,无法实现点击时的平滑展开和其余卡片下移效果。

修正方案

改用Stack作为容器,结合AnimatedPositioned控制卡片位置动画,AnimatedSize实现卡片高度的平滑变化,具体代码如下:

import 'dart:math' as math;
import 'package:flutter/material.dart';

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

  @override
  State<MerchantCards> createState() => _MerchantCardsState();
}

class _MerchantCardsState extends State<MerchantCards> with SingleTickerProviderStateMixin {
  int? _selectedCardIndex;
  // 折叠状态下卡片高度与重叠偏移量
  final double _collapsedHeight = 60;
  final double _overlapOffset = 40;
  // 展开状态下卡片高度
  final double _expandedHeight = 200;

  void _onCardTap(int index) {
    setState(() {
      // 切换选中状态:再次点击已选中卡片则取消选中
      _selectedCardIndex = _selectedCardIndex == index ? null : index;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Overlapping Animated Cards"),
      ),
      body: Stack(
        children: List.generate(3, (index) {
          // 计算卡片top位置:选中卡片置顶,未选中卡片按规则偏移
          double topPosition;
          if (_selectedCardIndex == index) {
            topPosition = 0;
          } else if (_selectedCardIndex != null) {
            if (index < _selectedCardIndex!) {
              topPosition = _expandedHeight + (index * _overlapOffset);
            } else {
              topPosition = _expandedHeight + ((index - 1) * _overlapOffset);
            }
          } else {
            topPosition = index * _overlapOffset;
          }

          return AnimatedPositioned(
            duration: const Duration(milliseconds: 300),
            curve: Curves.easeInOut,
            top: topPosition,
            left: 20,
            right: 20,
            child: AnimatedSize(
              duration: const Duration(milliseconds: 300),
              curve: Curves.easeInOut,
              child: GestureDetector(
                onTap: () => _onCardTap(index),
                child: Card(
                  elevation: 4,
                  color: Color((math.Random().nextDouble() * 0xFFFFFF).toInt()).withOpacity(1.0),
                  child: SizedBox(
                    height: _selectedCardIndex == index ? _expandedHeight : _collapsedHeight,
                    child: Center(
                      child: Text("Card ${index + 1}", style: const TextStyle(fontSize: 18)),
                    ),
                  ),
                ),
              ),
            ),
          );
        }).reversed.toList(), // 反转列表确保第一个卡片在底层,最后一个在顶层
      ),
    );
  }
}

关键实现点

  • Stack容器:支持卡片叠放,通过reversed调整层级,保证视觉上的叠放顺序
  • AnimatedPositioned:控制卡片top属性的平滑动画,实现选中卡片置顶、其余卡片下移的过渡效果
  • AnimatedSize:配合卡片高度变化,实现折叠/展开的高度动画
  • 选中逻辑优化:支持再次点击已选中卡片取消选中,恢复初始重叠状态

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.18 06:05:57