跳转到正文
YGJoon 技术博客

Flutter 路由动画详解

发布于:
·1.1k 字 · 约 3 分钟·作者:
PageRouteBuilder 手写转场:淡入、滑动、缩放三种过渡效果,附可复用的 CustomPageRoute 封装
Flutter 路由动画详解文章封面
目录

简介

路由动画可以让页面切换更加流畅自然。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'),
            ),
          ],
        ),
      ),
    );
  }
}

最佳实践

  1. 转场时长 200~300ms:Material 默认 300ms,iOS 默认也是这个量级;超过 500ms 用户会觉得”卡”,低于 150ms 会觉得”闪”
  2. 曲线先抄默认:Curves.easeInOut / fastOutSlowIn 是系统调教好的,自定义先从这里起步
  3. GetX 里用内置 transition:transition: Transition.rightToLeft 这类够用了,别手写 PageRouteBuilder——除非真有品牌动效需求
  4. 动画只做”方向+淡入淡出”:缩放/旋转转场在业务 App 里 99% 是负优化(用户无感,还容易晕)

注意事项

  1. 别覆盖 iOS 边缘右滑返回手势:自定义转场(尤其 rightToLeftWithFade)和原生返回手势冲突,测试机上偶现卡死——作者真实踩坑,最后改回默认了事
  2. 转场期间别做耗时操作:动画掉帧的锅,八成是”跳转同时在查数据库/解码图片”——先跳过去,再加载数据
  3. 低端机实测:旗舰机上丝滑不代表千元机上不掉帧,真机测试别只测手里的主力机
  4. 无障碍:系统开了”减弱动态效果”时,考虑缩短或跳过转场动画(MediaQuery.disableAnimations)

实战案例

页面转场动画这个场景,我还没在项目里深度定制过——说实话,业务 App 里 99% 的跳转用默认动画用户根本无感。我的货运和电商项目(旅行类应用 等)全是 GetX 默认转场,iOS 右滑进入、Android 淡入,没改过。唯一一次想改,是老板说”跳转要丝滑一点”,我试了 GetX 的 transition: Transition.rightToLeftWithFade,效果是有了,但和 iOS 的边缘右滑返回手势冲突,测试机上偶现卡死,又改回去了。教训:转场动画别为了炫而改,原生平台的默认手势(尤其是 iOS 右滑返回)优先级最高。真要做品牌化动效,先保证返回手势不炸。参考:Flutter 官方文档转场动画章节。

总结

转场动画的 80% 场景用 PageRouteBuilder 就够了:淡入、滑动、缩放三种过渡拼一拼,再包一层 CustomPageRoute 复用。记住一条:别和平台原生手势打架——iOS 右滑返回的优先级高于一切自定义动画,动效再炫,返回手势炸了就是负分。


分享这篇文章:


相关文章

上一篇
Flutter 交错动画详解
下一篇
Flutter 自定义动画详解