Flutter中的动画
Flutter中的动画系统基于类型化 Animation 对象。小部件可以直接通过读取它们的当前值并监听它们的状态变化将这些动画合并到它们的构建函数中,或者它们可以使用动画作为它们传递给其他小部件的更精细动画的基础。个人感觉Flutter中的动画和Android中的属性动画有点相似,作用于widget的属性值,既然有了参照物了,理解起来就没那么难了。
动画类型
补间(Tween)动画
在补间动画中,定义了开始点和结束点、时间线以及定义转换时间和速度的曲线。然后由框架计算如何从开始点过渡到结束点。
基于物理的动画
在基于物理的动画中,运动被模拟为与真实世界的行为相似。例如,当你掷球时,它在何处落地,取决于抛球速度有多快、球有多重、距离地面有多远。 类似地,将连接在弹簧上的球落下(并弹起)与连接到绳子上的球放下的方式也是不同。
动画的部分特点
- Animation对象知道动画的当前状态(例如,它是开始、停止还是向前或向后移动),但它不知道屏幕上显示的内容。
- AnimationController管理Animation。
- CurvedAnimation 管理动画的过程.(插值器)
- Tween在正在执行动画的对象所使用的数据范围之间生成值。例如,Tween可能会生成从红到蓝之间的色值,或者从0到255。
- 使用Listeners和StatusListeners监听动画状态改变。:在addListeners方法里可以拿到动画过程中的一个一个的值。addStatusListeners中可以监听动画加载状态(开始,结束,消失等)。
常见的动画模式
Animation<>
在Flutter中,Animation对象本身和UI渲染没有任何关系。Animation是一个抽象类,它拥有其当前值和状态(完成或停止)。其中一个比较常用的Animation类是Animation< double >。
- Animation对象是一个在一段时间内依次生成一个区间之间值的类。
- Animation对象的输出可以是线性的、曲线的、一个步进函数或者任何其他可以设计的映射。 根据Animation对象的控制方式,动画可以反向运行,甚至可以在中间切换方向。
- Animation还可以生成除double之外的其他类型值,如:Animation< Color >或 Animation< Size >。
- Animation对象有状态。可以通过访问其value属性获取动画的当前值。
- Animation对象本身和UI渲染没有任何关系。
AnimationController
要创建动画,首先要创建一个 AnimationController。除了作为动画本身,AnimationController还可以控制动画。例如,可以告诉控制器播放动画 forward 或stop 动画。还可以fling 使用物理模拟(如弹簧)来制作动画。
AnimationController是Animation< double >类型 ,是一个特殊的Animation对象,在屏幕刷新的每一帧,就会生成一个新的值。默认情况下,AnimationController在给定的时间段内会线性的生成从0.0到1.0的数字。
在某些情况下,值(position,值动画的当前值)可能会超出AnimationController的0.0-1.0的范围。例如,fling()函数允许您提供速度(velocity)、力量(force)、position(通过Force对象)。位置(position)可以是任何东西,因此可以在0.0到1.0范围之外。 CurvedAnimation生成的值也可以超出0.0到1.0的范围。根据选择的曲线,CurvedAnimation的输出可以具有比输入更大的范围。例如,Curves.elasticIn等弹性曲线会生成大于或小于默认范围的值。
//创建一个Animation对象,但不会启动它运行:
final AnimationController controller = new AnimationController(
duration: const Duration(milliseconds: 2000), vsync: this);
- AnimationController派生自Animation< double >,因此可以在需要Animation对象的任何地方使用。
- AnimationController具有控制动画的其他方法。例如,.forward()方法可以启动动画。数字的产生与屏幕刷新有关,因此每秒钟通常会产生60个数字,在生成每个数字后,Animation对象可以调用添加的Listener对象
- 当创建一个AnimationController时,需要传递一个vsync参数,存在vsync时会防止屏幕外动画(动画的UI不在当前屏幕时)消耗不必要的资源。 通过将SingleTickerProviderStateMixin添加到类定义中,可以将stateful对象作为vsync的值。vsync对象会绑定动画的定时器到一个可视的widget,所以当widget不显示时,动画定时器将会暂停,当widget再次显示时,动画定时器重新恢复执行,这样就可以避免动画相关UI不在当前屏幕时消耗资源。 如果要使用自定义的State对象作为vsync时,请包含TickerProviderStateMixin。
class MyAnimationControllerState extends State<CollectionWidget> with TickerProviderStateMixin{
//创建一个Animation对象,但不会启动它运行:
final AnimationController controller = new AnimationController(
duration: const Duration(milliseconds: 2000), vsync: this);
}
常用方法
- animateTo(double target, { Duration duration, Curve curve: Curves.linear }) :将动画从其当前值驱动到目标值
- animateWith(Simulation simulation):根据给定的需要模拟的物体运动(类似物体的速度,受到的力等)开展动画
- dispose() :释放此对象使用的资源。调用此方法后,该对象不再可用。
- fling({double velocity: 1.0, AnimationBehavior animationBehavior }) :使用临界阻尼弹簧(在lowerBound 和upperBound内)和初始速度驱动动画。
- forward({double from }) :开始向前运行此动画(到最后)。
- repeat({double min, double max, Duration period }) :以向前方向开始运行此动画,并在完成时重新启动动画。
- reset() :将控制器的值设置为lowerBound,停止动画(如果正在进行中),并重置为其开始点或解除状态。
- reverse({double from }):开始反向运行此动画(朝向开头)。
- stop({bool canceled: true }):停止运行此动画。
CurvedAnimation
将曲线应用于另一个动画。是对该动画的包装,我的理解是Android中的插值器
当想要将非线性曲线应用于动画对象时,CurvedAnimation很有用,特别是如果想要在动画前进时与后退时使用不同的曲线。
根据给定的曲线,CurvedAnimation的输出可能比其输入具有更宽的范围。例如,弹性曲线(如 Curves.elasticIn)将显着超过或低于0.0到1.0的默认范围。
- CurvedAnimation 将动画过程定义为一个非线性曲线.
AnimationController controller = new AnimationController(vsync: this,duration: Duration(milliseconds:3000));
//立方体动画曲线,缓慢开始并快速结束(easeIn)。
final CurvedAnimation curve =
new CurvedAnimation(parent: controller, curve: Curves.easeIn);
- Curves 类类定义了许多常用的曲线,也可以创建自己的
class ShakeCurve extends Curve {
@override
double transform(double t) {
return math.sin(t * math.PI * 2);
}
}
final CurvedAnimation curve =
new CurvedAnimation(parent: controller, curve: ShakeCurve());
Tween
默认情况下,AnimationController对象的范围从0.0到1.0。如果需要不同的范围或不同的数据类型,则可以使用Tween来配置动画以生成不同的范围或数据类型的值。
//Tween生成从-200.0到0.0的值:
final Tween doubleTween = new Tween<double>(begin: -200.0, end: 0.0);
- Tween是一个无状态(stateless)对象,需要begin和end值。Tween的唯一职责就是定义从输入范围到输出范围的映射。
- Tween继承自Animatable,而不是继承自Animation。Animatable与Animation相似,不是必须输出double值。
//ColorTween指定两种颜色之间的过渡。
final Tween colorTween =
new ColorTween(begin: Colors.transparent, end: Colors.black54);
- Tween对象不存储任何状态。相反,它提供了evaluate(Animation animation)方法将映射函数应用于动画当前值。 Animation对象的当前值可以通过value()方法取到。
- 要使用Tween对象,请调用其animate()方法,传入一个控制器对象。
//500毫秒内生成从0到255的整数值。
final AnimationController controller = new AnimationController(
duration: const Duration(milliseconds: 500), vsync: this);
Animation<int> alpha = new IntTween(begin: 0, end: 255).animate(controller);
//一个控制器、一条曲线和一个Tween:500毫秒内生成从0到255的整数值(快速开始并缓慢结束)。
final AnimationController controller = new AnimationController(
duration: const Duration(milliseconds: 500), vsync: this);
final Animation curve =
new CurvedAnimation(parent: controller, curve: Curves.easeOut);
Animation<int> alpha = new IntTween(begin: 0, end: 255).animate(curve);
动画监听
一个Animation对象可以拥有Listeners和StatusListeners监听器,可以用addListener()和addStatusListener()来添加。 只要动画的值发生变化,就会调用监听器。一个Listener最常见的行为是调用setState()来触发UI重建。动画开始、结束、向前移动或向后移动(如AnimationStatus所定义)时会调用StatusListener。
//一个简单的实例
AnimationController controller = new AnimationController(vsync: this,duration: Duration(milliseconds:widget.time));
CurvedAnimation curvedAnimation = new CurvedAnimation(parent: controller,curve: Curves.linear);
Tween animationTween = new Tween(begin: 0.0,end: 360.0);
animation = animationTween.animate(curvedAnimation);
animation.addStatusListener((status){
if(status==AnimationStatus.completed){
...
}
});
animation.addListener((){
angle = animation.value;
setState(() {
});
});
controller.forward();
AnimatedWidget
AnimatedWidget类允许从setState()调用中的动画代码中分离出widget代码。AnimatedWidget不需要维护一个State对象来保存动画。AnimatedWidget(基类)中会自动调用addListener()和setState()。
- AnimatedWidget助手类(而不是addListener()和setState())来给widget添加动画
- AnimatedWidget创建一个可重用动画的widget。要从widget中分离出动画过渡,使用AnimatedBuilder。
- Flutter API提供的关于AnimatedWidget的示例包括:AnimatedBuilder、AnimatedModalBarrier、DecoratedBoxTransition、FadeTransition、PositionedTransition、RelativePositionedTransition、RotationTransition、ScaleTransition、SizeTransition、SlideTransition。
//AnimatedWidget的构造函数只接受一个动画对象。 为了解决这个问题,可以创建了自己的Tween对象并显式计算了这些值。
class AnimateLogo extends AnimatedWidget {
static final _opacityTween = new Tween<double>(begin: 0.1, end: 1.0);
static final _sizeTween = new Tween<double>(begin: 0.0, end: 300.0);
AnimateLogo({Key key, Animation<double> animation})
: super(key: key, listenable: animation);
@override
Widget build(BuildContext context) {
final Animation<double> animation = listenable;
return new Center(
child: new Opacity(opacity: _opacityTween.evaluate(animation),
child: new Container(
margin: new EdgeInsets.symmetric(vertical: 10.0),
width: _sizeTween.evaluate(animation),
height: _sizeTween.evaluate(animation),
child: new FlutterLogo(),
),
),
);
}
}
AnimatedBuilder
问题:AnimatedWidget更改动画需要更改显示l的widget,解决方案是将职责分离:(1.显示widget 2.定义Animation对象 3.渲染过渡效果)可以借助AnimatedBuilder类完成此分离。AnimatedBuilder是渲染树中的一个独立的类。 与AnimatedWidget类似,AnimatedBuilder自动监听来自Animation对象的通知,并根据需要将该控件树标记为脏(dirty),因此不需要手动调用addListener()。
- AnimatedBuilder 不知道如何渲染widget,也不知道如何管理Animation对象。
- AnimatedBuilder渲染过渡
- AnimatedBuilder将动画描述为另一个widget的build方法的一部分。如果你只是想用可复用的动画定义一个widget,使用AnimatedWidget。
- Flutter API中AnimatedBuilder的示例包括: BottomSheet、ExpansionTile、 PopupMenu、ProgressIndicator、RefreshIndicator、Scaffold、SnackBar、TabBar、TextField。
class Spinner extends StatefulWidget {
@override
_SpinnerState createState() => _SpinnerState();
}
//使用AnimatedBuilder构建,并使用 builder功能来避免每次都重建Container。
class _SpinnerState extends State<Spinner> with SingleTickerProviderStateMixin {
AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 10),
vsync: this,
)..repeat();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
child: Container(width: 200.0, height: 200.0, color: Colors.green),
builder: (BuildContext context, Widget child) {
return Transform.rotate(
angle: _controller.value * 2.0 * math.pi,
child: child,
);
},
);
}
}
并行动画
在同一个动画控制器上使用多个Tween,每一个Tween管理动画的一种效果
//sizeAnimation.value来获取大小,通过opacityAnimation.value来获取不透明度
final AnimationController controller =
new AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
final Animation<double> sizeAnimation =
new Tween(begin: 0.0, end: 300.0).animate(controller);
final Animation<double> opacityAnimation =
new Tween(begin: 0.1, end: 1.0).animate(controller);
交错动画
交错动画是:视觉变化发生在一系列操作中,而不是一次性发生。 动画可能是纯粹顺序的,在下一个动画之后会发生一次更改,或者可能部分或完全重叠。 它也可能有间隙,没有发生变化
- 交错动画由一个动画序列或重叠的动画组成。
- 要创建交错动画,需要使用多个动画对象。
- 一个AnimationController控制所有动画。
- 每个动画对象在间隔(Interval)期间指定动画。
- 对于要执行动画的每个属性都要创建一个Tween。
- 对于要设置动画的每个属性,创建一个Tween。
- 基本结构
- 所有动画都由同一个AnimationController驱动 。
- 无论动画实时持续多长时间,控制器的值必须介于0.0和1.0之间。
- 每个动画的 间隔 介于0.0和1.0之间。
- 对于在间隔中设置动画的每个属性,请创建一个 Tween。 Tween指定该属性的开始值和结束值。
- Tween生成一个 由控制器管理的 Animation对象。
// 在2000毫秒内完成,(0-0.1)透明动画(未显示----完全显示)----(间隔0.025)--->
//(0.125-0.250)宽度变大动画----------->(0.0250-0.375)高度变大动画同时携带padding变大动画--------->(0.375-0.500)圆角变大动画------->(0.500-0.750)颜色过渡动画
class StaggerAnimation extends StatelessWidget{
StaggerAnimation({Key key,this.controller}):
opacity = new Tween<double>(begin: 0.0, end: 1.0)
.animate(new CurvedAnimation(parent: controller, curve:new Interval(0.0, 0.100,curve: Curves.ease))),
width = new Tween<double>(begin: 50.0, end: 150.0)
.animate(new CurvedAnimation(parent: controller, curve: new Interval(0.125,0.250, curve: Curves.ease))),
height=new Tween<double>(begin:50.0,end: 150.0 )
.animate(new CurvedAnimation(parent: controller, curve: new Interval(0.250, 0.375,curve: Curves.ease))),
padding = new EdgeInsetsTween(begin: const EdgeInsets.only(bottom: 16.0),end: const EdgeInsets.only(bottom: 75.0))
.animate(new CurvedAnimation(parent: controller, curve: new Interval(0.250, 0.375,curve: Curves.ease))),
borderRadius = new BorderRadiusTween(begin: BorderRadius.circular(4.0),end: BorderRadius.circular(75.0))
.animate(new CurvedAnimation(parent: controller, curve: new Interval(0.375, 0.500,curve: Curves.ease))),
color = new ColorTween(begin: Colors.indigo[100],end: Colors.orange[400])
.animate(new CurvedAnimation(parent: controller, curve: new Interval(0.500, 0.750,curve: Curves.ease)))
,super(key:key);
final Animation<double> controller;
final Animation<double> opacity;
final Animation<double> width;
final Animation<double> height;
final Animation<EdgeInsets> padding;
final Animation<BorderRadius> borderRadius;
final Animation<Color> color;
Widget _AnimatedBuilder(BuildContext context,Widget child){
return new Container(
padding: padding.value,
alignment: Alignment.bottomCenter,
child: Opacity(opacity: opacity.value,
child: new Container(
width: width.value,
height: height.value,
decoration: BoxDecoration(
color: color.value,
border: Border.all(
color: Colors.indigo[300],
width: 3.0,
),
borderRadius: borderRadius.value,
),
// child: child,
),
),
);
}
@override
Widget build(BuildContext context) {
return new AnimatedBuilder(animation: controller, builder: _AnimatedBuilder);
}
}
AnimationController _controller;
@override
void initState(){
super.initState();
_controller = new AnimationController(vsync: this,duration: const Duration(milliseconds: 2000),)
..addStatusListener((status){
setState(() {
if(status==AnimationStatus.completed){
_controller.reverse();
}else if(status==AnimationStatus.dismissed){
_controller.forward();
}
});
});
}
...
child: new StaggerAnimation(controller: _controller.view,),
共享元素转换Hero动画
通常称为 共享元素转换 或 共享元素动画的动画风格。当PageRoute推或与弹出导航,整个屏幕的内容被替换。旧路线消失,出现新路线。如果两条路线上都有一个共同的视觉特征,那么在路线转换期间一个页面之间物理移动到另一个页面会很有帮助。这样的动画称为Hero动画。
- 路由和导航Route 和 Navigator
Route是应用程序的“屏幕”或“页面”的抽象(可以认为是Activity), Navigator是管理Route的Widget。Navigator可以通过push和pop route以实现页面切换。
和Android中可以在AndroidManifest.xml中声明Activities相似,在Flutter中,可以将具有指定Route的Map传递到顶层MaterialApp实例
- 使用命名路线进行导航
void main() {
runApp(new MaterialApp(
home: new MyAppHome(), // becomes the route named '/'
routes: <String, WidgetBuilder> {
'/a': (BuildContext context) => new MyPage(title: 'page A'),
'/b': (BuildContext context) => new MyPage(title: 'page B'),
'/c': (BuildContext context) => new MyPage(title: 'page C'),
},
));
}
...
//通过Navigator来切换到命名路由的页面。
Navigator.of(context).pushNamed('/b');
- 导航到新屏幕并返回
要切换到新路由,使用 Navigator.push() 方法。该push()方法将一个Route路由添加到由导航器管理的路由中。可以创建自己的Route,或使用 MaterialPageRoute开箱即用的。
class FristRoute extends StatelessWidget{
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('FristRoute'),
),
body: new Center(
child: new RaisedButton(onPressed: (){
Navigator.of(context).push(MaterialPageRoute(builder: (context)=> SecondRoute()),);
},
child: Text('ToSecondRoute'),),
),
);
}
}
class SecondRoute extends StatelessWidget{
@override
Widget build(BuildContext context) {
return new MaterialApp(
home: Scaffold(
appBar: AppBar(
title:Text('SecondRoute'),
),
body: new Center(
child: new RaisedButton(onPressed: (){
Navigator.of(context).pop() ;
},
child: Text('BackToFristRoute'),),
),
),
);
}
}
- 将数据发送到新屏幕
将数据通过构造传递给下一个页面
class Todo{//数据类
final String title;
final String description;
Todo(this.title,this.description);
}
Navigator.of(context).push(MaterialPageRoute(builder: (context)=>DetailScreen(todo: todos[index]),));
class DetailScreen extends StatelessWidget{
// Declare a field that holds the Todo
final Todo todo;
// In the constructor, require a Todo
DetailScreen({Key key, @required this.todo}) : super(key: key);
...
}
- 从屏幕返回数据:通过Navigator获取返回值
//result为上一页面返回的数据
_navigateAndDisplaySelection(BuildContext context) async{
final result = await Navigator.of(context).push(MaterialPageRoute(builder: (context){
return SelectionScreen();
}));
Scaffold.of(context)
..removeCurrentSnackBar()
..showSnackBar(SnackBar(content: Text("$result")));
}
//'Yep' 为返回的字符串
Navigator.of(context).pop('Yep');
- Hero小部件
要将窗口小部件标记为此类功能,将其包装在Hero窗口小部件中。当导航发生时,HeroController会识别每条路线上的Hero小部件。对于具有相同标签的每对Hero小部件,将触发 Hero动画。每个标记的路由不得包含多个Hero。 - 结构
- 定义一个起始 hero widget,称为*源 hero 。 hero 指定其图形表示(通常是图片)和识别标记,并且位于源路由定义的当前显示的 widget树中。定义一个起始 hero widget,称为源 hero *。 hero 指定其图形表示(通常是图片)和识别标记,并且位于源路由定义的当前显示的 widget树中。
- 定义一个结束的 hero widget,称为*目标 hero *。这位 hero 也指定了它的图形表示,以及与源 hero 相同的标记。重要的是两个 hero widget都使用相同的标签创建,通常是代表底层数据的对象。为了获得最佳效果, hero 应该有几乎相同的 widget树。
- 创建一个包含目标 hero 的路由。目标路由定义了动画结束时的 widget树。
通过导航器将目标路由入栈来触发动画。Navigator推送和弹出操作会为每对 hero 配对,并在源路由和目标路由中使用匹配的标签触发 hero 动画。 - 标准 hero 动画
在MaterialApp使用路由 home字段需要重新new class 一个页面
//重点是 :home: new FristPage()这句代码
class HeroAnimation extends StatelessWidget{
@override
Widget build(BuildContext context) {
timeDilation = 10.0;
return new MaterialApp(
routes: <String, WidgetBuilder>{
'/heronext': (BuildContext context) => new PhotoNextHero(),
},
home: new FristPage(),
);
}
}
class PhotoHero extends StatelessWidget{
PhotoHero({Key key,this.photo,this.onTap,this.width}):super(key:key);
final String photo;
final VoidCallback onTap ;
final double width;
@override
Widget build(BuildContext context) {
return new SizedBox(
child: new Hero(tag: photo,
child:new Material(
color: Colors.transparent,
child: new InkWell(
onTap: onTap,
child: new Image.asset(photo,
fit: BoxFit.contain,),
),
)
),
width: width,
);
}
}
class FristPage extends StatelessWidget{
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: const Text('Basic Hero Animation'),
),
body: new Center(
child: new PhotoHero(
photo: 'images/buttful.jpg',
width: 300.0,
onTap: (){
// Navigator.of(context).push(MaterialPageRoute<void>(builder: (context)=>new PhotoNextHero()));
Navigator.of(context).pushNamed('/heronext');
},
),
),
);
}
}
class PhotoNextHero extends StatelessWidget{
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: const Text('Flippers Page'),
),
body: new Container(
color: Colors.lightBlueAccent,
padding: const EdgeInsets.all(16.0),
alignment: Alignment.topLeft,
child: new PhotoHero(
photo: 'images/buttful.jpg',
width: 100.0,
onTap: (){
Navigator.of(context).pop();
},
),
),
);
}
}
- Radial hero 动画: 将 hero 从一个路由转移到另一个路由时,将其从圆形转换为矩形形状是平滑的效果,可以使用 hero widget实现。为了实现这一点,代码为两个剪辑形状:圆形和方形的相交部分提供动画。在整个动画中,圆形剪辑(和图片)从minRadius放大到maxRadius,而方形剪辑大小保持不变。同时,图片从源路由中的位置飞到目标路由中的位置。
//代码为两个剪辑形状:圆形和方形的相交部分提供动画。
//这是一个widget(child)的包装类
//最外层是圆形剪切
//需要过渡的图片外面是矩形剪切
@override
Widget build(BuildContext context) {
return new ClipOval(
child: new Center(
child: new SizedBox(
width: clipRectSize,
height: clipRectSize,
child: new ClipRect(
child: child,
),
),
),
);
}