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

Flutter卡牌游戏开发:实现卡牌平滑动画移至弃牌堆

Flutter卡牌移动至弃牌堆的动画解决方案

问题1:AnimatedPositioned无动画的修复

AnimatedPositioned触发动画的核心条件:

  • 父容器必须是Stack组件
  • 通过setState修改left/top等位置参数,且前后参数值存在明确差异

示例代码:

class _CardState extends State<CardWidget> {
  double _cardLeft = 100; // 手牌初始X位置
  double _cardTop = 400; // 手牌初始Y位置
  final _discardLeft = 20; // 弃牌堆基准X位置
  final _discardTop = 20; // 弃牌堆基准Y位置

  void moveToDiscard() {
    setState(() {
      // 修改位置参数触发动画
      _cardLeft = _discardLeft;
      _cardTop = _discardTop;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        AnimatedPositioned(
          duration: const Duration(milliseconds: 500),
          left: _cardLeft,
          top: _cardTop,
          child: GestureDetector(
            onTap: moveToDiscard,
            child: Image.asset('assets/card.png', width: 100, height: 150),
          ),
        ),
      ],
    );
  }
}

问题2:SlideTransition的堆叠与动画平衡

要解决手牌重叠、弃牌堆分散的问题,需给弃牌堆每张卡牌设置递增的微小偏移,同时用SlideTransition控制从手牌位置到弃牌目标位置的动画。

完整实现示例:

import 'package:flutter/material.dart';

class CardGame extends StatefulWidget {
  @override
  _CardGameState createState() => _CardGameState();
}

class _CardGameState extends State<CardGame> with SingleTickerProviderStateMixin {
  final List<CardData> _handCards = [];
  final List<CardData> _discardPile = [];
  late AnimationController _animController;

  @override
  void initState() {
    super.initState();
    _animController = AnimationController(
      duration: const Duration(milliseconds: 500),
      vsync: this,
    );
    // 初始化测试卡牌
    _handCards.add(CardData(
      id: 'card_1',
      image: const AssetImage('assets/card.png'),
      startPos: const Offset(100, 400),
    ));
  }

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

  void _moveCardToDiscard(CardData card) {
    setState(() {
      _handCards.remove(card);
      // 计算弃牌堆目标位置:基准位置 + 索引偏移(每张错开5px形成堆叠)
      final targetPos = Offset(
        20 + _discardPile.length * 5,
        20 + _discardPile.length * 5,
      );
      card.targetPos = targetPos;
      _discardPile.add(card);
    });
    // 重启动画
    _animController.forward(from: 0);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: [
          // 渲染手牌区(固定位置)
          ..._handCards.map((card) {
            return Positioned(
              left: card.startPos.dx,
              top: card.startPos.dy,
              child: GestureDetector(
                onTap: () => _moveCardToDiscard(card),
                child: Image(image: card.image, width: 100, height: 150),
              ),
            );
          }),
          // 渲染弃牌堆(带移动动画)
          ..._discardPile.map((card) {
            final anim = Tween<Offset>(
              begin: card.startPos,
              end: card.targetPos,
            ).animate(_animController);
            return SlideTransition(
              position: anim,
              child: Image(image: card.image, width: 100, height: 150),
            );
          }),
        ],
      ),
    );
  }
}

// 卡牌数据模型
class CardData {
  final String id;
  final ImageProvider image;
  final Offset startPos;
  Offset? targetPos;

  CardData({
    required this.id,
    required this.image,
    required this.startPos,
  });
}

关键说明

  • 弃牌堆通过_discardPile.length * 5设置递增偏移,既避免完全重叠,又不会分散排列
  • SlideTransition使用Tween<Offset>定义动画起止位置,配合AnimationController控制动画流程
  • 手牌区用Positioned固定位置,点击后从手牌列表移除,加入弃牌堆并触发动画

内容的提问来源于stack exchange,提问作者Jhevi P.

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.02 23:47:12