简介
路由动画可以让页面切换更加流畅自然。Flutter 提供了丰富的 API 来自定义路由转场动画。
基本动画
内置转场动画
// 淡入淡出
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => SecondPage(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(
opacity: animation,
child: child,
);
},
),
);
// 滑动
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => SecondPage(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(1.0, 0.0),
end: Offset.zero,
).animate(animation),
child: child,
);
},
),
);
自定义路由
class CustomPageRoute extends PageRouteBuilder {
final Widget page;
CustomPageRoute({required this.page})
: super(
pageBuilder: (context, animation, secondaryAnimation) => page,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
var begin = 0.0;
var end = 1.0;
var curve = Curves.ease;
var tween = Tween(begin: begin, end: end)
.chain(CurveTween(curve: curve));
return ScaleTransition(
scale: animation.drive(tween),
child: child,
);
},
);
}
// 使用
Navigator.push(
context,
CustomPageRoute(page: SecondPage()),
);
复杂动画
组合动画
class CompositePageRoute extends PageRouteBuilder {
final Widget page;
CompositePageRoute({required this.page})
: super(
pageBuilder: (context, animation, secondaryAnimation) => page,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
var fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(
CurvedAnimation(
parent: animation,
curve: Interval(0.0, 0.5),
),
);
var slideAnimation = Tween<Offset>(
begin: const Offset(0.0, 1.0),
end: Offset.zero,
).animate(
CurvedAnimation(
parent: animation,
curve: Interval(0.5, 1.0),
),
);
return FadeTransition(
opacity: fadeAnimation,
child: SlideTransition(
position: slideAnimation,
child: child,
),
);
},
);
}
完整示例
class RouteAnimationDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Route Animations')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () => _showFadeRoute(context),
child: Text('Fade Route'),
),
ElevatedButton(
onPressed: () => _showSlideRoute(context),
child: Text('Slide Route'),
),
ElevatedButton(
onPressed: () => _showScaleRoute(context),
child: Text('Scale Route'),
),
ElevatedButton(
onPressed: () => _showCompositeRoute(context),
child: Text('Composite Route'),
),
],
),
),
);
}
void _showFadeRoute(BuildContext context) {
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) =>
DetailPage(title: 'Fade Route'),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(
opacity: animation,
child: child,
);
},
),
);
}
void _showSlideRoute(BuildContext context) {
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) =>
DetailPage(title: 'Slide Route'),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(1.0, 0.0),
end: Offset.zero,
).animate(
CurvedAnimation(
parent: animation,
curve: Curves.easeInOut,
),
),
child: child,
);
},
),
);
}
void _showScaleRoute(BuildContext context) {
Navigator.push(
context,
CustomPageRoute(
page: DetailPage(title: 'Scale Route'),
),
);
}
void _showCompositeRoute(BuildContext context) {
Navigator.push(
context,
CompositePageRoute(
page: DetailPage(title: 'Composite Route'),
),
);
}
}
class DetailPage extends StatelessWidget {
final String title;
const DetailPage({
Key? key,
required this.title,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(title)),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
title,
style: Theme.of(context).textTheme.headline4,
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () => Navigator.pop(context),
child: Text('Go Back'),
),
],
),
),
);
}
}
最佳实践
- 转场时长 200~300ms:Material 默认 300ms,iOS 默认也是这个量级;超过 500ms 用户会觉得”卡”,低于 150ms 会觉得”闪”
- 曲线先抄默认:Curves.easeInOut / fastOutSlowIn 是系统调教好的,自定义先从这里起步
- GetX 里用内置 transition:
transition: Transition.rightToLeft这类够用了,别手写 PageRouteBuilder——除非真有品牌动效需求 - 动画只做”方向+淡入淡出”:缩放/旋转转场在业务 App 里 99% 是负优化(用户无感,还容易晕)
注意事项
- 别覆盖 iOS 边缘右滑返回手势:自定义转场(尤其 rightToLeftWithFade)和原生返回手势冲突,测试机上偶现卡死——作者真实踩坑,最后改回默认了事
- 转场期间别做耗时操作:动画掉帧的锅,八成是”跳转同时在查数据库/解码图片”——先跳过去,再加载数据
- 低端机实测:旗舰机上丝滑不代表千元机上不掉帧,真机测试别只测手里的主力机
- 无障碍:系统开了”减弱动态效果”时,考虑缩短或跳过转场动画(MediaQuery.disableAnimations)
实战案例
页面转场动画这个场景,我还没在项目里深度定制过——说实话,业务 App 里 99% 的跳转用默认动画用户根本无感。我的货运和电商项目(旅行类应用 等)全是 GetX 默认转场,iOS 右滑进入、Android 淡入,没改过。唯一一次想改,是老板说”跳转要丝滑一点”,我试了 GetX 的 transition: Transition.rightToLeftWithFade,效果是有了,但和 iOS 的边缘右滑返回手势冲突,测试机上偶现卡死,又改回去了。教训:转场动画别为了炫而改,原生平台的默认手势(尤其是 iOS 右滑返回)优先级最高。真要做品牌化动效,先保证返回手势不炸。参考:Flutter 官方文档转场动画章节。
总结
转场动画的 80% 场景用 PageRouteBuilder 就够了:淡入、滑动、缩放三种过渡拼一拼,再包一层 CustomPageRoute 复用。记住一条:别和平台原生手势打架——iOS 右滑返回的优先级高于一切自定义动画,动效再炫,返回手势炸了就是负分。
