三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

Flutter Hero 动画完全指南

Flutter Hero 动画完全指南

Flutter Hero 动画完全指南

引言

Hero 动画是 Flutter 中实现页面间元素过渡的强大工具,它允许元素在页面跳转时平滑地从一个页面"飞"到另一个页面。本文将深入探讨 Hero 动画的各种用法和高级技巧。

基础概念回顾

什么是 Hero 动画

Hero 动画是一种共享元素过渡效果,它让同一个 Widget 在两个页面之间平滑过渡。

基本语法

// 源页面 Hero( tag: 'hero-tag', child: Image.network('image.jpg'), ) // 目标页面 Hero( tag: 'hero-tag', child: Image.network('image.jpg'), )

高级技巧一:基本 Hero 动画

创建简单的 Hero 动画

class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('首页')), body: Center( child: Hero( tag: 'avatar', child: GestureDetector( onTap: () { Navigator.push( context, MaterialPageRoute(builder: (_) => DetailPage()), ); }, child: CircleAvatar( backgroundImage: NetworkImage('https://example.com/avatar.jpg'), radius: 50, ), ), ), ), ); } } class DetailPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('详情页')), body: Center( child: Hero( tag: 'avatar', child: CircleAvatar( backgroundImage: NetworkImage('https://example.com/avatar.jpg'), radius: 100, ), ), ), ); } }

自定义过渡效果

Hero( tag: 'custom-hero', createRectTween: (begin, end) { return CustomRectTween(begin: begin, end: end); }, child: MyWidget(), )

高级技巧二:Hero 动画样式

添加圆角

Hero( tag: 'rounded-image', child: ClipRRect( borderRadius: BorderRadius.circular(16), child: Image.network('image.jpg'), ), )

添加阴影

Hero( tag: 'shadow-hero', child: Container( decoration: BoxDecoration( boxShadow: [ BoxShadow( color: Colors.black26, blurRadius: 8, offset: Offset(2, 2), ), ], ), child: Image.network('image.jpg'), ), )

高级技巧三:多个 Hero 动画

并行过渡多个元素

// 源页面 Row( children: [ Hero( tag: 'image-hero', child: Image.network('image.jpg'), ), Hero( tag: 'text-hero', child: Text('标题'), ), ], ) // 目标页面 Column( children: [ Hero( tag: 'image-hero', child: Image.network('image.jpg'), ), Hero( tag: 'text-hero', child: Text('标题'), ), ], )

高级技巧四:Hero 动画控制器

使用 HeroController

MaterialApp( home: MyHomePage(), heroController: HeroController( createRectTween: (begin, end) { return MaterialRectArcTween(begin: begin, end: end); }, ), )

自定义过渡曲线

Hero( tag: 'curve-hero', flightShuttleBuilder: ( BuildContext flightContext, Animation<double> animation, HeroFlightDirection flightDirection, BuildContext fromHeroContext, BuildContext toHeroContext, ) { return ScaleTransition( scale: animation.drive( CurveTween(curve: Curves.easeInOut), ), child: Image.network('image.jpg'), ); }, child: Image.network('image.jpg'), )

实战案例:图片画廊

class GalleryPage extends StatelessWidget { final List<String> images = [ 'https://example.com/img1.jpg', 'https://example.com/img2.jpg', 'https://example.com/img3.jpg', ]; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('图片画廊')), body: GridView.builder( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, ), itemCount: images.length, itemBuilder: (context, index) { return GestureDetector( onTap: () { Navigator.push( context, PageRouteBuilder( pageBuilder: (_, __, ___) => DetailImagePage( imageUrl: images[index], index: index, ), ), ); }, child: Hero( tag: 'image-$index', child: Image.network( images[index], fit: BoxFit.cover, ), ), ); }, ), ); } } class DetailImagePage extends StatelessWidget { final String imageUrl; final int index; DetailImagePage({required this.imageUrl, required this.index}); @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.black, body: Center( child: Hero( tag: 'image-$index', child: Image.network(imageUrl), ), ), ); } }

实战案例:卡片详情页

class CardListPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('卡片列表')), body: ListView.builder( itemCount: 10, itemBuilder: (context, index) { return Card( child: ListTile( leading: Hero( tag: 'avatar-$index', child: CircleAvatar( child: Text('${index + 1}'), ), ), title: Hero( tag: 'title-$index', child: Text('卡片 ${index + 1}'), ), onTap: () { Navigator.push( context, MaterialPageRoute( builder: (_) => CardDetailPage(index: index), ), ); }, ), ); }, ), ); } } class CardDetailPage extends StatelessWidget { final int index; CardDetailPage({required this.index}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Hero(tag: 'title-$index', child: Text('卡片 ${index + 1}'))), body: Center( child: Hero( tag: 'avatar-$index', child: CircleAvatar( radius: 80, child: Text('${index + 1}'), ), ), ), ); } }

实战案例:Hero 动画与页面路由

class CustomHeroRoute extends PageRouteBuilder { final Widget page; CustomHeroRoute({required this.page}) : super( pageBuilder: (context, animation, secondaryAnimation) => page, transitionsBuilder: (context, animation, secondaryAnimation, child) { return FadeTransition( opacity: animation, child: child, ); }, ); } // 使用 Navigator.push( context, CustomHeroRoute(page: DetailPage()), );

常见问题与解决方案

Q1:Hero 动画不生效?

A:确保两个页面的 Hero tag 相同:

// 正确 Hero(tag: 'same-tag', child: ...) // 错误 Hero(tag: 'tag1', child: ...) // 源页面 Hero(tag: 'tag2', child: ...) // 目标页面

Q2:如何自定义过渡路径?

A:使用 createRectTween:

Hero( tag: 'custom-path', createRectTween: (begin, end) { return MaterialRectCenterArcTween(begin: begin, end: end); }, child: ..., )

Q3:如何在 Hero 动画期间显示遮罩?

A:使用 flightShuttleBuilder:

Hero( tag: 'mask-hero', flightShuttleBuilder: (context, animation, flightDirection, from, to) { return Material( color: Colors.black54, child: Image.network('image.jpg'), ); }, child: Image.network('image.jpg'), )

最佳实践

1. 使用唯一的 tag

// 推荐 Hero(tag: 'user-avatar-${user.id}', child: ...) // 不推荐 Hero(tag: 'avatar', child: ...) // 多个元素使用相同 tag

2. 保持子 Widget 类型一致

// 推荐 Hero(tag: 'image', child: Image.network('url')) // 两边都是 Image // 不推荐 Hero(tag: 'image', child: Image.network('url')) // 源页面 Hero(tag: 'image', child: Container()) // 目标页面

3. 避免复杂的 Hero 子 Widget

// 推荐 Hero( tag: 'simple-hero', child: Image.network('url'), // 简单 Widget ) // 不推荐 Hero( tag: 'complex-hero', child: Container( child: Column( children: [...], // 复杂嵌套 ), ), )

总结

Flutter 的 Hero 动画是创建流畅页面过渡的强大工具。通过本文的学习,你应该能够:

  1. 创建基本的 Hero 动画
  2. 实现多个元素的并行过渡
  3. 自定义过渡效果和路径
  4. 处理常见问题
  5. 遵循最佳实践

掌握这些技巧,能够帮助你创建更加吸引人的用户体验。

← 返回列表