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

相关推荐
红尘散仙21 分钟前
谁说终端只能敲键盘?我用 Rust 给小说阅读器加上了鼠标
前端·后端·rust
陈随易2 小时前
C++26 草案被否,二十八年来头一回
前端·后端·程序员
ZhengEnCi2 小时前
Agent 工具怎么折叠海量输出才「爽」:Trae、Claude Code 的渐进式披露设计原理
前端·agent
IT_陈寒5 小时前
Redis内存暴涨,没想到是AOF重写捣的鬼
前端·人工智能·后端
ikoala5 小时前
让 Codex 自己查账:一个省 token 的小技巧
前端·后端·ai编程
ikoala5 小时前
AI 写页面像说明书,修 Bug 像贴膏药?把这两句话写进 AGENTS.md
前端·javascript·ai编程
好的佩奇6 小时前
Dart 之扩展方法
android·flutter·dart
去伪存真7 小时前
AI Agent Harness 深度测评与技术选型实战
前端·agent
好的佩奇8 小时前
蓝牙之经典蓝牙
android·flutter·蓝牙
FRing0310 小时前
vue2干货总结7(自定义指令、插槽、单页应用程序、路由的使用(VueRouter))
开发语言·前端·vue.js·vue2