觀前提醒:本文假設你已經有一定的 Flutter 開發經驗,對Flutter 的 Widget,RenderObject 等概念有所了解,并且知道如何開啟 DevTools。
現有一個簡單的汽泡動畫需要實作,如下圖:
如何快速提升 Flutter App 中的動畫性能 一、直接通過 AnimationController 實作
當看到這個效果圖的時候,很快啊,啪一下思路就來了。涉及到動畫,有狀态,用
StatefulWidget
,State 裡建立一個
AnimationController
,用兩個
Container
對應兩個圈,外圈的
Container
的寬高監聽動畫跟着更新就行。
代碼如下:
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
| const double size = 56;
class BubbleAnimationByAnimationController extends StatefulWidget {
@override
_BubbleAnimationByAnimationControllerState createState() => _BubbleAnimationByAnimationControllerState();
}
class _BubbleAnimationByAnimationControllerState extends State<BubbleAnimationByAnimationController>
with SingleTickerProviderStateMixin {
AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 1),
vsync: this,
)..addListener(() => setState(() {}));
_controller.repeat(reverse: true);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
// 兩個 `Container` 對應兩個圈
return Container(
alignment: Alignment.center,
constraints: BoxConstraints.tight(
Size.square((1 + _controller.value * 0.2) * size),
),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.blue[200],
),
child: Container(
alignment: Alignment.center,
padding: const EdgeInsets.all(8.0),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.blue,
),
width: size,
height: size,
child: Text(
'Hello world!',
style: TextStyle(color: Colors.white, fontSize: 12),
),
),
);
}
}
|
跑起來,很完美的實作了要求,如下圖所示:
如何快速提升 Flutter App 中的動畫性能 但且慢,仔細 review 一下代碼,有沒有發現,内圈的
Container
其實和動畫并沒有什麼關系,換句話說,它并不需要跟随動畫一起被 build。
用 DevTools 的 Timeline 開啟
Track Widgets Builds
跟蹤一下,如下圖所示:
如何快速提升 Flutter App 中的動畫性能 可以發現,在
Build
階段,BubbleAnimationByAnimationController 因為 setState 引發 rebuild,進而重新 build 了兩個
Container
,包括内圈裡的
Text
。
解決辦法也很簡單,把内圈的 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
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
61
62
| class BubbleAnimationByAnimationController extends StatefulWidget {
final Widget child;
const BubbleAnimationByAnimationController({this.child});
@override
_BubbleAnimationByAnimationControllerState createState() => _BubbleAnimationByAnimationControllerState();
}
class _BubbleAnimationByAnimationControllerState extends State<BubbleAnimationByAnimationController>
with SingleTickerProviderStateMixin {
AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 1),
vsync: this,
)..addListener(() => setState(() {}));
_controller.repeat(reverse: true);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
// 外圈 `Container` 包裹内圈
return Container(
alignment: Alignment.center,
constraints: BoxConstraints.tight(
Size.square((1 + _controller.value * 0.2) * size),
),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.blue[200],
),
// 這裡的 widget.child 不會 rebuild
child: widget.child,
);
}
}
// 使用時,外部建構内圈的Widget
final Widget buble = BubbleAnimationByAnimationController(
child: Container(
alignment: Alignment.center,
padding: const EdgeInsets.all(8.0),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.blue,
),
width: size,
height: size,
child: Text(
'Hello world!',
style: TextStyle(color: Colors.white, fontSize: 12),
),
),
);
|
二、通過 AnimatedBuilder 實作
其實 Flutter 官方提供的
AnimatedBuilder
就是這麼做的,它将不變部分的 child 交由外部建構。
用 AnimatedBuilder 改造代碼如下:
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
61
62
63
64
65
| class BubbleAnimationByAnimatedBuilder extends StatefulWidget {
@override
_BubbleAnimationByAnimatedBuilderState createState() =>
_BubbleAnimationByAnimatedBuilderState();
}
class _BubbleAnimationByAnimatedBuilderState
extends State<BubbleAnimationByAnimatedBuilder>
with SingleTickerProviderStateMixin {
AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 1),
vsync: this,
);
// 注意:這裡不需要監聽了并setState了,AnimatedBuilder 已經内部這樣做了
_controller.repeat(reverse: true);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
// 用 AnimatedBuilder 内部監聽動畫
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Container(
alignment: Alignment.center,
constraints: BoxConstraints.tight(
Size.square((1 + _controller.value * 0.2) * size),
),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.blue[200],
),
child: child, // 這個child 其實就是外部建構好的 内圈 `Container`
);
},
child: Container(
alignment: Alignment.center,
padding: const EdgeInsets.all(8.0),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.blue,
),
width: size,
height: size,
child: Text(
'Hello world!',
style: TextStyle(color: Colors.white, fontSize: 12),
),
),
);
}
}
|
再次跑起來,非常完美。DevTools 的 Timeline 如下圖所示:
如何快速提升 Flutter App 中的動畫性能 可以看到,Build 階段完全沒有 rebuild 内圈的内容,隻有外圈
Container
随着 rebuild。
且慢,還沒完呢,還有沒有辦法完全不 rebuild 呢?畢竟這個動畫很簡單,内圈完全不變的,隻有外圈随時間累加而放大/縮小。這個外圈動畫自己畫行不行?
三、用 CustomPaint 實作
Flutter 提供了一個Widget 叫
CustomPaint
,它隻需要我們實作一個
CustomPainter
自己往
Canvas
繪制内容。
先定義一個
CustomPainter
,根據動畫的值畫外圈,代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| class _BubblePainter extends CustomPainter {
final Animation<double> animation;
const _BubblePainter(this.animation) : super(repaint: animation);
@override
void paint(Canvas canvas, Size size) {
final center = size.center(Offset.zero);
// 跟随動畫放大/縮小圈的半徑
final radius = center.dx * (1 + animation.value * 0.2);
final paint = Paint()
..color = Colors.blue[200]
..isAntiAlias = true;
canvas.drawCircle(center, radius, paint);
}
@override
bool shouldRepaint(_BubblePainter oldDelegate) {
return oldDelegate.animation != this.animation;
}
}
|
特别注意,父類構造方法的調用不能省
super(repaint: animation)
,後面告訴你為什麼。
其它代碼跟之前沒什麼兩樣,如下:
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
| class BubbleAnimationByCustomPaint extends StatefulWidget {
@override
_BubbleAnimationByCustomPaintState createState() =>
_BubbleAnimationByCustomPaintState();
}
class _BubbleAnimationByCustomPaintState
extends State<BubbleAnimationByCustomPaint>
with SingleTickerProviderStateMixin {
AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 1),
vsync: this,
);
_controller.repeat(reverse: true);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: _BubblePainter(_controller),
// CustomPaint 的大小會自動使用 child 的大小
child: Container(
alignment: Alignment.center,
padding: const EdgeInsets.all(8.0),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.blue,
),
width: size,
height: size,
child: Text(
'Hello world!',
style: TextStyle(color: Colors.white, fontSize: 12),
),
),
);
}
}
|
跑起來,跟之前版本一樣的完美。
如何快速提升 Flutter App 中的動畫性能 你可能好奇了,
CustomPaint
怎麼會自己動起來呢?其實,秘密就在
CustomPainter
的構造方法裡的
repaint
參數。
由
CustomPaint
建立的 RenderObject 對象
RenderCustomPaint
會監聽這個
repaint
,而該對象是外部傳入的
_controller
,動畫更新觸發
markNeedsPaint()
,進而畫面動起來了。可以戳這裡看一眼 RenderCustomPaint 源碼。
這次 DevTools 的 Timeline 如下圖所示,完全沒有了 Build 的蹤影:
如何快速提升 Flutter App 中的動畫性能 再且慢,還沒結束。到這裡隻是解決了
Build
階段頻繁rebuild 的問題,看上圖所示,
Paint
階段似乎還能再擠幾滴性能出來?
最後的最後
怎麼跟蹤檢視 repaint 呢,總不至于打log吧?
開啟 DevTools 的
Repaint RainBow
選項即可。或者在代碼中設定
debugRepaintRainbowEnabled = true
在手機畫面上立馬會看到色塊,如果畫面上有動畫的話更明顯,其會随着 paint 的次數增加而變化,像彩虹燈一樣。如下圖:
如何快速提升 Flutter App 中的動畫性能 可以看到,整個 APP 界面包括頭部的
AppBar
的顔色是跟着内部的汽泡一起變的,說明在随着内部動畫而發生
repaint
Flutter 提供了一個
RepaintBoundary
用于限制重繪區域,專門用來解決此問題。
使用方式很簡單,直接套在
CustomPaint
外面,代碼如下:
1
2
3
4
5
6
7
8
9
| @override
Widget build(BuildContext context) {
return RepaintBoundary(
child: CustomPaint(
painter: _BubblePainter(_controller),
child: Container(...),
),
);
}
|
效果立杆見影,彩虹圖如下圖所示,隻重繪了動畫的區域:
如何快速提升 Flutter App 中的動畫性能 相對應的,Paint 階段耗時也很明顯的降低:
如何快速提升 Flutter App 中的動畫性能 結語
恭喜你,又離資深 Flutter 開發更近了一步。通過本文,你應該學會了如何讓 Flutter 動畫動得更有效率。關注公衆号 逆鋒起筆,回複 pdf,下載下傳你需要的各種學習資料。
還在等什麼呢,趕快回去按本文思路優化你項目中的動畫吧。
如有更好的思路,或者其它的點,歡迎留下你的評論。
如何快速提升 Flutter App 中的動畫性能