Flutter中心圆形展开式页面过渡效果实现求助
Flutter实现中心圆形展开的页面过渡效果
下面提供两种满足需求的实现方案,分别对应你提到的两种思路:
方案一:通过裁剪第一个页面实现圆形展开过渡
这种方式直接对第一个页面进行圆形裁剪,随着裁剪区域从中心向外扩大,逐步显示底层的第二个页面。如果需要用自定义Widget替代中心圆形区域,也可以在过渡层添加对应组件并配合动画调整显示状态。
实现代码
import 'package:flutter/material.dart'; import 'dart:math'; class CircleExpandTransition extends StatefulWidget { final Widget firstPage; final Widget secondPage; final AnimationController controller; const CircleExpandTransition({ super.key, required this.firstPage, required this.secondPage, required this.controller, }); @override State<CircleExpandTransition> createState() => _CircleExpandTransitionState(); } class _CircleExpandTransitionState extends State<CircleExpandTransition> { @override Widget build(BuildContext context) { return Stack( children: [ // 底层是目标页面 widget.secondPage, // 上层是被裁剪的初始页面 AnimatedBuilder( animation: widget.controller, builder: (context, child) { return ClipPath( clipper: _CircleExpandClipper( progress: widget.controller.value, screenSize: MediaQuery.of(context).size, ), child: child, ); }, child: widget.firstPage, ), // 可选:添加中心自定义Widget,随过渡动画隐藏/缩放 AnimatedBuilder( animation: widget.controller, builder: (context, child) { return Opacity( opacity: 1 - widget.controller.value, child: Transform.scale( scale: 1 + widget.controller.value * 1.5, child: child, ), ); }, child: const Center( child: Icon(Icons.navigate_next, size: 60, color: Colors.white), ), ), ], ); } } class _CircleExpandClipper extends CustomClipper<Path> { final double progress; final Size screenSize; _CircleExpandClipper({required this.progress, required this.screenSize}); @override Path getClip(Size size) { final center = Offset(screenSize.width / 2, screenSize.height / 2); final maxRadius = sqrt(screenSize.width * screenSize.width + screenSize.height * screenSize.height) / 2; final currentRadius = maxRadius * progress; // 绘制反向路径:先填充整个屏幕,再减去圆形区域 final path = Path() ..addRect(Rect.fromLTWH(0, 0, screenSize.width, screenSize.height)) ..addOval(Rect.fromCircle(center: center, radius: currentRadius)) ..fillType = PathFillType.evenOdd; return path; } @override bool shouldReclip(covariant _CircleExpandClipper oldClipper) { return progress != oldClipper.progress; } }
使用示例
class TransitionDemo extends StatefulWidget { const TransitionDemo({super.key}); @override State<TransitionDemo> createState() => _TransitionDemoState(); } class _TransitionDemoState extends State<TransitionDemo> with SingleTickerProviderStateMixin { late AnimationController _controller; bool _isSecondPageShown = false; @override void initState() { super.initState(); _controller = AnimationController( vsync: this, duration: const Duration(milliseconds: 800), ); } @override void dispose() { _controller.dispose(); super.dispose(); } void _toggleTransition() { setState(() => _isSecondPageShown = !_isSecondPageShown); _isSecondPageShown ? _controller.forward() : _controller.reverse(); } @override Widget build(BuildContext context) { return Scaffold( body: CircleExpandTransition( firstPage: Scaffold( backgroundColor: Colors.deepPurple, body: Center( child: ElevatedButton( onPressed: _toggleTransition, child: const Text('展开到第二个页面', style: TextStyle(fontSize: 18)), ), ), ), secondPage: Scaffold( backgroundColor: Colors.teal, body: Center( child: ElevatedButton( onPressed: _toggleTransition, child: const Text('返回第一个页面', style: TextStyle(fontSize: 18)), ), ), ), controller: _controller, ), ); } }
方案二:带透明孔洞的遮罩动画
这种方式通过在第一个页面上方绘制带透明孔洞的遮罩,随着孔洞从中心向外扩大,逐步显示下方的第二个页面,适合需要保留第一个页面全部内容,仅通过遮罩控制显示区域的场景。
实现代码
import 'package:flutter/material.dart'; import 'dart:math'; class HoleExpandTransition extends StatefulWidget { final Widget backgroundPage; final Widget foregroundPage; final AnimationController controller; final Color maskColor; const HoleExpandTransition({ super.key, required this.backgroundPage, required this.foregroundPage, required this.controller, required this.maskColor, }); @override State<HoleExpandTransition> createState() => _HoleExpandTransitionState(); } class _HoleExpandTransitionState extends State<HoleExpandTransition> { @override Widget build(BuildContext context) { return Stack( children: [ // 底层目标页面 widget.backgroundPage, // 上层初始页面+遮罩 Stack( children: [ widget.foregroundPage, AnimatedBuilder( animation: widget.controller, builder: (context, _) { return CustomPaint( size: MediaQuery.of(context).size, painter: _HoleMaskPainter( progress: widget.controller.value, screenSize: MediaQuery.of(context).size, maskColor: widget.maskColor, ), ); }, ), ], ), ], ); } } class _HoleMaskPainter extends CustomPainter { final double progress; final Size screenSize; final Color maskColor; _HoleMaskPainter({ required this.progress, required this.screenSize, required this.maskColor, }); @override void paint(Canvas canvas, Size size) { final maskPaint = Paint()..color = maskColor; final clearPaint = Paint()..blendMode = BlendMode.clear; // 绘制全屏遮罩 canvas.drawRect(Rect.fromLTWH(0, 0, screenSize.width, screenSize.height), maskPaint); // 绘制透明孔洞 final center = Offset(screenSize.width / 2, screenSize.height / 2); final maxRadius = sqrt(screenSize.width * screenSize.width + screenSize.height * screenSize.height) / 2; final currentRadius = maxRadius * progress; canvas.drawCircle(center, currentRadius, clearPaint); } @override bool shouldRepaint(covariant _HoleMaskPainter oldDelegate) { return progress != oldDelegate.progress || maskColor != oldDelegate.maskColor; } }
使用示例
只需将CircleExpandTransition替换为HoleExpandTransition,并传入遮罩颜色(与第一个页面背景色一致):
HoleExpandTransition( backgroundPage: Scaffold(backgroundColor: Colors.orange, ...), foregroundPage: Scaffold(backgroundColor: Colors.blue, ...), controller: _controller, maskColor: Colors.blue, )
内容的提问来源于stack exchange,提问作者Giacomo M
相关产品推荐
相关产品推荐

