Flutter中的动画系统基于类型化 Animation 对象。小部件可以直接通过读取它们的当前值并监听它们的状态变化将这些动画合并到它们的构建函数中,或者它们可以使用动画作为它们传递给其他小部件的更精细动画的基础。个人感觉Flutter中的动画和Android中的属性动画有点相似,作用于widget的属性值,既然有了参照物了,理解起来就没那么难了。
在补间动画中,定义了开始点和结束点、时间线以及定义转换时间和速度的曲线。然后由框架计算如何从开始点过渡到结束点。
在基于物理的动画中,运动被模拟为与真实世界的行为相似。例如,当你掷球时,它在何处落地,取决于抛球速度有多快、球有多重、距离地面有多远。 类似地,将连接在弹簧上的球落下(并弹起)与连接到绳子上的球放下的方式也是不同。
在Flutter中,Animation对象本身和UI渲染没有任何关系。Animation是一个抽象类,它拥有其当前值和状态(完成或停止)。其中一个比较常用的Animation类是Animation< double >。
要创建动画,首先要创建一个 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);
class MyAnimationControllerState extends State with TickerProviderStateMixin{
//创建一个Animation对象,但不会启动它运行:
final AnimationController controller = new AnimationController(
duration: const Duration(milliseconds: 2000), vsync: this);
}
常用方法
将曲线应用于另一个动画。是对该动画的包装,我的理解是Android中的插值器
当想要将非线性曲线应用于动画对象时,CurvedAnimation很有用,特别是如果想要在动画前进时与后退时使用不同的曲线。
根据给定的曲线,CurvedAnimation的输出可能比其输入具有更宽的范围。例如,弹性曲线(如 Curves.elasticIn)将显着超过或低于0.0到1.0的默认范围。
AnimationController controller = new AnimationController(vsync: this,duration: Duration(milliseconds:3000));
//立方体动画曲线,缓慢开始并快速结束(easeIn)。
final CurvedAnimation curve =
new CurvedAnimation(parent: controller, curve: Curves.easeIn);
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());
默认情况下,AnimationController对象的范围从0.0到1.0。如果需要不同的范围或不同的数据类型,则可以使用Tween来配置动画以生成不同的范围或数据类型的值。
//Tween生成从-200.0到0.0的值:
final Tween doubleTween = new Tween(begin: -200.0, end: 0.0);
//ColorTween指定两种颜色之间的过渡。
final Tween colorTween =
new ColorTween(begin: Colors.transparent, end: Colors.black54);
//500毫秒内生成从0到255的整数值。
final AnimationController controller = new AnimationController(
duration: const Duration(milliseconds: 500), vsync: this);
Animation 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 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类允许从setState()调用中的动画代码中分离出widget代码。AnimatedWidget不需要维护一个State对象来保存动画。AnimatedWidget(基类)中会自动调用addListener()和setState()。
//AnimatedWidget的构造函数只接受一个动画对象。 为了解决这个问题,可以创建了自己的Tween对象并显式计算了这些值。
class AnimateLogo extends AnimatedWidget {
static final _opacityTween = new Tween(begin: 0.1, end: 1.0);
static final _sizeTween = new Tween(begin: 0.0, end: 300.0);
AnimateLogo({Key key, Animation animation})
: super(key: key, listenable: animation);
@override
Widget build(BuildContext context) {
final Animation 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(),
),
),
);
}
}
问题:AnimatedWidget更改动画需要更改显示l的widget,解决方案是将职责分离:(1.显示widget 2.定义Animation对象 3.渲染过渡效果)可以借助AnimatedBuilder类完成此分离。AnimatedBuilder是渲染树中的一个独立的类。 与AnimatedWidget类似,AnimatedBuilder自动监听来自Animation对象的通知,并根据需要将该控件树标记为脏(dirty),因此不需要手动调用addListener()。
class Spinner extends StatefulWidget {
@override
_SpinnerState createState() => _SpinnerState();
}
//使用AnimatedBuilder构建,并使用 builder功能来避免每次都重建Container。
class _SpinnerState extends State 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 sizeAnimation =
new Tween(begin: 0.0, end: 300.0).animate(controller);
final Animation opacityAnimation =
new Tween(begin: 0.1, end: 1.0).animate(controller);
交错动画是:视觉变化发生在一系列操作中,而不是一次性发生。 动画可能是纯粹顺序的,在下一个动画之后会发生一次更改,或者可能部分或完全重叠。 它也可能有间隙,没有发生变化
// 在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(begin: 0.0, end: 1.0)
.animate(new CurvedAnimation(parent: controller, curve:new Interval(0.0, 0.100,curve: Curves.ease))),
width = new Tween(begin: 50.0, end: 150.0)
.animate(new CurvedAnimation(parent: controller, curve: new Interval(0.125,0.250, curve: Curves.ease))),
height=new Tween(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 controller;
final Animation opacity;
final Animation width;
final Animation height;
final Animation padding;
final Animation borderRadius;
final Animation 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,),
通常称为 共享元素转换 或 共享元素动画的动画风格。当PageRoute推或与弹出导航,整个屏幕的内容被替换。旧路线消失,出现新路线。如果两条路线上都有一个共同的视觉特征,那么在路线转换期间一个页面之间物理移动到另一个页面会很有帮助。这样的动画称为Hero动画。
void main() {
runApp(new MaterialApp(
home: new MyAppHome(), // becomes the route named '/'
routes: {
'/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');
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);
...
}
//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');
//重点是 :home: new FristPage()这句代码
class HeroAnimation extends StatelessWidget{
@override
Widget build(BuildContext context) {
timeDilation = 10.0;
return new MaterialApp(
routes: {
'/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(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();
},
),
),
);
}
}
//代码为两个剪辑形状:圆形和方形的相交部分提供动画。
//这是一个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,
),
),
),
);
}