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

相关推荐
漂流瓶jz4 小时前
总结CSS组件化演进之路:命名规范/CSS Modules/CSS in JS/原子化CSS
前端·javascript·css
踩着两条虫4 小时前
「AI + 低代码」的可视化设计器
开发语言·前端·低代码·设计模式·架构
Jagger_4 小时前
项目上线忙碌结束之后,为什么总想找点事做?
前端
GalenZhang8885 小时前
OpenClaw 配置多个飞书账号实战指南
前端·chrome·飞书·openclaw
萌新小码农‍6 小时前
python装饰器
开发语言·前端·python
threelab6 小时前
Three.js 初中数学函数可视化 | 三维可视化 / AI 提示词
开发语言·前端·javascript·人工智能·3d·着色器
爱学习的程序媛7 小时前
浏览器工作原理全景解析
前端·浏览器·web
我是若尘8 小时前
用 Git Worktree 同时开多个需求,不用来回 stash
前端
IT_陈寒8 小时前
Vue的v-for为什么不加key也能工作?我差点翻车
前端·人工智能·后端
小碗羊肉8 小时前
【JavaWeb | 第十二篇】项目实战——登录功能
java·前端·数据库