Hero 指的是能够在路由(页面)之间“飞行”的 widget,简单来讲 Hero 动画就是在路由切换时,有一个共享的 widget 能够在新旧路由间切换。因为共享的 widget 在新旧路由页面上的位置、外观可能有所差别,因此在路由切换时会从旧路逐渐过渡到新路由中的指定位置,这样就会产生一个 Hero 动画。canvas
实现一个简单的 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(); })); }, ), )
微信
app
less
ide
函数
学习
flex
动画
ui
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源创计划”,欢迎正在阅读的你也加入,一块儿分享。