每一个平台对于动画的实现大同小异,手段大部分都是在60帧(部分Android机型90FPS,部分iPad是120FPS)的刷新频率下实现UI的屡次变化,利用人眼视觉残留实现“流畅”的动做。html
在理想状态下,Flutter 可以实现 60 FPS。(这里的刷新频率是否跟随硬件,找到资料再更新)markdown
可是每种UI框架对动画的抽象方式都不同,而 Flutter 中实现一个动画须要涉及到 Animation、Curve、Controller、Tween这四个角色。框架
Animation是一个抽象类,从下面的Animation的部分源码能够看出,class Animation 仅定义了动画当前的值和状态,以及监听方法等内容,与UI展现样式等相关定义和属性无关(color, width 等)。less
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;
// 代码省略
// ...
}
复制代码
继承自 abstract class Animation
类,用于控制动画的 进行(forward
)、中止(stop
)、反向播放(reverse
)。ide
默认状况下,AnimationController
对象会在动画的每一帧,按照动画曲线的规律生成 0 - 1 区间内的值。以下代码:函数
final AnimationController controller = new AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
复制代码
这段代码的意义是:在2000毫秒(秒)内,随着帧刷新的频率,线性的生成 0 - 1 区间内的值。动画
其中 vsync 参数须要传入 TickerProvider 对象,用于屏幕刷新时的回调。ui
传入 TickerProvider 对象除了提供屏幕刷新回调之外,还能够防止 屏外动画的问题出现。当动画UI不在当前屏幕时,或者手机锁屏时,动画刷新会中止,避免消耗没必要要的资源。this
固然,动画如此经常使用的操做,Flutter毫不会让你花费很大的代价去实现,一般咱们只要在 State 中 mixin SingleTickerProviderStateMixin 便可。 以下代码:spa
class _StaggerDemoState extends State<StaggerDemo> with TickerProviderStateMixin {
//...
}
复制代码
动画的实现一般伴随着 AnimationControlelr
对象的 dispose
,势必须要借助 StatefullWidget
的声明周期方法dispose
。天然也会有对应的State
对象,因此这里不须要去纠结 StatelessWidget
是否可使用 TickerProviderStateMixin
的疑问。
以上所讲的内容均是在 线性动画 下,若是想要执行非线性动画,则须要借助 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<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子类给开发者使用:
动画的当前值,动画的执行,动画曲线,动画区间 都已经实现了,那下一步就是结合这四个部分,实现动画效果。
先来看一个完整的动画定义:
// 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();
}
}
复制代码
相似于组动画,或者动画组的概念。同时或者交叉的执行多个动画。
下面咱们实现一个:
的一个组合动画。
咱们直接看代码(未完待续...)