如何在Flutter中实现如图所示的纸张滑动翻页动画?
实现纸张翻折动画方案
你要的这种局部翻页露出下一页的效果,用PageView确实做不到——因为PageView是整页切换逻辑,而这种效果属于自定义的局部翻折动画,需要通过AnimationController结合Transform、ClipPath来实现,下面是具体方案:
核心思路
- 用
Stack堆叠当前页和下一页,下一页在底层,当前页在上层执行翻折动画 - 通过
AnimationController控制翻折角度、裁剪范围,模拟纸张翻折形态 - 自定义
CustomClipper裁剪当前页的翻折区域,配合旋转变换实现翻折视觉效果 - 加入阴影和页面反向显示,增强翻折的真实感
完整代码示例
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
相关产品推荐
相关产品推荐

