Flutter组件封装:翻转组件 NFlipCard

一、需求来源

最近研究 Transition 系列动画,随手实现一个 iOS中支持的翻转动画,效果如下:

二、使用示例

dart 复制代码
NFlipCard(
  fontBuilder: (onToggle) {
    return GestureDetector(
      onTap: onToggle,
      child: Container(
        decoration: BoxDecoration(
          color: Colors.transparent,
          border: Border.all(color: Colors.yellow),
          borderRadius: BorderRadius.all(Radius.circular(0)),
        ),
        child: Image(
          image: AssetImage(Assets.imagesBgMk11),
          width: 300,
          height: 400,
          fit: BoxFit.contain,
        ),
      ),
    );
  },
  backBuilder: (onToggle) {
    return GestureDetector(
      onTap: onToggle,
      child: Container(
        decoration: BoxDecoration(
          color: Colors.transparent,
          border: Border.all(color: Colors.grey),
          borderRadius: BorderRadius.all(Radius.circular(0)),
        ),
        child: Image(
          image: AssetImage(Assets.imagesBgNfs),
          width: 380,
          height: 300,
          fit: BoxFit.contain,
        ),
      ),
    );
  },
),

三、源码 NFlipCard

dart 复制代码
//
//  NFlipCard.dart
//  flutter_templet_project
//
//  Created by shang on 2026/3/25 12:06.
//  Copyright © 2026/3/25 shang. All rights reserved.
//

import 'dart:math' as math;

import 'package:flutter/material.dart';

/// 翻转组件
class NFlipCard extends StatefulWidget {
  const NFlipCard({
    super.key,
    this.axis = Axis.vertical,
    this.fontBuilder,
    this.backBuilder,
  });

  /// 翻转方向
  final Axis axis;
  final Widget Function(VoidCallback onToggle)? fontBuilder;
  final Widget Function(VoidCallback onToggle)? backBuilder;

  @override
  State<NFlipCard> createState() => _NFlipCardState();
}

class _NFlipCardState extends State<NFlipCard> {
  bool _flipped = false;

  void toggle() {
    _flipped = !_flipped;
    setState(() {});
  }

  @override
  void didUpdateWidget(covariant NFlipCard oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.axis != widget.axis ||
        oldWidget.fontBuilder?.call(toggle) != widget.fontBuilder?.call(toggle) ||
        oldWidget.backBuilder?.call(toggle) != widget.backBuilder?.call(toggle)) {
      setState(() {});
    }
  }

  @override
  Widget build(BuildContext context) {
    return TweenAnimationBuilder<double>(
      tween: Tween(begin: 0, end: _flipped ? 1 : 0),
      duration: const Duration(milliseconds: 500),
      builder: (context, value, child) {
        final angle = value * math.pi; // 0 → π
        final isBack = angle > math.pi / 2;

        final transformFront = Matrix4.identity()..setEntry(3, 2, 0.001) // 🔥 透视
            ;
        final transformBack = Matrix4.identity();

        if (widget.axis == Axis.horizontal) {
          transformFront.rotateY(angle);
          transformBack.rotateY(math.pi);
        } else {
          transformFront.rotateX(angle);
          transformBack.rotateX(math.pi);
        }

        return Transform(
          alignment: Alignment.center,
          transform: transformFront,
          child: isBack
              ? Transform(
                  alignment: Alignment.center,
                  transform: transformBack,
                  child: buildBack(),
                )
              : buildFront(),
        );
      },
    );
  }

  Widget buildFront() {
    return widget.fontBuilder?.call(toggle) ??
        _card(
          width: 200,
          height: 100,
          color: Colors.blue,
          text: "Front",
        );
  }

  Widget buildBack() {
    return widget.backBuilder?.call(toggle) ??
        _card(
          width: 100,
          height: 200,
          color: Colors.red,
          text: "Back",
        );
  }

  Widget _card({
    double? width,
    double? height,
    required Color color,
    required String text,
  }) {
    return Container(
      width: width,
      height: height,
      alignment: Alignment.center,
      decoration: BoxDecoration(
        color: color,
        borderRadius: BorderRadius.circular(12),
      ),
      child: Text(
        text,
        style: const TextStyle(color: Colors.white, fontSize: 20),
      ),
    );
  }
}

最后、总结

核心是使用 Matrix4 方法 rotateY 实现 Y 轴旋转。

github

相关推荐
拖孩2 小时前
AI 帮我写的小程序「厨菜记」上线满月了,挣了两百块!!!
前端·后端·微信小程序
用户360055579002 小时前
Kestrel_VLLM - 13 · 为什么排除 MoE FFN Tiling 与 Tensor Core
前端
Hilaku3 小时前
AI 流式输出都用 SSE,什么时候必须换 WebSocket?
前端·javascript·程序员
奇牙coding3 小时前
Codex CLI 升级后多文件编辑静默失效:config.yaml 枚举变更排查与修复指南
java·前端·javascript·ai
小羊没烦恼!3 小时前
Ajax与JSON的一些总结
android·java·前端·数据库·mysql·算法·okhttp
程序员老赵3 小时前
Docker 部署 Filestash:轻松搭建浏览器文件管理平台
前端·docker·开源
夏天要喝冰可乐3 小时前
给 23 万字修仙小说造个阅读站:浏览器内打包 EPUB 分页阅读 + 三主题实战
前端·产品·全栈
茶底世界之下3 小时前
林间步道要不要加暗角?用 Awaking 把强度收在20%
前端·ios·图片资源
执子念的飞鱼3 小时前
纯前端预览 275 种文件格式是怎么做到的
前端·vue.js·react.js
行者全栈架构师3 小时前
【鸿蒙心迹】互动卡片全链路——把核心功能放到桌面(FormKit 实战)(HarmonyOS 7.x)
前端·算法·架构