一个App中如果能有优秀的动画效果,能让App看起来显得更加高大上。此篇我们就来介绍一下Flutter中Animation体系。
-
我们先来一个简单的例子,来实现透明度渐变动画:
class FadeInDemo extends StatefulWidget { @override State createState() { return _FadeInDemoState(); } } class _FadeInDemoState extends State { double opacity = 0.0; @override Widget build(BuildContext context) { return Column( children: <Widget>[ MaterialButton( child: Text( "Click Me", style: TextStyle(color: Colors.blueAccent), ), onPressed: () => setState(() { opacity = 1; }), ), AnimatedOpacity( duration: const Duration(seconds: 2), opacity: opacity, child: Text('Flutter Animation Demo01') ) ], ); } }
这里我们借助于AnimatedOpacity来实现渐变:我们通过点击按钮来触发动画效果,如下图所示。
我们来总结一下实现步骤:
- 使用AnimatedOpacity来包裹需要实现透明度渐变动画的Widget,并指定duration和opacity参数。这俩参数也好理解:duration自然是动画时间,opacity表示透明度(取值范围为0~1,0表示透明)
- 触发动画:通过setState()方法,我们可以直接指定opacity的最终值(为1,即完全显示)。因为所谓的动画,肯定是有起始状态和结束状态,然后在指定的动画时间内慢慢发生变化。
2.使用AnimatedContainer来实现其他属性变化的动画.
class AnimatedContainerDemo extends StatefulWidget { @override State createState() { return _AnimatedContainerDemoState(); } } class _AnimatedContainerDemoState extends State<AnimatedContainerDemo> { Color color; double borderRadius; double margin; double randomBorderRadius() { return Random().nextDouble() * 64; } double randomMargin() { return Random().nextDouble() * 64; } Color randomColor() { return Color(0xFFFFFFFF & Random().nextInt(0xFFFFFFFF)); } void change() { setState(() { initState(); }); } @override void initState() { color = randomColor(); borderRadius = randomBorderRadius(); margin = randomMargin(); } @override Widget build(BuildContext context) { return Scaffold( body: Center( child: Column( children: <Widget>[ SizedBox( width: 128, height: 128, child: AnimatedContainer( curve: Curves.easeInOutBack, duration: const Duration(milliseconds: 400), margin: EdgeInsets.all(margin), decoration: BoxDecoration( color: color, borderRadius: BorderRadius.circular(borderRadius), ), ), ), MaterialButton( color: Theme.of(context).primaryColor, child: Text( 'change', style: TextStyle(color: Colors.white), ), onPressed: () => change(), ) ], ), ), ); } }
运行效果如下:
我们这次同时修改了margin、color以及borderRadius三个属性。AnimatedContainer的使用思路和AnimatedOpacity类似:
- 包裹子widget,指定duration及相关属性参数
- 在setState方法中指定属性的动画终止状态
实际上我们刚刚介绍的两种实现方式被称之为隐式动画(implicit animation),可以理解成对于Animation子系统进行了一层封装,方便我们开发者使用。下面我们正式来介绍Animation子系统的重要组成类:
3.Animation类:通过这个类,我们可以知道当前的动画值(animation.value)以及当前的状态(通过设置对应的监听listener),但是对于屏幕上如何显示、widget如何渲染,它是不知道的,换句话说也不是它所关心的,这点从软件设计上耦合性也更低。其次,从代码角度上,它是一个抽象类:
4.AnimationController类。
从类本身上看,它是继承自Animation类的,并且泛型类型为double。从作用上来看,我们可以通过AnimationController来指定动画时长,以及它提供的forward()、reverse()方法来触发动画的执行。
5.CurvedAnimation类:同样继承自Animation类,并且泛型类型为double。它主要用来描述非线性变化的动画,有点类似Android中的属性动画的插值器。
6.Tween类.
从类的层次结构上,它有所不同,不再是继承自Animation,而是继承自Animatable。它主要用来指定起始状态和终止状态的。
好,我们已经对这四个类有了一定的了解,下面我们就从实例来看看他们是如何结合在一起使用的。
7.实例一:
class LogoDemo extends StatefulWidget {
@override
State createState() => _LogoState();
}
class _LogoState extends State with SingleTickerProviderStateMixin {
// 注释1:这里已经出现了我们前面提到的Animation和AnimationController类
Animation<double> animation;
AnimationController controller;
@override
void initState() {
super.initState();
// 注释2:
// 在构造一个AnimationController对象时,我们需要传递两个参数
// vsync:主要为了防止一些超出屏幕之外的动画而导致的不必要的资源消耗。我们这里就传递
// this,除此之外,我们还需要使用with关键字来接入SingleTickerProviderStateMixin类型
// duration:指定动画时长
controller =
AnimationController(vsync: this, duration: const Duration(seconds: 2));
// 注释3: 通过Tween对象来指定起始值和终止值,并且通过animate方法返回一个Animation对象,
// 并且设置了监听,最后在监听回调中调用setState(),从而刷新页面
animation = Tween<double>(begin: 0, end: 300).animate(controller)
..addListener(() {
setState(() {});
});
// 注释4: 启动动画
controller.forward();
}
@override
Widget build(BuildContext context) {
return Center(
child: Container(
margin: EdgeInsets.symmetric(vertical: 10),
// 注释5:通过Animation对象类获取对应的变化值
height: animation.value,
width: animation.value,
child: FlutterLogo(),
),
);
}
@override
void dispose() {
// 注释6:对controller解注册
controller.dispose();
super.dispose();
}
}
运行效果如图所示:
这里涉及到了两个Dart语言本身的知识点:mixin和..。mixin这里推荐一篇medium上的文章:https://medium.com/flutter-community/dart-what-are-mixins-3a72344011f3;而..很简单,它就是为了方便链式调用。我们可以看下前面addListener()方法,方法本身返回的void类型,但是我们最终却返回了一个Animation类型的对象。这其中就是..起到的作用,它可以使得方法返回调用这个方法的对象本身。
8.使用AnimatedWidget来重构上面的代码:
class LogoDemo extends StatefulWidget {
@override
State createState() => _LogoState();
}
class _LogoState extends State with SingleTickerProviderStateMixin {
Animation<double> animation;
AnimationController controller;
@override
void initState() {
super.initState();
controller =
AnimationController(vsync: this, duration: const Duration(seconds: 2));
animation = Tween<double>(begin: 0, end: 300).animate(controller)
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller.reverse();
} else if (status == AnimationStatus.dismissed) {
controller.forward();
}
print('$status');
});
controller.forward();
}
@override
Widget build(BuildContext context) {
return AnimatedLogo(animation: animation);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
class AnimatedLogo extends AnimatedWidget {
AnimatedLogo({Key key, Animation<double> animation})
: super(key: key, listenable: animation);
@override
Widget build(BuildContext context) {
final animation = listenable as Animation;
return Center(
child: Container(
margin: EdgeInsets.symmetric(vertical: 10),
height: animation.value,
width: animation.value,
child: FlutterLogo(),
),
);
}
}
先看效果:
大部分代码是和之前的例子是一样的,不同的是:
- 使用AnimatedWidget,并且Animation对象作为参数传递进来
- 省略了在addListener的回调里调用setState方法来触发页面刷新
这样写的好处:
- 省去了需要调用setState的重复代码
- 使得程序耦合性更低。试想一下,我们的App中有多处都需要实现Logo的resize动画,这个时候我们只需要在使用处定义Animation的描述,最后都使用这里的AnimatedLogo。这样做就使得Widget和Animation的描述进行分离。
我们可以去看一下AnimatedWidget类的源码:
abstract class AnimatedWidget extends StatefulWidget {
/// Creates a widget that rebuilds when the given listenable changes.
///
/// The [listenable] argument is required.
const AnimatedWidget({
Key key,
@required this.listenable,
}) : assert(listenable != null),
super(key: key);
/// The [Listenable] to which this widget is listening.
///
/// Commonly an [Animation] or a [ChangeNotifier].
final Listenable listenable;
/// Override this method to build widgets that depend on the state of the
/// listenable (e.g., the current value of the animation).
@protected
Widget build(BuildContext context);
/// Subclasses typically do not override this method.
@override
_AnimatedState createState() => _AnimatedState();
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<Listenable>('animation', listenable));
}
}
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.
});
}
@override
Widget build(BuildContext context) => widget.build(context);
}
可以看到在initState方法里,添加了动画监听,回调的执行逻辑为_handleChange()方法,而_handleChange()的实现就是调用了setState方法,这点和我们之前在第7条中例子的写法一样。也就是说,AnimatedWidget只是做了一层封装而已。
9.使用AnimatedBuilder进一步重构上面的代码:
class LogoDemo extends StatefulWidget {
@override
State createState() => _LogoState();
}
class _LogoState extends State with SingleTickerProviderStateMixin {
Animation<double> animation;
AnimationController controller;
@override
void initState() {
super.initState();
controller =
AnimationController(vsync: this, duration: const Duration(seconds: 2));
animation = Tween<double>(begin: 0, end: 300).animate(controller);
controller.forward();
}
@override
Widget build(BuildContext context) {
return GrowTransition(child: LogoWidget(), animation: animation);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
class LogoWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.symmetric(vertical: 10),
child: FlutterLogo(),
);
}
}
class GrowTransition extends StatelessWidget {
GrowTransition({this.child, this.animation});
final Widget child;
final Animation<double> animation;
@override
Widget build(BuildContext context) {
return Center(
child: AnimatedBuilder(
animation: animation,
builder: (context, child) => Container(
height: animation.value,
width: animation.value,
child: child,
),
child: child),
);
}
}
我们可以在任意地方使用这里GrowTransition,代码进行进一步分离。
10.使用Transition。
Flutter还为我们提供一些封装好的Transition,方便我们实现动画效果,下面我们就以ScaleTransition为例,说明如何去使用这些Transition。
class ScaleTransitionDemo extends StatefulWidget {
@override
State createState() => _LogoState();
}
class _LogoState extends State with SingleTickerProviderStateMixin {
Animation<double> animation;
AnimationController controller;
@override
void initState() {
super.initState();
controller =
AnimationController(vsync: this, duration: const Duration(seconds: 2));
animation = Tween<double>(begin: 0, end: 10).animate(controller);
controller.forward();
}
@override
Widget build(BuildContext context) {
return Center(
child: ScaleTransition(
scale: animation,
child: FlutterLogo(),
));
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
ScaleTransition在使用时需要指定两个参数:
- scale: 就是一个Animation对象
- child: 需要实现缩放动画的widget
最后再注意一下Tween中指定的值所表示的含义,它表示的倍数。比如我们这里end填入了10,表示动画结束状态为放大10倍。我们可以通过ScaleTransition的源码来说服大家:
class ScaleTransition extends AnimatedWidget {
/// Creates a scale transition.
///
/// The [scale] argument must not be null. The [alignment] argument defaults
/// to [Alignment.center].
const ScaleTransition({
Key key,
@required Animation<double> scale,
this.alignment = Alignment.center,
this.child,
}) : assert(scale != null),
super(key: key, listenable: scale);
/// The animation that controls the scale of the child.
///
/// If the current value of the scale animation is v, the child will be
/// painted v times its normal size.
Animation<double> get scale => listenable;
/// The alignment of the origin of the coordinate system in which the scale
/// takes place, relative to the size of the box.
///
/// For example, to set the origin of the scale to bottom middle, you can use
/// an alignment of (0.0, 1.0).
final Alignment alignment;
/// The widget below this widget in the tree.
///
/// {@macro flutter.widgets.child}
final Widget child;
@override
Widget build(BuildContext context) {
final double scaleValue = scale.value;
final Matrix4 transform = Matrix4.identity()
..scale(scaleValue, scaleValue, 1.0);
return Transform(
transform: transform,
alignment: alignment,
child: child,
);
}
}
可以看到,ScaleTransition也是继承自我们前面已经介绍过的AnimatedWidget,然后重点关注build()方法里,用到了Matrix4矩阵,这里的scale.value实际上就是Animation.value,而Matrix4.identity()..scale(),它的三个参数分别表示在x轴、y轴以及z轴上缩放的倍数。
与ScaleTransition类似的还有SlideTransition、RotationTransition等等,读者可以自己去尝试一下,这里就不在一一赘述了。