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

相关推荐
这儿有一堆花3 分钟前
深入解析 Video.js:现代 Web 视频播放的工程实践
前端·javascript·音视频
烤麻辣烫19 分钟前
JS基础
开发语言·前端·javascript·学习
IT_陈寒1 小时前
Vue的响应式把我坑惨了,原来问题出在这
前端·人工智能·后端
2603_953527991 小时前
WordPress Finale Lite 插件高危漏洞检测与利用工具 (CVE-2024-30485)
前端·python·安全·web3·xss
2601_949818091 小时前
头歌答案--爬虫实战
java·前端·爬虫
猫猫不是喵喵.2 小时前
layui表单项次大数据量导入并提交
前端·javascript·layui
张小潇2 小时前
AOSP15 WMS/AMS系统开发 - 窗口层级源码分析
android·前端
whuhewei2 小时前
HTTP1/2/3演变
前端·计算机网络
腹黑天蝎座2 小时前
从零实现一个前端监控系统:性能、错误与用户行为全方位监控
前端·监控
Hooray3 小时前
为了在 Vue 项目里用上想要的 React 组件,我写了这个 skill
前端·ai编程