每个平台对于动画的实现大同小异,手段大部分都是在60帧(部分Android机型90FPS,部分iPad是120FPS)的刷新频率下实现UI的多次变化,利用人眼视觉残留实现“流畅”的动作。
- 16 FPS: 比较流畅
- 32 FPS: 非常的细腻平滑
- 大于32 FPS: 人眼感受无差别
在理想状态下,Flutter 能够实现 60 FPS。(这里的刷新频率是否跟随硬件,找到资料再更新)
但是每种UI框架对动画的抽象方式都不一样,而 Flutter 中实现一个动画需要涉及到 Animation、Curve、Controller、Tween这四个角色。
动画分解
Animation
Animation是一个抽象类,从下面的Animation的部分源码可以看出,class Animation 仅定义了动画当前的值和状态,以及监听方法等内容,与UI展示样式等相关定义和属性无关(color, width 等)。
abstract class Animation<T> extends Listenable implements ValueListenable<T> {
const Animation();
@override
void addListener(VoidCallback listener);
@override
void removeListener(VoidCallback listener);
void addStatusListener(AnimationStatusListener listener);
void removeStatusListener(AnimationStatusListener listener);
AnimationStatus get status;
@override
T get value;
bool get isDismissed => status == AnimationStatus.dismissed;
bool get isCompleted => status == AnimationStatus.completed;
// 代码省略
// ...
}
AnimationController
继承自 abstract class Animation
类,用于控制动画的 进行(forward
)、停止(stop
)、反向播放(reverse
)。
默认情况下,AnimationController
对象会在动画的每一帧,按照动画曲线的规律生成 0 - 1 区间内的值。如下代码:
final AnimationController controller = new AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
这段代码的意义是:在2000毫秒(秒)内,随着帧刷新的频率,线性的生成 0 - 1 区间内的值。
其中 vsync 参数需要传入 TickerProvider 对象,用于屏幕刷新时的回调。
传入 TickerProvider 对象除了提供屏幕刷新回调以外,还可以防止 屏外动画的问题出现。当动画UI不在当前屏幕时,或者手机锁屏时,动画刷新会停止,避免消耗不必要的资源。
当然,动画如此常用的操作,Flutter绝不会让你花费很大的代价去实现,通常我们只要在 State 中 mixin SingleTickerProviderStateMixin 即可。 如下代码:
class _StaggerDemoState extends State<StaggerDemo> with TickerProviderStateMixin {
//...
}
动画的实现通常伴随着 AnimationControlelr
对象的 dispose
,势必需要借助 StatefullWidget
的声明周期方法dispose
。自然也会有对应的State
对象,所以这里不需要去纠结 StatelessWidget
是否可以使用 TickerProviderStateMixin
的疑问。
以上所讲的内容均是在 线性动画 下,如果想要执行非线性动画,则需要借助 Curve
的协助。
Curve
动画中使用 Curve
可以改变动画曲线,Curve
已经提供常用的动画曲线,下面列出部分枚举类:
Curves曲线 | 动画过程 - | - linear | 匀速的 decelerate | 匀减速 ease | 开始加速,后面减速 easeIn | 开始慢,后面快 easeOut | 开始快,后面慢 easeInOut | 开始慢,然后加速,最后再减速
除了已经提供好的 Curve
曲线,也可以自定义动画曲线,实现起来也很简单。下面是一个正弦曲线的实现:
class ShakeCurve extends Curve {
@override
double transform(double t) {
return math.sin(t * math.PI * 2);
}
}
从上面的代码可以看出,要实现一个自定义动画曲线,只需要重写 Curve
的 transform
方法即可。
如果要实现的动画效果不满足于0-1的区间的话,还可以借助 Tween
对象实现自定义动画区间。
Tween
提供开发自定义动画区间的能力。如下示例所示:
Tween<double>(begin: -200.0, end: 0.0);
Tween<Color>(begin: Colors.transparent, end: Colors.black54);
Tween<EdgeInsets>(begin: const EdgeInsets.only(left: .0), end: const EdgeInsets.only(left: 100.0)
这些均不是0 - 1 的区间。Color
的区间可以实现一个Color
到另一个Color
的渐变过渡。EdgeInsets
可以实现间距的渐变。
如此看起来,貌似Tween就已经可以实现0-1到任意区间的映射了,然而,事情绝不会如此顺利。
让我们来看一下 Tween的定义:
class Tween<T extends dynamic> extends Animatable<T> {
Tween({ this.begin, this.end });
T begin;
T end;
/// Returns the value this variable has at the given animation clock value.
///
/// The default implementation of this method uses the [+], [-], and [*]
/// operators on `T`. The [begin] and [end] properties must therefore be
/// non-null by the time this method is called.
@protected
T lerp(double t) {
assert(begin != null);
assert(end != null);
return begin + (end - begin) * t;
}
@override
T transform(double t) {
if (t == 0.0)
return begin;
if (t == 1.0)
return end;
return lerp(t);
}
@override
String toString() => '$runtimeType($begin \u2192 $end)';
}
从上面的源码可以看出,Tween
的定义十分的简单,绝不会支持所有类型的区间映射。lerp
函数已经告诉我们,类型 T
的对象,需要进行 + - *
三则运算,因此,能够运用 Tween
自动实现区间映射的对象,只能是实现了三则运算的对象,如上面例子中的 double
、EdgeInsets
,而Color
则不能直接使用Tween
实现区间映射。这时候就需要开发者自行实现double -> Color
类型的区间映射。
那我们就自己实现一个 ColorTween
:
class ColorTween extends Tween<Color> {
ColorTween({ Color begin, Color end }) : super(begin: begin, end: end);
@override
Color lerp(double t) => Color.lerp(begin, end, t);
}
我们利用 Color
对象的 lerp
方法很轻松的实现了 ColorTween
,只是重写了 Tween
类的 lerp
方法,返回区间映射的计算方法而已。类比ColorTween
的实现,其他类型的区间映射,也可以如此写。只要你喜欢,也可以实现正弦函数的区间映射关系。
Flutter 已经提供了一些线程的Tween子类给开发者使用:
- ColorTween
- IntTween
- ReverseTween
- SizeTween
- RectTween
- StepTween
- ConstantTween
动画的当前值,动画的执行,动画曲线,动画区间 都已经实现了,那下一步就是结合这四个部分,实现动画效果。
完整的动画创建过程
先来看一个完整的动画定义:
// 1. 创建动画控制类 AnimationController,用于执行动画
final AnimationController _controller = AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
// 2. 使用 CurvedAnimation 结合 动画控制类 和 动画曲线类,返回一个 「具有指定动画曲线的」「动画」 对象
fianl CurvedAnimation curvedAnimation = CurvedAnimation(
parent: _controller,
curve: Curves.ease,
);
// 3. 自定义区间 再次结合 CurvedAnimation,生成一个「具有指定区间值」和「指定动画曲线」的「动画」对象
final Animation<double> height = Tween<double>(begin: 0, end: 300.0).animate(curvedAnimation);
上述就是一个完整的动画创建过程,其中使用了一个新的 CurvedAnimation
类,从名字就可以看出,CurvedAnimation
和Curve
、Animation
都有关系,关系就是结合这两者,生成一个具有指定动画曲线的Animation
对象。
CurvedAnimation
继承自 Animation<T>
,同样,它可以标识动画的当前值,却不能控制动画的执行。
动画已经创建出来了,怎么根据创建好的动画构建出可以刷新的动画呢。
这里有主要的三种方式构建动画UI。有兴趣的可以看这篇文章。这里就直接上手最推荐使用AnimationedBuilder
。示例代码如下:
@override
Widget build(BuildContext context) {
return Center(
child: AnimatedBuilder(
animation: _controller,
builder: (BuildContext context, Widget _) => Container(
width: 200,
height: height.value,
color: Colors.red,
),
),
);
}
完整代码如下:
class BasicAnimation extends StatefulWidget {
@override
_BasicAnimationState createState() => _BasicAnimationState();
}
class _BasicAnimationState extends State<BasicAnimation> with SingleTickerProviderStateMixin {
AnimationController _animationController;
Animation<double> _height;
@override
void initState() {
_animationController = AnimationController(vsync: this, duration: Duration(seconds: 1));
_height = CurvedAnimation(parent: _animationController, curve: Curves.easeInOut);
_height = Tween<double>(begin: 0, end: 200).animate(_height);
super.initState();
_animationController.forward();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
width: 300,
height: 300,
color: Colors.grey,
alignment: Alignment.bottomCenter,
child: _buildAnimatedWidget,
),
));
}
Widget get _buildAnimatedWidget => AnimatedBuilder(
animation: _animationController,
builder: (BuildContext context, Widget child) {
print('build animation');
return Container(
width: 40,
height: _height.value,
color: Colors.red,
);
},
);
@override
void dispose() {
_animationController?.dispose();
super.dispose();
}
}
交织动画(组合动画)
类似于组动画,或者动画组的概念。同时或者交叉的执行多个动画。
下面我们实现一个:
- 前半部分高度从0->300,同时颜色从绿色到红色
- 后半部分往右平移
- 动画执行结束之后,反向执行,直到结束
的一个组合动画。
我们直接看代码(未完待续...)
其他动画
- AnimatedCrossFade
- 转场动画:Hero