imageパッケージでアニメーションGIF生成

June 30, 2021

次アプリを作るならアニメーションを作る系のものを作りたいな、と思っていて調べていたら imageパッケージでアニメーションGIFが作れそうだった。

良い参考記事を見つけたのでやってみた。

https://scrapbox.io/kurogoma4d-lab/Flutter_Web%E3%81%A7Widget%E3%82%92gif%E5%87%BA%E5%8A%9B%E3%81%97%E3%81%9F%E3%81%8B%E3%81%A3%E3%81%9F

まずは表示のためのCustomPainter。 といっても四角形を描画しているだけ。 (ゆくゆくアプリを作るなら図形を描画することになるだろうからCustomPainterで)

painter.dart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

class Painter extends CustomPainter {
  final Color color;

  Painter(this.color);

  @override
  void paint(Canvas canvas, Size size) {
    final Rect rect = Offset.zero & size;
    canvas.drawRect(
      rect,
      Paint()..color = color,
    );
  }

  @override
  bool shouldRepaint(Painter oldDelegate) => oldDelegate.color != color;
}

mainでアニメーション&GIF生成。

_captureRepaintBoundary から画像を生成して、_images に追加。 _generateGif で GIFを生成。 (普通に生成するとUIが固まってしまったのでcomputeで実行)

main.dart (_MyHomePageState)

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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
class _MyHomePageState extends State<MyHomePage>
    with SingleTickerProviderStateMixin {
  late AnimationController _animationController;
  late Animation<double> _animation;
  final List<ui.Image> _images = [];
  final _repaintBoundaryKey = GlobalKey();
  var _generating = false;

  @override
  void initState() {
    super.initState();

    _animationController = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 1),
    );
    _animation = Tween<double>(
      begin: 20,
      end: 220,
    ).animate(_animationController)
      ..addStatusListener((status) {
        if (status == AnimationStatus.completed) {
          _animationController.reverse();
        } else if (status == AnimationStatus.dismissed) {
          _animationController.forward();
        }
      })
      ..addListener(_capture);

    _animationController.forward();
  }

  void _capture() async {
    final RenderRepaintBoundary boundary = _repaintBoundaryKey.currentContext
        ?.findRenderObject() as RenderRepaintBoundary;
    if (boundary.debugNeedsPaint) {
      return;
    }
    final image =
        await boundary.toImage(pixelRatio: ui.window.devicePixelRatio);
    setState(() {
      _images.add(image);
    });
  }

  @override
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }

  void _generateGif() async {
    setState(() {
      _generating = true;
    });
    _animationController.stop();

    final animation = image.Animation();
    final directory = await getApplicationDocumentsDirectory();

    final options = {
      "animation": animation,
      "path": directory.path,
    };

    for (final frameImage in _images.toList()) {
      final imageBytes = await frameImage.toByteData();

      final translatedImage = image.Image.fromBytes(
        frameImage.width,
        frameImage.height,
        imageBytes!.buffer.asUint8List().toList(),
      );

      animation.addFrame(translatedImage);
    }

    compute(_encodeGif, options).then((result) {
      setState(() {
        _generating = false;
      });
    });
  }

  static void _encodeGif(Map<String, Object> option) async {
    final encoder = image.GifEncoder();

    final animation = option["animation"] as image.Animation;
    final path = option["path"] as String;

    final encoded = encoder.encodeAnimation(animation);

    final file = File('${path}/generated.gif');
    await file.writeAsBytes(encoded!.whereType<int>().toList());

    print('completed ${file.path}');
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            AnimatedBuilder(
              animation: _animation,
              builder: (context, child) => Stack(
                children: <Widget>[
                  RepaintBoundary(
                    key: _repaintBoundaryKey,
                    child: Container(
                      color: Colors.white,
                      width: 300,
                      height: 300,
                      child: CustomPaint(
                        painter: Painter(
                            Colors.blue.withAlpha(_animation.value.toInt())),
                      ),
                    ),
                  ),
                ],
              ),
            ),
            Container(
              margin: EdgeInsets.only(top: 16),
              child: Visibility(
                visible: _generating,
                maintainSize: true,
                maintainAnimation: true,
                maintainState: true,
                child: CircularProgressIndicator(),
              ),
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _generateGif,
        tooltip: 'Generate',
        child: Icon(Icons.save),
      ),
    );
  }
}

画面はこんな感じ。 右下のフロッピーをタップするとアニメーションが止まってGIF生成が始まる。

生成されたGIFはこれ。

参考記事にも

画像として出力してみるとフレームレートおかしい

と書いてあったが確かにアニメーション速度が遅くなっている。 それは今後の課題とする。 あと生成時間が長いのでそこもなんとかしたい。

[追記]

以下に続きを書いた。

https://flutter.tnantoka.com/entry/2021/07/05/213938