将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
相关产品推荐
相关产品推荐

