Flutter 自定义 ImplicitlyAnimatedWidget 及源码解析

前言

Flutter 在刚出来的时候学习过一下,之后就没有碰过了,感觉东西太少,那时候我也没还没写前端,不习惯这种方式写代码;但是最近疫情关系,有时候没事干开始看 Flutter,也喜欢上了,下面来介绍一下我最喜欢的 ImplicitlyAnimatedWidget(隐式动画组件),如何自定义和源码解读。

什么是 ImplicitlyAnimation

在 Flutter 里面,动画分成 ExplicitAnimation(显示动画)和 ImplicitlAnimation(隐式动画),如果你希望通过 setState 的方式就可以改变 widget 状态,同时增加过度动画,比如我希望 widget 变成透明,然后是慢慢透明的。而如果你希望用 controller 来开始动画,或者有其他动画一起配合,可能就需要 ExplicitAnimation,请注意这里需要你自己去管理生命周期,比较麻烦。

官方视频里面提到,如果:

  • 我的动画永远重复吗?比如很多播放器都会有一直旋转的黑胶唱片的元素。
  • 动画不是连续的吗?比如只会从小到大的动画,而不会有大到小的动画。
  • 会有很多 widget 配合动画吗?

如果这需要任意一个,请选择 ExpicitAnimatin

源码解析

源码分为两部分,ImplicitlyAnimatedWidgetImplicitlyAnimatedWidgetState 下面分辨解析

Widget

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
abstract class ImplicitlyAnimatedWidget extends StatefulWidget {
const ImplicitlyAnimatedWidget({
Key key,
this.curve = Curves.linear,
@required this.duration,
this.onEnd,
}) : assert(curve != null),
assert(duration != null),
super(key: key);

final Curve curve;

final Duration duration;

final VoidCallback onEnd;

@override
ImplicitlyAnimatedWidgetState<ImplicitlyAnimatedWidget> createState();

@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(IntProperty('duration', duration.inMilliseconds, unit: 'ms'));
}
}

State

ImplicitlyAnimatedWidget 其实很简单,先定义了 curve(动画曲线), duration(时间), onEnd(结束会调),没什么太多内容,接下来看看核心 ImplicitlyAnimatedWidgetState,但是由于代码过多,一部分一部分来:

1
2
3
4
5
6
@protected
AnimationController get controller => _controller;
AnimationController _controller;

Animation<double> get animation => _animation;
Animation<double> _animation;

默认定义了 _controller, _animation,接下来看 initState

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
@override
void initState() {
super.initState();
// 初始化 _controller
_controller = AnimationController(
duration: widget.duration,
debugLabel: kDebugMode ? '${widget.toStringShort()}' : null,
vsync: this,
);
_controller.addStatusListener((AnimationStatus status) {
switch (status) {
// 动画完成后的会调
case AnimationStatus.completed:
if (widget.onEnd != null)
widget.onEnd();
break;
case AnimationStatus.dismissed:
case AnimationStatus.forward:
case AnimationStatus.reverse:
}
});
_updateCurve();
_constructTweens();
didUpdateTweens();
}

// 检查 curve 初始化或更新 _animation
void _updateCurve() {
if (widget.curve != null)
_animation = CurvedAnimation(parent: _controller, curve: widget.curve);
else
_animation = _controller;
}

// 初始化或者更新 Tween
bool _constructTweens() {
bool shouldStartAnimation = false;
// 这就是我们平时使用覆盖的方法,一般我们覆盖实现的时候是这样的:void forEachTween(visitor)
forEachTween((Tween<dynamic> tween, dynamic targetValue, TweenConstructor<dynamic> constructor) {
if (targetValue != null) {
tween ??= constructor(targetValue);
if (_shouldAnimateTween(tween, targetValue))
// 是否需要开始做动画,这里注意一点,visitor 最后一次调用决定了是否做动画
shouldStartAnimation = true;
} else {
tween = null;
}
return tween;
});
return shouldStartAnimation;
}

// 判断是否需要做动画,下文详解
bool _shouldAnimateTween(Tween<dynamic> tween, dynamic targetValue) {
return targetValue != (tween.end ?? tween.begin);
}

// 回调
@protected
void didUpdateTweens() { }

initState 就是这样了,流程大概就是这样,初始化 _controller_animation,然后创建 tween,注意的点就是,因为 shouldStartAnimation 是取决于最后一次调用 visitor,所以如果我们有多个:

1
2
3
4
5
6
7
@override
void forEachTween(visitor) {
_colorTween = visitor(
_colorTween, widget.color, (dynamic value) => ColorTween(begin: value));
_sizeTween = visitor(
_sizeTween, widget.size, (dynamic value) => SizeTween(begin: value));
}

shouldStartAnimation 是取决 size 的调用。

update 情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@override
void didUpdateWidget(T oldWidget) {
super.didUpdateWidget(oldWidget);
// 老规矩,更新 curve, duration
if (widget.curve != oldWidget.curve)
_updateCurve();
_controller.duration = widget.duration;
// 这里调用了 initState 使用过的 _constructTweens 方法,由于我们在 initState 已经创建好了 tween,所以,走到 _shouldAnimateTween(tween, targetValue) 会判断最后一个 visitor, targetValue 和 tween.end 不一致则动画开始。
if (_constructTweens()) {
forEachTween((Tween<dynamic> tween, dynamic targetValue, TweenConstructor<dynamic> constructor) {
_updateTween(tween, targetValue);
return tween;
});
// 最后设置动画开始
_controller
..value = 0.0
..forward();
// 回调
didUpdateTweens();
}
}

// 判断动画可以开始之后,设置 end 和目前的 beign 值。
void _updateTween(Tween<dynamic> tween, dynamic targetValue) {
if (tween == null)
return;
tween
..begin = tween.evaluate(_animation)
..end = targetValue;
}

// 生命周期
@override
void dispose() {
_controller.dispose();
super.dispose();
}

这里我们基本上了解了动画的控制过程,里面是有 controller 去维护,比较简单,但是值得注意一点是,假设我们做了个自定义 ImplicitlyAnimatedWidget 是从 0 位置跑到 100,但是在 50 的位置改变成 90,那么所花费时间是 150,每次改变都是按照 duration 的时间来决定。

visitor 最容易混乱的地方,只要记住,第一个参数是 tween,第二个参数是 targetValue,也就是 tween.end,第三个参数是初始化,就很简单了~

实践

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class ImplicitlyAnimatedDemo extends ImplicitlyAnimatedWidget {
final Color color;

ImplicitlyAnimatedDemo({
Key key,
Curve curve = Curves.linear,
Duration duration,
VoidCallback onEnd,
@required this.color,
}) : super(
key: key,
curve: curve,
duration: duration,
onEnd: onEnd,
);

@override
ImplicitlyAnimatedWidgetState<ImplicitlyAnimatedWidget> createState() =>
_ImplicitlyAnimatedDemoState();
}

class _ImplicitlyAnimatedDemoState
extends AnimatedWidgetBaseState<ImplicitlyAnimatedDemo> {
ColorTween _colorTween;

@override
Widget build(BuildContext context) {
return Container(
width: 100,
height: 100,
color: _colorTween?.evaluate(animation),
);
}

@override
void forEachTween(visitor) {
_colorTween = visitor(
_colorTween, widget.color, (dynamic value) => ColorTween(begin: value));
}
}

这个 widget 很简单,主要就是改变颜色,可以发现到,我这里没有继承 ImplicitlyAnimatedWidgetState,因为其内部没有 setState,而 AnimatedWidgetBaseState 实现了 setState 去更新 widget

总结

ImplicitlyAnimatedWidget 是我最喜欢的控件,他足够简单,配合 bloc 下更是舒服;在大部分情况下我们可能只是需要改变一下 widget,增加一个过渡动画十分好看,但是绝大部分我们可能也不需要自定义,使用 Animated 开头的 widget 很多都有封装,比如 AnimatedOpacity,AnimatedPadding 等,如果是在需要自定义,可以使用 TweenAnimationBuilder,更为方便的创建。
最后,许多人在学习 Flutter 的时候会找个 api 来写练手,为此我封装了一个官方文档提及到的 newsapi 封装库 newsapi package,如果快速练手可以考虑选择~