在前面实践组件的开发中,我们做了一个登录的界面,里面有一个组件Hero,不知道大家是否记得?当时没有展开来说,是因为它属于动画的内容,本文就要重点讲解Hero动画。
做过Java开发Android的程序员应该都清楚,Shared Element Transition可以让Activity或Fragment做出流畅的动画,同样,在Flutter开发中,Hero动画也能实现类似的效果。简单来说,Hero的作用就是在路由之间做出流畅的转场动画。
Hero组件的用法是需要同时定义源组件和目标组件,其中源组件和目标组件被Hero包裹在需要动画控制的组件外面,如果有一方不指定,在有些情况下,界面就会卡死,我们先来看看它的基本用法,首先是main.dart代码:
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("我是第一个界面"),
),
body: Center(
child: GestureDetector(
child: Hero(
tag: "tag1",
child: FlutterLogo(
size: 200,
),
),
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (BuildContext context)=>CustomFlutterLogoPage()));
},
),
),
);
}
}
代码很简单,就是监听点击事件ontap,hero包裹FlutterLogo组件,然后点击跳转到第二个界面。接着我们再来看看第二个页面CustomFlutterLogo.dart的代码:
import 'package:flutter/material.dart';
class CustomFlutterLogoPage extends StatefulWidget {
CustomFlutterLogoPage({Key key, this.title}) : super(key: key);
final String title;
@override
_CustomFlutterLogoState createState() => _CustomFlutterLogoState();
}
class _CustomFlutterLogoState extends State<CustomFlutterLogoPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("我是第二个页面"),),
body: Center(
child: Hero(
tag: "tag2",
child: CustomFlutterLogo(
size: 400,
name: "我是第二个页面",
),
),
),
);
}
}
class CustomFlutterLogo extends StatelessWidget{
final double size;
final String name;
CustomFlutterLogo({this.size=200.0,this.name});
@override
Widget build(BuildContext context) {
return Container(
child: Center(
child: FlutterLogo(
size: this.size,
),
),
);
}
}
这段代码也很简单,就是常用的组件,只是在外层套了一层Hero动画组件,不过这里有一点我们需要注意,hero里面有一个tag属性,必须写上,不然会报错,不信的读者,可以删除后运行试试。
我们基本已经掌握了Hero路由跳转动画的用法,但我们不能只看表面,不明其原理,因为后面讲解的动画也会涉及到这些知识,所以我们必须掌握。
Hero动画,它的整个运动过程分为3个步骤,即动画开始(t=0.0),动画进行中,动画结束(t=1.0),下面是Hero动画运动示意图:
如上图所示,两个路由之间还有一个Overlay层。在动画开始时,Flutter会计算出Hero的位置并复制一份,然后绘制到Overlay层上。复制的Hero和源Hero的大小是一致的,并且该Hero是在所有路由之上。在动画实现的过程中,Flutter会逐渐把源Hero移除屏幕。在动画进行中Flutter是依靠Tween来实现,通过createRectTween属性把Tween传给Hero。Hero内部默认使用MeterialRectArcTween的曲线路径进行移动动画的操作。在动画结束时,Flutter将Overlay中的Hero移除,且完成了Hero在目标路由上的显示,这时Overlay是空白的。
Hero中所有变换都是通过HeroController来实现的,HeroController是在MeterialApp中通过initState和didUpdateWidget方法来完成初始化的,源码如下所示:
class _MaterialAppState extends State<MaterialApp>{
HeroController heroController;
@override
void initState(){
super.initState();
_heroController=HeroController(createRectTween:_createRectTween);
_updateNavigator();
}
@override
void didUpdateWidget(MaterialApp oldWidget){
super.didUpdateWidget(oldWidget);
if(widget.navigatorKey!=oldWidget.navigatorKey){
_heroController=HeroController(createRectTween:_createRectTween);
}
_updateNavigator();
}
RectTween _createRectTween(Rect begin,Rect end){
return MaterialRectArcTween(begin:begin,end:end);
}
}
在初始化HeroController时,Flutter携带了一个参数,就是_createRectTween,该参数返回的默认项就是MaterialRectArcTween。Flutter源码里还为我们实现了第二种RectTween返回值,即MaterialRectCenterArcTween。由此可见,可以对createRectTween进行自定义。我们再看看HeroController的具体内容,代码如下:
@override
void didPush(Route<dynamic> route,Route<dynamic> previousRoute){
assert(navigator!=null);
assert(route!=null);
_maybeStartHeroTransition(previousRoute,route,HeroFlightDirection.push,false);
}
@override
void didPop(Route<dynamic> route,Route<dynamic> previousRoute){
assert(navigator!=null);
assert(route!=null);
_maybeStartHeroTransition(route,previousRoute,HeroFlightDirection.pop,false);
}
HeroController其实继承的是NavigatorObserver。在路由操作的didPush和didPop回调方法里,可以调用_maybeStartHeroTransition,并通过WidgetsBinding把源路由,目标路由,HeroController关联起来。在使用didPush和didPop回调时,通过调用_startHeroTransition方法让Hero动起来,只不过前者是正向的,后者是逆向的。