如何使用Flutter实现Apple Wallet卡片的交互行为?
实现Apple Wallet风格的卡片交互效果
核心思路
放弃Hero组件(它更适合跨页面跳转场景),改用同一页面内的状态管理+动画组件实现:通过维护选中卡片的状态,控制每个卡片的尺寸、位置和展示内容,用AnimatedContainer实现平滑过渡动画。
完整实现代码
import 'package:flutter/material.dart'; class WalletCard { final String id; final String title; final Color color; final String icon; WalletCard({ required this.id, required this.title, required this.color, required this.icon, }); } class WalletPage extends StatefulWidget { const WalletPage({super.key}); @override State<WalletPage> createState() => _WalletPageState(); } class _WalletPageState extends State<WalletPage> { final List<WalletCard> _cards = [ WalletCard(id: '1', title: '会员卡', color: Colors.blue.shade700, icon: '💳'), WalletCard(id: '2', title: '优惠券', color: Colors.orange.shade700, icon: '🎟️'), WalletCard(id: '3', title: '门禁卡', color: Colors.green.shade700, icon: '🔑'), WalletCard(id: '4', title: '交通卡', color: Colors.purple.shade700, icon: '🚇'), ]; String? _selectedCardId; void _toggleCardSelection(String cardId) { setState(() { _selectedCardId = _selectedCardId == cardId ? null : cardId; }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Wallet 风格卡片')), body: Column( children: [ // 顶部展示选中的卡片 if (_selectedCardId != null) AnimatedContainer( duration: const Duration(milliseconds: 350), curve: Curves.easeInOut, height: 220, margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: GestureDetector( onTap: () => _toggleCardSelection(_selectedCardId!), child: _buildCard(_cards.firstWhere((c) => c.id == _selectedCardId)), ), ), // 下方展示折叠/正常状态的卡片列表 Expanded( child: ListView.builder( padding: const EdgeInsets.symmetric(horizontal: 16), itemCount: _cards.length, itemBuilder: (context, index) { final card = _cards[index]; // 选中的卡片已经在顶部显示,这里隐藏 if (_selectedCardId != null && card.id == _selectedCardId) { return const SizedBox.shrink(); } return AnimatedContainer( duration: const Duration(milliseconds: 350), curve: Curves.easeInOut, height: _selectedCardId != null ? 65 : 130, margin: const EdgeInsets.only(bottom: 8), child: GestureDetector( onTap: () => _toggleCardSelection(card.id), child: _buildCard(card, isCollapsed: _selectedCardId != null), ), ); }, ), ), ], ), ); } Widget _buildCard(WalletCard card, {bool isCollapsed = false}) { return Container( decoration: BoxDecoration( color: card.color, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.15), blurRadius: 10, offset: const Offset(0, 3), ), ], ), padding: const EdgeInsets.all(16), child: isCollapsed ? Row( children: [ Text(card.icon, style: const TextStyle(fontSize: 26)), const SizedBox(width: 16), Text( card.title, style: const TextStyle(fontSize: 18, color: Colors.white, fontWeight: FontWeight.w500), ), ], ) : Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ Text(card.icon, style: const TextStyle(fontSize: 36)), const SizedBox(height: 16), Text( card.title, style: const TextStyle(fontSize: 22, color: Colors.white, fontWeight: FontWeight.w600), ), const SizedBox(height: 8), const Text('余额: ¥128.00', style: TextStyle(color: Colors.white70, fontSize: 14)), ], ), ); } }
关键细节说明
- 状态管理:通过
_selectedCardId标记当前选中的卡片,点击时切换状态(选中/取消选中) - 动画过渡:用
AnimatedContainer自动处理高度变化的平滑动画,配合Curves.easeInOut让动画更自然 - 布局逻辑:选中的卡片单独放在顶部区域,列表中隐藏该卡片;未选中时所有卡片以正常尺寸展示,点击任意卡片触发状态切换
- 卡片样式:模拟Apple Wallet的圆角、阴影设计,折叠状态只展示图标和标题,展开状态显示完整信息
可选优化方向
- 添加卡片展开时的缩放动画:用
Transform.scale配合AnimatedBuilder实现 - 支持手势返回:给顶部卡片添加向下滑动手势,触发取消选中操作
- 调整z-index:确保选中的卡片在最上层,避免被其他元素遮挡
内容的提问来源于stack exchange,提问作者Abolela-H45
相关产品推荐
相关产品推荐

