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

如何使用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)),
              ],
            ),
    );
  }
}

关键细节说明

  1. 状态管理:通过_selectedCardId标记当前选中的卡片,点击时切换状态(选中/取消选中)
  2. 动画过渡:用AnimatedContainer自动处理高度变化的平滑动画,配合Curves.easeInOut让动画更自然
  3. 布局逻辑:选中的卡片单独放在顶部区域,列表中隐藏该卡片;未选中时所有卡片以正常尺寸展示,点击任意卡片触发状态切换
  4. 卡片样式:模拟Apple Wallet的圆角、阴影设计,折叠状态只展示图标和标题,展开状态显示完整信息

可选优化方向

  • 添加卡片展开时的缩放动画:用Transform.scale配合AnimatedBuilder实现
  • 支持手势返回:给顶部卡片添加向下滑动手势,触发取消选中操作
  • 调整z-index:确保选中的卡片在最上层,避免被其他元素遮挡

内容的提问来源于stack exchange,提问作者Abolela-H45

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.25 05:20:38