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

将Flutter卡片堆叠动画从翻转动画改为实体卡片浏览效果

Flutter实体卡片堆叠浏览实现方案

核心思路:放弃原rotateX的3D翻转动画,改用Stack+Positioned组合模拟实体堆叠层级,配合AnimationController处理卡片切换的平移/缩放动画,通过GestureDetector实现点击、滑动交互。

关键实现细节

  • 堆叠层级模拟:只显示顶部3张卡片,非顶部卡片通过top偏移、scale缩放、opacity透明度营造堆叠感,后面的卡片直接隐藏以优化性能。
  • 交互逻辑:
    • 点击顶部卡片:触发动画将顶部卡片移出屏幕(向右平移+淡出),动画结束后将该卡片移至列表末尾,下一张卡片成为顶部。
    • 左右滑动:拖动顶部卡片时实时跟随手势偏移,松手后根据拖动距离/速度判断是否触发切换,完成后更新列表顺序。
  • 动画控制:用AnimationController驱动平移、缩放、透明度动画,确保切换过程流畅自然。

完整代码示例

import 'package:flutter/material.dart';

void main() => runApp(const CardStackApp());

class CardStackApp extends StatelessWidget {
  const CardStackApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        backgroundColor: Colors.grey[100],
        appBar: AppBar(title: const Text('实体卡片堆叠')),
        body: const CardStack(),
      ),
    );
  }
}

class CardItem {
  final String title;
  final Color color;

  CardItem({required this.title, required this.color});
}

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

  @override
  State<CardStack> createState() => _CardStackState();
}

class _CardStackState extends State<CardStack> with SingleTickerProviderStateMixin {
  late AnimationController _animationController;
  late Animation<double> _translateAnimation;
  late Animation<double> _opacityAnimation;
  late Animation<double> _scaleAnimation;

  final List<CardItem> _cardList = [
    CardItem(title: '卡片1', color: Colors.red),
    CardItem(title: '卡片2', color: Colors.blue),
    CardItem(title: '卡片3', color: Colors.green),
    CardItem(title: '卡片4', color: Colors.orange),
    CardItem(title: '卡片5', color: Colors.purple),
  ];

  // 拖动时的实时偏移量
  double _dragOffset = 0;

  @override
  void initState() {
    super.initState();
    _animationController = AnimationController(
      duration: const Duration(milliseconds: 300),
      vsync: this,
    );

    // 初始化动画曲线
    _translateAnimation = Tween<double>(begin: 0, end: MediaQuery.of(context).size.width * 1.2)
        .animate(CurvedAnimation(parent: _animationController, curve: Curves.easeInOut));
    _opacityAnimation = Tween<double>(begin: 1, end: 0)
        .animate(CurvedAnimation(parent: _animationController, curve: Curves.easeIn));
    _scaleAnimation = Tween<double>(begin: 1, end: 0.9)
        .animate(CurvedAnimation(parent: _animationController, curve: Curves.easeOut));

    _animationController.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        // 动画结束后,将顶部卡片移至列表末尾
        setState(() {
          final topCard = _cardList.removeAt(0);
          _cardList.add(topCard);
          _dragOffset = 0;
        });
        _animationController.reset();
      }
    });
  }

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

  // 触发卡片切换动画
  void _triggerCardSwitch({bool isLeft = false}) {
    setState(() {
      // 切换动画方向(左滑/右滑)
      _translateAnimation = Tween<double>(
        begin: _dragOffset,
        end: isLeft ? -MediaQuery.of(context).size.width * 1.2 : MediaQuery.of(context).size.width * 1.2,
      ).animate(CurvedAnimation(parent: _animationController, curve: Curves.easeInOut));
    });
    _animationController.forward();
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: SizedBox(
        height: 400,
        child: GestureDetector(
          onTap: () => _triggerCardSwitch(),
          onHorizontalDragUpdate: (details) {
            setState(() {
              _dragOffset += details.delta.dx;
            });
          },
          onHorizontalDragEnd: (details) {
            // 根据拖动距离和速度判断是否触发切换
            if (_dragOffset.abs() > 100 || details.velocity.pixelsPerSecond.dx.abs() > 500) {
              _triggerCardSwitch(isLeft: _dragOffset < 0);
            } else {
              // 拖动距离不足,回弹
              setState(() => _dragOffset = 0);
            }
          },
          child: Stack(
            alignment: Alignment.center,
            children: [
              // 第3层卡片
              if (_cardList.length >= 3)
                Positioned(
                  top: 20,
                  child: Opacity(
                    opacity: 0.8,
                    child: Transform.scale(
                      scale: 0.9,
                      child: _buildCard(_cardList[2]),
                    ),
                  ),
                ),
              // 第2层卡片
              if (_cardList.length >= 2)
                Positioned(
                  top: 10,
                  child: Opacity(
                    opacity: 0.9,
                    child: Transform.scale(
                      scale: 0.95,
                      child: _buildCard(_cardList[1]),
                    ),
                  ),
                ),
              // 顶部卡片(带动画)
              AnimatedBuilder(
                animation: _animationController,
                builder: (context, child) {
                  return Transform.translate(
                    offset: Offset(_translateAnimation.value + _dragOffset, 0),
                    child: Opacity(
                      opacity: _opacityAnimation.value,
                      child: Transform.scale(
                        scale: _scaleAnimation.value,
                        child: _buildCard(_cardList[0]),
                      ),
                    ),
                  );
                },
              ),
            ],
          ),
        ),
      ),
    );
  }

  Widget _buildCard(CardItem item) {
    return Container(
      width: 300,
      height: 380,
      decoration: BoxDecoration(
        color: item.color,
        borderRadius: BorderRadius.circular(16),
        boxShadow: const [
          BoxShadow(
            color: Colors.black26,
            blurRadius: 8,
            offset: Offset(0, 4),
          ),
        ],
      ),
      child: Center(
        child: Text(
          item.title,
          style: const TextStyle(fontSize: 24, color: Colors.white, fontWeight: FontWeight.bold),
        ),
      ),
    );
  }
}

可调整参数

  • 修改Positioned的top值、Transform.scale的scale值、Opacity的opacity值,可调整堆叠的紧凑程度。
  • 调整AnimationController的duration,可改变切换动画的快慢。
  • 拖动触发阈值(_dragOffset.abs() > 100)可根据需求调整。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 08:52:01