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

Flutter开发:如何同时翻转两张FlipCard并解决状态同步问题

解决方案:Flutter卡片匹配游戏的翻转动画与状态控制

方案一:修复FlipCard包的使用问题

核心问题是手动维护状态导致的不一致,以及未正确处理动画期间的点击锁定。以下是修正后的实现:

1. 定义核心变量

List<FlipCardController> _cardControllers = List.generate(12, (_) => FlipCardController());
bool _isProcessing = false; // 动画处理中禁止点击
List<int> _pickedCards = [];
List<String> _cardValues = ['A','A','B','B','C','C','D','D','E','E','F','F']; // 卡片匹配值

2. 构建卡片网格

GridView.builder(
  gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3),
  itemCount: 12,
  itemBuilder: (context, index) {
    return FlipCard(
      controller: _cardControllers[index],
      front: const Card(child: Center(child: Text('背面'))),
      back: Card(child: Center(child: Text(_cardValues[index]))),
      onTap: _isProcessing ? null : () => _onCardTap(index), // 处理中禁用点击
    );
  },
)

3. 卡片点击与匹配校验逻辑

void _onCardTap(int index) {
  // 只允许选择未翻转的卡片,且最多选2张
  if (_pickedCards.length < 2 && _cardControllers[index].state!.isFront) {
    _pickedCards.add(index);
    _cardControllers[index].toggleCard();
    
    // 选满2张开始校验
    if (_pickedCards.length == 2) {
      _checkMatch();
    }
  }
}

void _checkMatch() async {
  _isProcessing = true;
  setState(() {});
  
  // 等待翻转动画完成(默认300ms,留50ms缓冲)
  await Future.delayed(const Duration(milliseconds: 350));
  
  bool isMatch = _cardValues[_pickedCards[0]] == _cardValues[_pickedCards[1]];
  
  if (!isMatch) {
    // 不匹配时延迟1秒给用户查看,再翻回
    await Future.delayed(const Duration(milliseconds: 1000));
    _cardControllers[_pickedCards[0]].toggleCard();
    _cardControllers[_pickedCards[1]].toggleCard();
  }
  
  // 重置状态
  _pickedCards.clear();
  _isProcessing = false;
  setState(() {});
}

关键改进点:

  • 用_isProcessing全局锁禁止动画期间的点击,避免并发操作
  • 不再手动维护isFront状态,直接通过FlipCardController的state获取真实状态
  • 等待动画完成后再执行校验和翻回操作,确保状态同步

方案二:自定义翻转动画(不依赖FlipCard包)

如果不想用第三方包,可以自己实现可控的翻转动画,避免依赖问题:

1. 自定义翻转卡片Widget

class FlipCardWidget extends StatefulWidget {
  final String frontText;
  final String backText;
  final bool isFlipped;
  final VoidCallback? onTap;

  const FlipCardWidget({
    super.key,
    required this.frontText,
    required this.backText,
    required this.isFlipped,
    this.onTap,
  });

  @override
  State<FlipCardWidget> createState() => _FlipCardWidgetState();
}

class _FlipCardWidgetState extends State<FlipCardWidget> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(milliseconds: 300),
      vsync: this,
    );
    _animation = Tween<double>(begin: 0, end: 1).animate(_controller);
    // 初始化时同步翻转状态
    if (widget.isFlipped) _controller.value = 1;
  }

  @override
  void didUpdateWidget(covariant FlipCardWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    // 状态变化时触发动画
    if (widget.isFlipped != oldWidget.isFlipped) {
      widget.isFlipped ? _controller.forward() : _controller.reverse();
    }
  }

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

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: widget.onTap,
      child: AnimatedBuilder(
        animation: _animation,
        builder: (context, child) {
          final isFront = _animation.value < 0.5;
          return Transform(
            transform: Matrix4.identity()
              ..setEntry(3, 2, 0.001) // 添加透视效果
              ..rotateY(_animation.value * pi),
            alignment: Alignment.center,
            child: isFront
                ? Card(child: Center(child: Text(widget.frontText)))
                : Transform(
                    // 背面需要反转,避免文字倒置
                    transform: Matrix4.identity()..rotateY(pi),
                    alignment: Alignment.center,
                    child: Card(child: Center(child: Text(widget.backText))),
                  ),
          );
        },
      ),
    );
  }
}

2. 主页面使用自定义卡片

List<bool> _isFlipped = List.generate(12, (_) => false);
bool _isProcessing = false;
List<int> _pickedCards = [];
List<String> _cardValues = ['A','A','B','B','C','C','D','D','E','E','F','F'];

GridView.builder(
  gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3),
  itemCount: 12,
  itemBuilder: (context, index) {
    return FlipCardWidget(
      frontText: '背面',
      backText: _cardValues[index],
      isFlipped: _isFlipped[index],
      onTap: _isProcessing ? null : () => _onCardTap(index),
    );
  },
)

3. 点击与校验逻辑

void _onCardTap(int index) {
  if (_pickedCards.length < 2 && !_isFlipped[index]) {
    setState(() => _isFlipped[index] = true);
    _pickedCards.add(index);
    
    if (_pickedCards.length == 2) {
      _checkMatch();
    }
  }
}

void _checkMatch() async {
  _isProcessing = true;
  setState(() {});
  
  await Future.delayed(const Duration(milliseconds: 300)); // 等待动画完成
  
  bool isMatch = _cardValues[_pickedCards[0]] == _cardValues[_pickedCards[1]];
  
  if (!isMatch) {
    await Future.delayed(const Duration(milliseconds: 1000));
    setState(() {
      _isFlipped[_pickedCards[0]] = false;
      _isFlipped[_pickedCards[1]] = false;
    });
  }
  
  _pickedCards.clear();
  _isProcessing = false;
  setState(() {});
}

优势:完全可控的动画逻辑,避免第三方包的状态同步问题,自定义程度更高。


内容的提问来源于stack exchange,提问作者Giovanni Caciuppo

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 19:33:13