作为一个移动端UI框架,Flutter 也拥有自己的动画体系。
分类
Flutter 动画分为两类:补间动画(Tween)和 基于物理的动画。
本文主要介绍第一类动画。
动画的基本类
Animation<T>
Animation是一个抽象的类,主要保存动画的状态和当前值。最常用的Animation类是Animation<double>
T 有很多类型,如Color、Offset。后面会详细介绍
可以通过Animation中的 value 属性获得当前动画的值。
动画的监听:
- addListener() 每一帧动画执行的监听
- addStatusListener() 动画状态改变的监听。有下面四种状态
AnimationController
AnimationController 继承Animation<double>,负责控制动画的执行,停止等。
AnimationController 会在动画的每一帧,就会生成一个新的值。默认情况下,AnimationController在给定的时间段内线性的生成从0.0到1.0(默认区间)的数字。
创建 AnimationController,则需要传入一个 vsync 参数。
Tween
默认情况下,AnimationController对象的范围从0.0到1.0。
如果你需要不同范围或者不同的数据类型,就需要tween来配置动画以生成不同的范围或数据类型的值
Tween的子类如下图所示:
例子
class PageState extends State<HomePage> with SingleTickerProviderStateMixin {
AnimationController controller;
//doubler类型动画
Animation<double> doubleAnimation;
//颜色动画
Animation<Color> colorAnimation;
//位置动画
Animation<Offset> offsetAnimation;
//圆角动画
Animation<BorderRadius> radiusAnimation;
//装饰动画
Animation<Decoration> decorationAnimation;
//字体动画
Animation<TextStyle> textStyleAnimation;
@override
void initState() {
// TODO: implement initState
super.initState();
//创建AnimationController
controller = new AnimationController(
vsync: this, duration: Duration(milliseconds: 2000));
//animation第一种创建方式:
doubleAnimation = new Tween<double>(begin: 0.0, end: 200.0).animate(controller)
..addListener(() {
setState(() {});
})
..addStatusListener((AnimationStatus status) {
//执行完成后反向执行
if (status == AnimationStatus.completed) {
controller.reverse();
} else if (status == AnimationStatus.dismissed) {
//反向执行完成,正向执行
controller.forward();
}
});
//animation第二种创建方式:
offsetAnimation = controller.drive(
Tween<Offset>(begin: Offset(0.0, 0.0),end: Offset(400.0, 200.0))
);
colorAnimation = ColorTween(begin: Colors.yellow,end: Colors.red).animate(controller);
radiusAnimation = BorderRadiusTween(begin: BorderRadius.circular(0),end: BorderRadius.circular(50)).animate(controller);
decorationAnimation = DecorationTween(begin: BoxDecoration(color: Colors.purple,borderRadius: BorderRadius.circular(0),),
end: BoxDecoration(color: Colors.lightBlueAccent,borderRadius: BorderRadius.circular(40))).animate(controller);
textStyleAnimation = TextStyleTween(begin: TextStyle(color: Colors.black,fontSize: 20,fontWeight: FontWeight.w100),
end: TextStyle(color: Colors.purple,fontSize: 30,fontWeight: FontWeight.w700)).animate(controller);
//启动动画
controller.forward();
}
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
appBar: AppBar(title: Text("Tween动画"),),
body: Container(
alignment: Alignment.center,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
SizedBox(
height: 200,
child: Container(
height: doubleAnimation.value,
width: doubleAnimation.value,
child: FlutterLogo(),
),
),
Container(
margin: EdgeInsets.only(left: offsetAnimation.value.dx),
width: 50,
height: 50,
color: Colors.green,
),
Container(
height: 100,
width: 100,
color: colorAnimation.value,
),
SizedBox(height: 10,),
Container(
height: 100,
width: 100,
decoration: BoxDecoration(borderRadius: radiusAnimation.value,color: Colors.blue),
),
Container(
height: 60,
width: 200,
decoration: decorationAnimation.value,
),
Container(
height: 100,
child: Text("TestStyleTween",style: textStyleAnimation.value,),
),
],
),
)
);
}
@override
void dispose() {
// TODO: implement dispose
super.dispose();
controller.dispose();
}
}
tween动画执行效果图
这里列举里几种比较简单的Tween动画。在上面的代码中我们通过对animation设置addListener()对每一帧的变化进行监听,当animation 中的value插值发生改变时调用 setState(() {});刷新布局,从而达到动画过度的效果。
当我们state中的布局复杂的时候,我们在每一帧变化的时候都调用setState来刷新widget树,会把state中所有的widget都重新绘制,这样就会造成不必要的性能消耗,我们只需要刷新执行动画的那个widget就行了。Flutter为我们提供了AnimatedWidget。
AnimatedWidget
源码
abstract class AnimatedWidget extends StatefulWidget {
//创建一个widget,当listenable 发生改变时重构
const AnimatedWidget({
Key key,
@required this.listenable,
}) : assert(listenable != null),
super(key: key);
//声明一个Listenable ,帧动画监听
final Listenable listenable;
@protected
Widget build(BuildContext context);
/// Subclasses typically do not override this method.
@override
_AnimatedState createState() => _AnimatedState();
...
}
AnimatedWidget继承自StatefulWidget,拥有自己的状态,并且实例一个listenable用来监听帧动画,当动画方法变化时,刷新AnimatedWidget。
__AnimatedState
方法源码如下:
class _AnimatedState extends State<AnimatedWidget> {
@override
void initState() {
super.initState();
//添加监听的回调
widget.listenable.addListener(_handleChange);
}
@override
void didUpdateWidget(AnimatedWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.listenable != oldWidget.listenable) {
oldWidget.listenable.removeListener(_handleChange);
widget.listenable.addListener(_handleChange);
}
}
@override
void dispose() {
widget.listenable.removeListener(_handleChange);
super.dispose();
}
//当帧动画发生改变时触发刷新
void _handleChange() {
setState(() {
// The listenable's state is our build state, and it changed already.
});
}
//调用build()方法,重构AnimatedWidget
@override
Widget build(BuildContext context) => widget.build(context);
}
当listenable触发刷新的时候,调用 setState
重构AnimatedWidget,虽然到最后还是调用setState,但是刷新的对象是不同的。
Listenable
从源码中可以看到在AnimatedWidget声明了一个Listenable,用来监听每一帧的变化。那么Listenable又是啥呢。我们可以看一小截Animation的源码:
可以看到Animation也是继承自Listenable,Listenable 源码如下:
abstract class Listenable {
const Listenable();
factory Listenable.merge(List<Listenable> listenables) = _MergingListenable;
void addListener(VoidCallback listener);
void removeListener(VoidCallback listener);
}
所以Animation也是一个Listenable 实现类。源码看完,具体用法如下:
ColorAnimationWidget
声明一个ColorAnimationWidget类,继承自AnimatedWidget。代码如下:
class ColorAnimationWidget extends AnimatedWidget{
ColorAnimationWidget({Key key, Animation<Color> animation})
: super(key: key, listenable: animation);
@override
Widget build(BuildContext context) {
final Animation<Color> animation = listenable;
// TODO: implement build
return Center(
child: Container(
width: 200,
height: 200,
color: animation.value,
),
);
}
}
使用ColorAnimationWidget
class PageState extends State<HomePage> with SingleTickerProviderStateMixin{
AnimationController _controller;
Animation<Color> _animation;
@override
void initState() {
// TODO: implement initState
super.initState();
_controller = AnimationController(vsync: this,duration: Duration(seconds: 2));
_animation = ColorTween(begin: Colors.lightBlueAccent,end: Colors.red).animate(_controller)
..addStatusListener((AnimationStatus status){
if(status == AnimationStatus.completed){
_controller.reverse();
}else if(status == AnimationStatus.dismissed){
_controller.forward();
}
});
_controller.forward();
}
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
body: ColorAnimationWidget(animation: _animation,),
);
}
}
class ColorAnimationWidget extends AnimatedWidget{
ColorAnimationWidget({Key key, Animation<Color> animation})
: super(key: key, listenable: animation);
@override
Widget build(BuildContext context) {
final Animation<Color> animation = listenable;
// TODO: implement build
return Center(
child: Container(
width: 200,
height: 200,
color: animation.value,
),
);
}
}
可以看到用法基本和不使用AnimatedWidget一样,唯一的区别就是使用AnimatedWidget时setState
是在AnimatedWidget内调用的,只刷新一个widget。
ColorAnimationWidget效果图:
CurvedAnimation
Tween动画默认为我们提供了区间内线性变化,如果我们需要曲线变化,则需要配合使用CurvedAnimation。如下图弹性效果:
用法如下:
class HomePageState extends State<HomePage> with TickerProviderStateMixin{
Animation animation;
AnimationController controller;
@override
void initState() {
// TODO: implement initState
super.initState();
controller = new AnimationController(vsync: this,duration: Duration(seconds: 2));
CurvedAnimation curve = new CurvedAnimation(parent: controller, curve: Curves.bounceOut);
animation = Tween<double>(begin: 0.0,end: 500).animate(curve)
..addListener((){
setState(() {
});
});
controller.forward();
}
@override
Widget build(BuildContext context) {
// TODO: implement build
return Container(
alignment: Alignment.topCenter,
child: Container(
margin: EdgeInsets.only(top: animation.value),
width: 100,
height: 100,
child: FlutterLogo(),
),
);
}
}
CurvedAnimation curve = new CurvedAnimation(parent: controller, curve: Curves.bounceOut);
这里我们使用的是bounceOut
效果
更多的曲线效果可以查看官网的效果图:Curves
我们看一下bounceOut
是如何实现的:
class _BounceOutCurve extends Curve {
const _BounceOutCurve._();
@override
double transformInternal(double t) {
return _bounce(t);
}
}
double _bounce(double t) {
if (t < 1.0 / 2.75) {
return 7.5625 * t * t;
} else if (t < 2 / 2.75) {
t -= 1.5 / 2.75;
return 7.5625 * t * t + 0.75;
} else if (t < 2.5 / 2.75) {
t -= 2.25 / 2.75;
return 7.5625 * t * t + 0.9375;
}
t -= 2.625 / 2.75;
return 7.5625 * t * t + 0.984375;
}
可以看到bounceOut 是继承 Curve类,实现它的transformInternal方法,在transformInternal实现它的轨迹。
AnimatedBuilder
上面我们实现了一个颜色变化的例子,假如我们现在需要实现一个大小变化的widget呢?是不是要在声明一个SizeAnimationWidget继承AnimationWidget ?
显然这样做是可以的。当然我们也有更好的做法,就是使用AnimatedBuilder来重构我们的widget。
什么是AnimatedBuilder
AnimatedBuilder继承自抽象的AnimationWidget ,目的为了构建通用的AnimationWidget 实现类,不用每次使用AnimationWidget 都要创建一个实现类。
AnimatedBuilder源码:
class AnimatedBuilder extends AnimatedWidget {
/// Creates an animated builder.
const AnimatedBuilder({
Key key,
@required Listenable animation,
@required this.builder,
this.child,
}) : assert(animation != null),
assert(builder != null),
super(key: key, listenable: animation);
final TransitionBuilder builder;
final Widget child;
@override
Widget build(BuildContext context) {
return builder(context, child);
}
}
使用的时候我们只需要传入animation和builder就行了
用法
class _LogoAppState extends State<LogoApp> with TickerProviderStateMixin {
Animation animation;
AnimationController controller;
initState() {
super.initState();
controller = new AnimationController(
duration: const Duration(milliseconds: 2000), vsync: this);
final CurvedAnimation curve =
new CurvedAnimation(parent: controller, curve: Curves.bounceOut);
animation = new Tween(begin: 0.0, end: 300.0).animate(curve);
controller.forward();
}
Widget build(BuildContext context) {
return new GrowTransition(child: new LogoWidget(), animation: animation);
}
dispose() {
controller.dispose();
super.dispose();
}
}
class GrowTransition extends StatelessWidget {
GrowTransition({this.child, this.animation});
final Widget child;
final Animation<double> animation;
Widget build(BuildContext context) {
return new Center(
child: new AnimatedBuilder(
animation: animation,
builder: (BuildContext context, Widget child) {
return new Container(
height: animation.value, width: animation.value, child: child);
},
child: child),
);
}
}
class LogoWidget extends StatelessWidget {
// Leave out the height and width so it fills the animating parent
build(BuildContext context) {
return new Container(
margin: new EdgeInsets.symmetric(vertical: 10.0),
child: new FlutterLogo(),
);
}
}
效果图
总结
tween动画到这里就结束了,Hero动画留下一次在补充吧
参考:Flutter动画教程