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

Flutter无依赖实现卡片翻转动画 求技术支持

实现Flutter卡片3D翻转动画(无第三方依赖)

要实现卡片正反面的翻转动画,我们可以利用Flutter自带的AnimationController和Transform组件实现3D旋转效果,无需第三方包。以下是修改后的完整代码:

import 'package:flutter/material.dart';

class NotificationItemCard extends StatefulWidget {
  const NotificationItemCard({
    Key? key,
  }) : super(key: key);

  @override
  State<NotificationItemCard> createState() => _NotificationItemCardState();
}

class _NotificationItemCardState extends State<NotificationItemCard> with SingleTickerProviderStateMixin {
  late bool showCardFrontSide;
  late AnimationController _animationController;
  late Animation<double> _rotationAnimation;

  @override
  void initState() {
    showCardFrontSide = true;
    // 初始化动画控制器,时长300毫秒可按需调整
    _animationController = AnimationController(
      duration: const Duration(milliseconds: 300),
      vsync: this,
    );
    // 创建旋转动画,从0到π(对应180度旋转)
    _rotationAnimation = Tween<double>(begin: 0, end: 1).animate(CurvedAnimation(
      parent: _animationController,
      curve: Curves.easeInOut,
    ));
    super.initState();
  }

  void onChangeView() {
    if (_animationController.status == AnimationStatus.completed) {
      _animationController.reverse();
    } else if (_animationController.status == AnimationStatus.dismissed) {
      _animationController.forward();
    }
    // 动画执行到一半时切换正反面(旋转90度时)
    _animationController.addStatusListener((status) {
      if (status == AnimationStatus.halfCompleted) {
        setState(() {
          showCardFrontSide = !showCardFrontSide;
        });
      }
    });
  }

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

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        AnimatedBuilder(
          animation: _rotationAnimation,
          builder: (context, child) {
            final angle = _rotationAnimation.value * 3.14159; // π弧度对应180度
            return Transform(
              alignment: Alignment.center,
              transform: Matrix4.identity()
                ..setEntry(3, 2, 0.001) // 添加透视效果,增强3D质感
                ..rotateY(angle),
              child: Container(
                height: 140.h,
                decoration: BoxDecoration(
                  border: Border.all(color: Colors.black),
                  borderRadius: BorderRadius.circular(8),
                ),
                child: showCardFrontSide
                    ? const NotificationCardFrontSide()
                    : NotificationCardBackSide(
                        onChangeView: onChangeView,
                      ),
              ),
            );
          },
        ),
        showCardFrontSide
            ? Align(
                alignment: const Alignment(0.95, -1),
                child: IconButton(
                  key: const ValueKey("IconButton"),
                  onPressed: onChangeView,
                  icon: const Icon(Icons.info_outline),
                ),
              )
            : const SizedBox.shrink()
      ],
    );
  }
}

class NotificationCardFrontSide extends StatelessWidget {
  const NotificationCardFrontSide({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        SizedBox(
          key: const ValueKey("FrontSideSizedBox"),
          width: 126.w,
          child: Center(
            child: CircleAvatar(
              radius: 50.r,
            ),
          ),
        ),
        SizedBox(
          key: const ValueKey("FrontSideSizedTextBox"),
          width: 222.w,
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text(
                "Premium Private LOBBY",
                style: Theme.of(context).textTheme.headlineMedium?.copyWith(overflow: TextOverflow.ellipsis),
                key: const ValueKey("FrontSideSizedTextBox1"),
              ),
              Text(
                "Prediction Deadline",
                style: Theme.of(context).textTheme.headlineMedium?.copyWith(overflow: TextOverflow.ellipsis),
                key: const ValueKey("FrontSideSizedTextBox2"),
              ),
              Text(
                "Premium Private LOBBY",
                style: Theme.of(context).textTheme.headlineMedium?.copyWith(overflow: TextOverflow.ellipsis),
                key: const ValueKey("FrontSideSizedTextBox3"),
              ),
              SizedBox(
                key: const ValueKey("FrontSideSizedButtonBox"),
                width: 150.w,
                height: 45.h,
                child: Row(
                  mainAxisAlignment: MainAxisAlignment.spaceAround,
                  children: [
                    SizedBox(
                      key: const ValueKey("FrontSideButtonSizedBox"),
                      width: 70.w,
                      child: TextButton(
                        onPressed: () {},
                        child: Text(
                          "deny",
                          style: Theme.of(context).textTheme.bodyMedium,
                        ),
                      ),
                    ),
                    SizedBox(
                      width: 70.w,
                      child: TextButton(
                        onPressed: () {},
                        child: Text(
                          "deny",
                          style: Theme.of(context).textTheme.bodyMedium,
                        ),
                      ),
                    ),
                  ],
                ),
              ),
            ],
          ),
        ),
      ],
    );
  }
}

class NotificationCardBackSide extends StatelessWidget {
  final VoidCallback onChangeView;

  const NotificationCardBackSide({
    Key? key,
    required this.onChangeView,
  }) : super(key: key);

  Widget getTeamLogo(String image) {
    return CircleAvatar(
      backgroundColor: const Color(0xFFD9D9D9),
      radius: 30.r,
      child: Image.network(
        image,
        errorBuilder: (context, error, stackTrace) {
          return Text(
            "Error",
            style: Theme.of(context).textTheme.displayMedium?.copyWith(
                  color: Colors.red,
                ),
          );
        },
        height: 65.h,
        width: 65.w,
        loadingBuilder: (context, child, loadingProgress) {
          if (loadingProgress == null) return child;
          return Center(
            child: Text(
              "Loading...",
              style: Theme.of(context).textTheme.displayMedium,
            ),
          );
        },
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      children: [
        SizedBox(
          height: 62.h,
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceAround,
            children: [
              getTeamLogo(""),
              Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text(
                    "Premium Private LOBBY",
                    style: Theme.of(context).textTheme.headlineMedium?.copyWith(overflow: TextOverflow.clip),
                    key: const ValueKey("BackSideSizedText1"),
                  ),
                  Text(
                    "Prediction Deadline",
                    style: Theme.of(context).textTheme.headlineMedium?.copyWith(overflow: TextOverflow.clip),
                    key: const ValueKey("BackSideSizedText2"),
                  ),
                ],
              ),
              getTeamLogo(""),
            ],
          ),
        ),
        SizedBox(
          key: const ValueKey("BackSideButtonBox"),
          height: 30.h,
          width: 100.w,
          child: OutlinedButton(
            onPressed: onChangeView,
            child: const Text("Go Back"),
            key: const ValueKey("BackSideButtonText"),
            style: ButtonStyle(
              shape: MaterialStateProperty.all(
                RoundedRectangleBorder(
                  borderRadius: BorderRadius.circular(
                    8.r,
                  ),
                ),
              ),
            ),
          ),
        )
      ],
    );
  }
}

核心实现要点:

  1. 动画基础配置

    • 混入SingleTickerProviderStateMixin为动画提供帧同步支持
    • 用AnimationController控制动画时长,CurvedAnimation添加缓动曲线让旋转更自然
  2. 3D旋转效果

    • 通过Matrix4添加透视参数(setEntry(3,2,0.001)),模拟真实3D空间
    • 使用rotateY(angle)实现绕Y轴的旋转,达到卡片翻转的视觉效果
  3. 正反面切换逻辑

    • 在动画执行到一半(旋转90度)时切换卡片显示状态,避免正反面同时出现
    • 通过forward()和reverse()控制动画的正向/反向播放,实现来回翻转
  4. 资源管理

    • 在dispose()方法中释放AnimationController,防止内存泄漏

内容的提问来源于stack exchange,提问作者Chaitya Vohera

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.12 16:05:15