天天看點

如何快速提升 Flutter App 中的動畫性能

觀前提醒:本文假設你已經有一定的 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 中的動畫性能