当前位置: 首页 > news >正文

Flutter 自定义转场动画:PageRoute 与自定义路由的深度定制

Flutter 自定义转场动画:PageRoute 与自定义路由的深度定制

一、那个"千篇一律的从右滑入"让产品追了我一周

产品指着竞品 App 的转场效果问我:"为什么他们的商品详情页是从底部弹上来的,而我们的是从右边滑入?"答案很尴尬:因为 Flutter 的MaterialPageRoute默认转场就是"从右向左滑动",要改成"从下向上",得写PageRouteBuilder

更进一步的诉求:"弹上来的时候,背景要有个半透明遮罩,详情卡片的顶部两角要圆角,而且点击背景可以关闭详情页。"这些需求如果攒在业务代码里实现,每个页面都要复制一整套转场逻辑。

自定义转场动画的本质:将路由转场封装为可复用动画组件,让业务开发只需要"声明用什么动画"而不是"实现什么动画"。

二、PageRoute 的动画生命周期

sequenceDiagram participant Navigator as Navigator participant Route as PageRoute participant Animation as Animation<double> participant Widget as 转场 Widget Navigator->>Route: push(Route) Route->>Route: createAnimationController() Route->>Animation: controller.forward() Note over Route,Animation: transStatus = forward<br/>animation 从 0.0 到 1.0 loop 每帧 Animation->>Route: animation.value 更新 Route->>Widget: buildTransitions(context, animation) Note over Widget: 根据 animation.value<br/>插值计算 transform/opacity end Navigator->>Route: pop() Route->>Animation: controller.reverse() Note over Route,Animation: transStatus = reverse<br/>animation 从 1.0 到 0.0 Animation-->>Navigator: animation.status == dismissed Navigator->>Route: dispose()

三、自定义转场组件实现

// lib/routes/custom_page_route.dart // 自定义路由转场动画集合 import 'package:flutter/material.dart'; /// /// ======================================== /// 1. 从底部弹出(Bottom Sheet 风格) /// ======================================== /// /// 适用于:商品详情、操作菜单、筛选面板 /// /// 动画特征: /// - 卡片从底部滑入 /// - 背景叠加半透明遮罩 /// - 点击遮罩可关闭 /// - 顶部圆角仿物理卡片 class BottomSheetRoute<T> extends PageRouteBuilder<T> { final Widget child; /// [barrierDismissible] 是否点击遮罩关闭 /// [barrierColor] 遮罩颜色 BottomSheetRoute({ required this.child, bool barrierDismissible = true, Color barrierColor = Colors.black54, }) : super( // 遮罩层配置 barrierDismissible: barrierDismissible, barrierColor: barrierColor, opaque: false, // 关键:背景透明才能看到遮罩下方的页面 // // 过渡时长控制: // 弹出(forward)比关闭(reverse)略长,符合"进入要稳、退出要快"的直觉 transitionDuration: const Duration(milliseconds: 350), reverseTransitionDuration: const Duration(milliseconds: 250), pageBuilder: (context, animation, secondaryAnimation) => child, transitionsBuilder: (context, animation, secondaryAnimation, child) { // // 底部弹出的核心动画计算 // // animation.value: 0.0(完全隐藏)→ 1.0(完全显示) // 对于 pop(reverse):1.0 → 0.0 // return Stack( children: [ // 遮罩层动画:淡入淡出 FadeTransition( opacity: animation, child: GestureDetector( onTap: barrierDismissible ? () => Navigator.of(context).pop() : null, child: Container(color: Colors.transparent), ), ), // 卡片动画:从底部滑入 + 弹性缓动 Align( alignment: Alignment.bottomCenter, child: SlideTransition( position: Tween<Offset>( begin: const Offset(0, 1), // 屏幕下方 end: Offset.zero, // 屏幕底边对齐 ).animate(CurvedAnimation( parent: animation, // easeOutBack:先快后慢 + 轻微超出目标位置再回弹 // 模拟物理上的"弹入"感 curve: Curves.easeOutBack, reverseCurve: Curves.easeIn, // 退出时无回弹,直接下滑 )), child: SafeArea( child: ClipRRect( // 顶部圆角:只在卡片上边加圆角,下边不留 borderRadius: const BorderRadius.vertical( top: Radius.circular(16), ), child: child, ), ), ), ), ], ); }, ); } /// /// ======================================== /// 2. 从中心缩放弹出(iOS 风格 Alert) /// ======================================== /// /// 适用于:确认弹窗、登录模态框 /// /// 动画特征: /// - 从 0.8 缩放至 1.0 + 透明度 0→1 /// - 背景模糊而非纯黑遮罩 class ScaleFadeRoute<T> extends PageRouteBuilder<T> { final Widget child; ScaleFadeRoute({ required this.child, bool barrierDismissible = true, }) : super( barrierDismissible: barrierDismissible, barrierColor: Colors.black54, opaque: false, transitionDuration: const Duration(milliseconds: 300), pageBuilder: (context, animation, secondaryAnimation) => child, transitionsBuilder: (context, animation, secondaryAnimation, child) { return FadeTransition( opacity: animation, child: ScaleTransition( // 组合动画:scale 从 0.8→1.0,opacity 从 0→1 scale: Tween<double>(begin: 0.8, end: 1.0).animate( CurvedAnimation( parent: animation, curve: Curves.easeOutBack, ), ), child: Center( child: child, ), ), ); }, ); } /// /// ======================================== /// 3. 共享元素过渡封装(Hero 动画增强) /// ======================================== /// /// Flutter 内置的 Hero 动画负责"一个 Widget 在两个页面间的连续过渡", /// 但默认的矩形裁剪过于生硬。这里做圆角 + 弹性缓动增强。 /// class SharedAxisRoute<T> extends PageRouteBuilder<T> { final Widget child; final Axis direction; // 滑动方向 SharedAxisRoute({ required this.child, this.direction = Axis.horizontal, }) : super( transitionDuration: const Duration(milliseconds: 350), reverseTransitionDuration: const Duration(milliseconds: 250), pageBuilder: (context, animation, secondaryAnimation) => child, transitionsBuilder: (context, animation, secondaryAnimation, child) { final isHorizontal = direction == Axis.horizontal; return SlideTransition( position: Tween<Offset>( begin: isHorizontal ? const Offset(0.3, 0) // 从右 30% 处滑入(留一点让旧页面可见) : const Offset(0, 0.3), // 从下方 30% 处滑入 end: Offset.zero, ).animate(CurvedAnimation( parent: animation, curve: Curves.easeOut, reverseCurve: Curves.easeIn, )), child: FadeTransition( opacity: Tween<double>(begin: 0.0, end: 1.0).animate(animation), child: child, ), ); }, ); } /// /// ======================================== /// 使用示例 /// ======================================== /// class ExampleUsage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('转场示例')), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // 1. 底部弹出详情页 ElevatedButton( onPressed: () { Navigator.of(context).push( BottomSheetRoute( child: ProductDetailPage(), ), ); }, child: const Text('从底部弹出商品详情'), ), // 2. 缩放弹出确认框 ElevatedButton( onPressed: () { Navigator.of(context).push( ScaleFadeRoute( child: Material( color: Colors.transparent, child: ConfirmDialog(), ), ), ); }, child: const Text('缩放弹出确认框'), ), ], ), ), ); } }

四、自定义转场的性能与边界

opaque: false的性能代价。当路由不透明(opaque: true,默认值)时,Flutter 知道下方的页面完全被遮挡,不会继续渲染它——这是一种 GPU 层级优化。设为false后,下方的页面持续渲染,在复杂页面(如包含 WebView、地图、大量动画)下会有明显性能影响。

Hero和自定义路由的兼容性。当自定义PageRoute中既有SlideTransition又有页面内的Hero动画时,两个动画系统的时间线可能不同步——Hero有自己独立的AnimationController,而自定义路由使用PageRouteanimation。解决方案是使用secondaryAnimation参数:它代表"当前路由被压入新页面下方时"的动画,传递给HeroflightShuttleBuilder来同步时间线。

过度定制的问题PageRouteBuilder的灵活性容易让人在每个页面都写一套转场逻辑。这恰恰是要避免的:用 3-5 个标准转场封装(底部弹出、缩放弹出、共享轴滑动),业务代码只选择不实现。

五、总结

Flutter 自定义转场动画的核心是PageRouteBuilder的两个关键参数:

  1. transitionDuration+reverseTransitionDuration——正向和反向可以有不同时长
  2. transitionsBuilder(context, animation, secondaryAnimation, child)——用 animation 的 0→1 值驱动所有转场 Widget 的插值变化
  3. opaque: false——背景透明的路由(BottomSheet、Modal 场景)

"从哪里进来、怎么进来、背景是什么、怎么出去"——这四个维度定义一个转场,封装到可复用的 Route 类中,全项目统一使用。不要让每个页面都在"手写转场动画"。

http://www.jsqmd.com/news/1216145/

相关文章:

  • 合肥投影仪哪家维修最专业?差点被坑八百块后我只认这家
  • 带孩子自驾游选什么大型SUV好:沃尔沃XC90、奔驰GLE与蔚来ES8家庭出行对比 - 信息情报站
  • GPIO入门
  • MySQL 索引使用规范:常见失效场景与优化实操方案
  • 操控基因谁更强:沃尔沃XC70、理想L7、传祺ES9动态质感深度对比 - 信息情报站
  • 即梦AI生成的视频怎么去水印?2026大马工具箱+快快无印免费工具实测教程 - 科技大爆炸
  • 【工具汇总】鸿蒙 ArkWeb完整调试工具步骤
  • Go 服务自动扩缩容:HPA 指标设计和冷启动优化
  • 20万中型家用轿车推荐:沃尔沃S60、奔驰C级、宝马3系安全与油耗对比 - 信息情报站
  • 低成本创业科普:为什么全媒体广告代理是可关注的轻资产赛道
  • 家庭用车纯电轿车怎么选:沃尔沃ES90、蔚来ET7与宝马i5空间安全与实用性对比 - 信息情报站
  • FW-D1 vs 迈来芯MLX90614:参数全面对比
  • Agent 任务队列架构:优先级、死信队列和状态追踪
  • 论文“双降”攻略,只需三步,有效规避AI检测,使用ChatGPT降AI率和降重一起搞定
  • 抖音实况图怎么保存到手机相册?2026大马工具箱+快快无印动态保存教程 - 科技大爆炸
  • 宝珀官方售后服务中心地址与客户服务热线实地考察报告+多信源验证(2026年7月最新) - 宝珀官方售后服务中心
  • 过孔越密越稳?信号穿过一片“孔墙”,参考平面可能先被割碎
  • 打工人必看!一个网站=GPT+Claude+MJ,做PPT/写周报效率翻倍
  • 学术写作中的 RAG 技术应用:如何解决 AI 生成内容的文献引用问题
  • 视频号视频怎么无水印下载保存?2026大马工具箱微信视频号方法全解析 - 科技大爆炸
  • 寄大件什么快递最快最便宜?2026年主流物流深度横评与省钱指南 - 快递物流资讯
  • 序说完整(第4期) | 陈为凯研究员详解T2T基因组解析园艺作物着丝粒演化研究思路
  • 踩坑记录:TDengine 落地钢铁设备预测性维护的时序建模记录
  • 计算机毕业设计YOLO+多模态大模型(LLM)水果蔬菜病害检测分析预警系统 智慧农业 深度学习 人工智能(源码+文档+ppt+讲解)
  • 亲身探访上海百达翡丽官方售后服务中心|地址与24小时客服电话(2026年7月最新) - 百达翡丽官方售后中心
  • 女生适合开的混动SUV:沃尔沃XC70对比魏牌蓝山与传祺ES9 - 信息情报站
  • 如果关注M4Markets信息透明度,是否自然?
  • 法式芝士月饼怎么做?铝箔杯配方、入模与烘烤步骤
  • 如何5分钟完成Steam Deck多系统引导配置:可视化工具部署指南
  • 2026北京黄金回收,投资金条持有3年想出手?毓典奢品汇专业收金条,手续简单打款快 - 旧奢新值