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

如何在Flutter中实现如图所示的纸张滑动翻页动画?

实现纸张翻折动画方案

你要的这种局部翻页露出下一页的效果,用PageView确实做不到——因为PageView是整页切换逻辑,而这种效果属于自定义的局部翻折动画,需要通过AnimationController结合Transform、ClipPath来实现,下面是具体方案:

核心思路

  1. 用Stack堆叠当前页和下一页,下一页在底层,当前页在上层执行翻折动画
  2. 通过AnimationController控制翻折角度、裁剪范围,模拟纸张翻折形态
  3. 自定义CustomClipper裁剪当前页的翻折区域,配合旋转变换实现翻折视觉效果
  4. 加入阴影和页面反向显示,增强翻折的真实感

完整代码示例

import 'package:flutter/material.dart';
import 'dart:math' as Math;

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        backgroundColor: Colors.grey[200],
        body: const PaperFlipDemo(),
      ),
    );
  }
}

class PaperFlipDemo extends StatefulWidget {
  const PaperFlipDemo({super.key});

  @override
  State<PaperFlipDemo> createState() => _PaperFlipDemoState();
}

class _PaperFlipDemoState extends State<PaperFlipDemo> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _flipAnimation;
  bool _isFlipped = false;

  // 模拟页面内容
  final List<Widget> _pages = [
    Container(color: Colors.white, child: const Center(child: Text("页面 1", style: TextStyle(fontSize: 24)))),
    Container(color: Colors.white, child: const Center(child: Text("页面 2", style: TextStyle(fontSize: 24)))),
  ];

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(milliseconds: 800),
      vsync: this,
    );
    _flipAnimation = Tween<double>(begin: 0, end: 1).animate(CurvedAnimation(
      parent: _controller,
      curve: Curves.easeInOut,
    ));
  }

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

  void _toggleFlip() {
    _isFlipped ? _controller.reverse() : _controller.forward();
    setState(() => _isFlipped = !_isFlipped);
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Expanded(
          child: Center(
            child: SizedBox(
              width: 300,
              height: 400,
              child: Stack(
                children: [
                  // 底层:下一页
                  _pages[_isFlipped ? 0 : 1],
                  // 上层:当前页,执行翻折动画
                  AnimatedBuilder(
                    animation: _flipAnimation,
                    builder: (context, child) {
                      final progress = _flipAnimation.value;
                      final flipAngle = progress * (Math.pi / 2);
                      final flipHeightRatio = 1 - progress;

                      return Stack(
                        children: [
                          // 未翻折的主体部分
                          ClipPath(
                            clipper: PaperFlipClipper(flipHeightRatio),
                            child: child,
                          ),
                          // 翻折的边角部分(带3D旋转和阴影)
                          if (progress > 0)
                            Transform(
                              alignment: Alignment.bottomCenter,
                              transform: Matrix4.identity()
                                ..setEntry(3, 2, 0.001) // 添加透视效果
                                ..rotateX(flipAngle),
                              child: ClipPath(
                                clipper: PaperFlipClipper(progress),
                                child: Container(
                                  color: Colors.white,
                                  child: Stack(
                                    children: [
                                      // 翻折部分反向显示下一页内容
                                      Transform(
                                        alignment: Alignment.bottomCenter,
                                        transform: Matrix4.identity()..rotateX(Math.pi),
                                        child: _pages[_isFlipped ? 0 : 1],
                                      ),
                                      // 翻折阴影,增强真实感
                                      Container(
                                        color: Colors.black.withOpacity(progress * 0.3),
                                      ),
                                    ],
                                  ),
                                ),
                              ),
                            ),
                        ],
                      );
                    },
                    child: _pages[_isFlipped ? 1 : 0],
                  ),
                ],
              ),
            ),
          ),
        ),
        // 底部触发按钮
        Padding(
          padding: const EdgeInsets.only(bottom: 40),
          child: ElevatedButton(
            onPressed: _toggleFlip,
            child: Text(_isFlipped ? "翻回去" : "翻页"),
          ),
        ),
      ],
    );
  }
}

// 自定义裁剪器:根据进度裁剪出纸张的翻折/未翻折区域
class PaperFlipClipper extends CustomClipper<Path> {
  final double progress;

  PaperFlipClipper(this.progress);

  @override
  Path getClip(Size size) {
    final path = Path();
    path.moveTo(0, 0);
    path.lineTo(size.width, 0);
    path.lineTo(size.width, size.height * progress);
    path.lineTo(0, size.height * progress);
    path.close();
    return path;
  }

  @override
  bool shouldReclip(covariant PaperFlipClipper oldClipper) {
    return oldClipper.progress != progress;
  }
}

关键细节说明

  • PaperFlipClipper:根据动画进度裁剪页面,区分未翻折主体和翻折边角
  • Transform.rotateX:实现3D翻转动画,配合透视参数让翻折效果更立体
  • 翻折部分反向显示下一页内容,并叠加渐变阴影,模拟纸张翻折后的真实视觉
  • 通过CurvedAnimation调整动画曲线,让翻折动作更流畅自然

内容的提问来源于stack exchange,提问作者Farid Ahmadov

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 22:58:13