Flutter Hero动画案例

Hero 指的是能够在路由(页面)之间“飞行”的 widget,简单来讲 Hero 动画就是在路由切换时,有一个共享的 widget 能够在新旧路由间切换。因为共享的 widget 在新旧路由页面上的位置、外观可能有所差别,因此在路由切换时会从旧路逐渐过渡到新路由中的指定位置,这样就会产生一个 Hero 动画。canvas

实现一个简单的 Hero 动画

    
  
  
  
   
   
            
   
   
  1. 微信

  2. app

  3. less

  4. ide

  5. 函数

  6. 学习

  7. flex

  8. 动画

  9. ui

Container( alignment: Alignment.center, child: InkWell( child: Hero( tag: "tag",//惟一标记,先后两个路由页Hero的tag必须相同 child: ClipOval( child: Image.asset( "assets/datas/night.jpg", width: 80, height: 80, fit: BoxFit.cover, ), )), onTap: () { Navigator.of(context).push( new MaterialPageRoute(builder: (BuildContext context) { return new HeroAnimationPage1(); })); }, ), )
  • HeroAnimationPage1

    
  
  
  
   
   
            
   
   
class HeroAnimationPage1 extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Hero1"), ), body: Center( child: Container( margin: EdgeInsets.all(20), child: Hero( tag: "tag", //惟一标记,先后两个路由页Hero的tag必须相同 child: ClipOval( child: Image.asset( "assets/datas/night.jpg", width: 120, height: 120, fit: BoxFit.cover, ), ), ), ), )); }}

效果图

能够看到,实现 Hero 动画只须要用 Hero 组件将要共享的 widget 包装起来,并提供一个相同的 tag 便可,中间的过渡帧都是 Flutter Framework 自动完成的。必需要注意, 先后路由页的共享 Hero 的 tag 必须是相同的,Flutter Framework 内部正是经过 tag 来肯定新旧路由页 widget 的对应关系的。

但咱们用的是 MaterialPageRoute 这个系统给咱们提供好的路由,这个路由能让咱们在 Android 或者 Ios 上呈现相应的页面跳转效果,但在这里和 Hero 合起来看有点杂乱,别扭,咱们稍微改一下:

    
  
  
  
   
   
            
   
   

Container( alignment: Alignment.center, child: InkWell( child: Hero( tag: "tag", //惟一标记,先后两个路由页Hero的tag必须相同 child: ClipOval( child: Image.asset( "assets/datas/night.jpg", width: 80, height: 80, fit: BoxFit.cover, ), )), onTap: () {// Navigator.of(context).push(// new MaterialPageRoute(builder: (BuildContext context) {// return new HeroAnimationPage1();// })); Navigator.push(context, PageRouteBuilder(pageBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation) { return new FadeTransition( opacity: animation, child: Scaffold( appBar: AppBar( title: Text("Fade"), ), body: HeroAnimationRouteWithFade(), ), ); })); }, ), )
  • HeroAnimationRouteWithFade

    
  
  
  
   
   
            
   
   
class HeroAnimationRouteWithFade extends StatelessWidget { @override Widget build(BuildContext context) { return Center( child: Container( margin: EdgeInsets.all(20), child: Hero( tag: "tag", //惟一标记,先后两个路由页Hero的tag必须相同 child: ClipOval( child: Image.asset( "assets/datas/night.jpg", width: 120, height: 120, fit: BoxFit.cover, ), ), ), ), ); }}

效果图 


经过 PageRouteBuilder 自定义本身的路由器,动画看起来明显干净了不少。

不过细心的我发现,在运动(飞翔)过程当中,图片在慢慢变大的同时好像也稍微有点变形。这其实我也不知道为啥,后来我从 Flutter 中文官网看到 MaterialRectCenterArcTween 这个类。这个类竟然能让控件在运动过程当中圆角不变形。

咱们先回头看看 Hero 的构造函数:

    
  
  
  
   
   
            
   
   
const Hero({ Key key, @required this.tag, this.createRectTween, this.flightShuttleBuilder, this.placeholderBuilder, this.transitionOnUserGestures = false, @required this.child, }) : assert(tag != null), assert(transitionOnUserGestures != null), assert(child != null), super(key: key);
  • tag:[必须]用于关联两个 Hero 动画的标识,先后两个路由页 Hero 的 tag 必须相同.

  • createRectTween:[可选]定义目标 Hero 的边界,在从起始位置到目的位置的运动过程当中该如何变化。

  • child:[必须]定义动画所呈现的 widget。

MaterialRectCenterArcTween 这个类正好能给咱们返回 createRectTween 参数所需的实例。

咱们再来看一个稍微高大上的 Hero 动画,其实我以为没啥改动,只是界面作的稍微复杂一点,添加了 createRectTween。

实现一个复杂的 Hero 动画 

  • 这里提一下 timeDilation = 3; 这是让过渡动画时间稍微慢一点,默认为 1.

    
  
  
  
   
   
            
   
   
timeDilation = 3; //动画过渡时间

其实第二个案例就只是添加了 createRectTween,你能够试着注释 createRectTween;或者案例 2 中直接注释 Hero 控件;看看它们的动画效果是啥,想来你印象会更深。

所有代码

    
  
  
  
   
   
            
   
   




















import 'package:flutter/material.dart';import 'package:flutter_travel/widgets/item_widget.dart';import 'package:flutter/scheduler.dart' show timeDilation;import 'dart:math' as math;const Tag = "tag"; ////惟一标记,先后两个路由页Hero的tag必须相同class HeroPage extends StatefulWidget { @override _HeroPageState createState() => _HeroPageState();}class _HeroPageState extends State<HeroPage> { @override void initState() { // TODO: implement initState super.initState(); } @override Widget build(BuildContext context) { timeDilation = 3; //动画过渡时间 return Scaffold( appBar: AppBar( title: Text("Hero"), ), body: Container( margin: EdgeInsets.only(top: 30, right: 5, left: 5), child: Column( children: <Widget>[ Expanded( flex: 1, child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: <Widget>[ Container( height: 100, alignment: Alignment.center, child: InkWell( child: Hero( tag: Tag, child: ClipOval( child: Image.asset( "assets/datas/night.jpg", width: 80, height: 80, fit: BoxFit.cover, ), )), onTap: () {// Navigator.of(context).push(// new MaterialPageRoute(builder: (BuildContext context) {// return new HeroAnimationPage1();// })); Navigator.push(context, PageRouteBuilder(pageBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation) { return new FadeTransition( opacity: animation, child: Scaffold( appBar: AppBar( title: Text("Fade"), ), body: HeroAnimationRouteWithFade(), ), ); })); }, ), ), Container( height: 40, width: double.infinity, alignment: Alignment.center, margin: EdgeInsets.only(top: 30), child: Text( "下面的案例仅仅只是添加了RectTween", style: TextStyle(fontWeight: FontWeight.bold), ), ), ], )), Expanded( flex: 2, child: Container( alignment: Alignment.bottomCenter, margin: EdgeInsets.only(bottom: 100), child: _buildHero(context, 'assets/datas/empty.png', '空空如也'), )) ], ), ), ); } @override void dispose() { // TODO: implement dispose super.dispose(); }}class HeroAnimationPage1 extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Hero1"), ), body: Center( child: Container( margin: EdgeInsets.all(20), child: Hero( tag: Tag, //惟一标记,先后两个路由页Hero的tag必须相同 child: ClipOval( child: Image.asset( "assets/datas/night.jpg", width: 120, height: 120, fit: BoxFit.cover, ), ), ), ), )); }}class HeroAnimationRouteWithFade extends StatelessWidget { @override Widget build(BuildContext context) { return Center( child: Container( margin: EdgeInsets.all(20), child: Hero( tag: "tag", //惟一标记,先后两个路由页Hero的tag必须相同 child: ClipOval( child: Image.asset( "assets/datas/night.jpg", width: 120, height: 120, fit: BoxFit.cover, ), ), ), ), ); }}/////////////////////////////////////华丽的分割线///////////////////////////////////const double kMinRadius = 40.0;const double kMaxRadius = 150.0;class Photo extends StatelessWidget { Photo({Key key, this.photo, this.color, this.onTap}) : super(key: key); final String photo; final Color color; final VoidCallback onTap; Widget build(BuildContext context) { return Material( color: Colors.grey.withOpacity(0.25), child: InkWell( onTap: onTap, child: Image.asset( photo, color: Colors.green.withOpacity(.8), fit: BoxFit.contain, ), ), ); }}RectTween _createRectTween(Rect begin, Rect end) { print("begin=${begin}\t end=${end}"); return MaterialRectCenterArcTween(begin: begin, end: end);}Widget _buildHero(BuildContext context, String imageName, String description) { return Container( width: kMinRadius * 2.0, height: kMinRadius * 2.0, child: Hero( createRectTween: _createRectTween, tag: imageName, child: RadialExpansion( maxRadius: kMaxRadius, child: Photo( photo: imageName, onTap: () { Navigator.of(context).push( PageRouteBuilder<void>( pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) { return FadeTransition( opacity: animation, child: _showDetailPage(context, imageName, description)); }, ), ); }, ), ), ), );}Widget _showDetailPage( BuildContext context, String imageName, String description) { return Container( color: Theme.of(context).canvasColor, child: Center( child: Card( elevation: 8.0, child: Column( mainAxisSize: MainAxisSize.min, children: [ SizedBox( width: kMaxRadius * 2.0, height: kMaxRadius * 2.0, child: Hero( createRectTween: _createRectTween, tag: imageName, child: RadialExpansion( maxRadius: kMaxRadius, child: Photo( photo: imageName, onTap: () { Navigator.of(context).pop(); }, ), ), ), ), Text( description, style: TextStyle( fontWeight: FontWeight.bold, ), textScaleFactor: 3.0, ), const SizedBox(height: 16.0), ], ), ), ), );}class RadialExpansion extends StatelessWidget { RadialExpansion({ Key key, this.maxRadius, this.child, }) : clipRectSize = 2.0 * (maxRadius / math.sqrt2), super(key: key); final double maxRadius; final clipRectSize; final Widget child; @override Widget build(BuildContext context) { return ClipOval( child: Center( child: SizedBox( width: clipRectSize, height: clipRectSize, child: ClipRect( child: child, ), ), ), ); }}


本文分享自微信公众号 - Flutter学习簿(gh_d739155d3b2c)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索