領(lǐng)取大佬們推薦的學(xué)習(xí)資料
觀前提醒:本文假設(shè)你已經(jīng)有一定的 Flutter 開發(fā)經(jīng)驗,對Flutter 的 Widget,RenderObject 等概念有所了解,并且知道如何開啟 DevTools。
現(xiàn)有一個簡單的汽泡動畫需要實現(xiàn),如下圖:

一、直接通過 AnimationController 實現(xiàn)
當(dāng)看到這個效果圖的時候,很快啊,啪一下思路就來了。涉及到動畫,有狀態(tài),用 StatefulWidget ,State 里創(chuàng)建一個 AnimationController,用兩個 Container 對應(yīng)兩個圈,外圈的 Container 的寬高監(jiān)聽動畫跟著更新就行。
代碼如下:
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` 對應(yīng)兩個圈 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), ), ), ); } }
|
跑起來,很完美的實現(xiàn)了要求,如下圖所示:

但且慢,仔細 review 一下代碼,有沒有發(fā)現(xiàn),內(nèi)圈的 Container其實和動畫并沒有什么關(guān)系,換句話說,它并不需要跟隨動畫一起被 build。
用 DevTools 的 Timeline 開啟Track Widgets Builds 跟蹤一下,如下圖所示:

可以發(fā)現(xiàn),在 Build 階段,BubbleAnimationByAnimationController 因為 setState 引發(fā) rebuild,進而重新 build 了兩個 Container,包括內(nèi)圈里的 Text。
解決辦法也很簡單,把內(nèi)圈的 Widget 提前構(gòu)建好,外圈直接用就行了:
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` 包裹內(nèi)圈 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, ); } }
// 使用時,外部構(gòu)建內(nèi)圈的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 實現(xiàn)
其實 Flutter 官方提供的AnimatedBuilder就是這么做的,它將不變部分的 child 交由外部構(gòu)建。
用 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, );
// 注意:這里不需要監(jiān)聽了并setState了,AnimatedBuilder 已經(jīng)內(nèi)部這樣做了
_controller.repeat(reverse: true); }
@override void dispose() { _controller.dispose(); super.dispose(); }
@override Widget build(BuildContext context) { // 用 AnimatedBuilder 內(nèi)部監(jiān)聽動畫 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 其實就是外部構(gòu)建好的 內(nèi)圈 `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 如下圖所示:

可以看到,Build 階段完全沒有 rebuild 內(nèi)圈的內(nèi)容,只有外圈 Container隨著 rebuild。
且慢,還沒完呢,還有沒有辦法完全不 rebuild 呢?畢竟這個動畫很簡單,內(nèi)圈完全不變的,只有外圈隨時間累加而放大/縮小。這個外圈動畫自己畫行不行?
三、用 CustomPaint 實現(xiàn)
Flutter 提供了一個Widget 叫 CustomPaint,它只需要我們實現(xiàn)一個 CustomPainter 自己往 Canvas 繪制內(nèi)容。
先定義一個 CustomPainter,根據(jù)動畫的值畫外圈,代碼如下:
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; } }
|
特別注意,父類構(gòu)造方法的調(diào)用不能省 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), ), ), ); } }
|
跑起來,跟之前版本一樣的完美。

你可能好奇了,CustomPaint 怎么會自己動起來呢?其實,秘密就在 CustomPainter 的構(gòu)造方法里的 repaint 參數(shù)。
由CustomPaint創(chuàng)建的 RenderObject 對象 RenderCustomPaint 會監(jiān)聽這個 repaint,而該對象是外部傳入的 _controller,動畫更新觸發(fā)markNeedsPaint(),進而畫面動起來了。可以戳這里看一眼 RenderCustomPaint 源碼。
這次 DevTools 的 Timeline 如下圖所示,完全沒有了 Build 的蹤影:

再且慢,還沒結(jié)束。到這里只是解決了 Build 階段頻繁rebuild 的問題,看上圖所示,Paint 階段似乎還能再擠幾滴性能出來?
最后的最后
怎么跟蹤查看 repaint 呢,總不至于打log吧?
開啟 DevTools 的 Repaint RainBow 選項即可。或者在代碼中設(shè)置debugRepaintRainbowEnabled = true。
在手機畫面上立馬會看到色塊,如果畫面上有動畫的話更明顯,其會隨著 paint 的次數(shù)增加而變化,像彩虹燈一樣。如下圖:

可以看到,整個 APP 界面包括頭部的 AppBar 的顏色是跟著內(nèi)部的汽泡一起變的,說明在隨著內(nèi)部動畫而發(fā)生 repaint。
Flutter 提供了一個 RepaintBoundary 用于限制重繪區(qū)域,專門用來解決此問題。
使用方式很簡單,直接套在CustomPaint外面,代碼如下:
1 2 3 4 5 6 7 8 9
|
@override Widget build(BuildContext context) { return RepaintBoundary( child: CustomPaint( painter: _BubblePainter(_controller), child: Container(...), ), ); }
|
效果立桿見影,彩虹圖如下圖所示,只重繪了動畫的區(qū)域:

相對應(yīng)的,Paint 階段耗時也很明顯的降低:

結(jié)語
恭喜你,又離資深 Flutter 開發(fā)更近了一步。通過本文,你應(yīng)該學(xué)會了如何讓 Flutter 動畫動得更有效率。關(guān)注公眾號 逆鋒起筆,回復(fù) pdf,下載你需要的各種學(xué)習(xí)資料。
還在等什么呢,趕快回去按本文思路優(yōu)化你項目中的動畫吧。
如有更好的思路,或者其它的點,歡迎留下你的評論。
原文地址:https://yrom.net/blog/2020/11/16/optimize-animation-in-flutter/
點個『在看』支持下