Flutter notes

Flutterに関するメモ書き

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

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

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

scrapbox.io

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

painter.dart

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)

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生成が始まる。

f:id:tnantoka:20210630225845p:plain

生成されたGIFはこれ。

f:id:tnantoka:20210630225858g:plain

参考記事にも

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

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

[追記]

以下に続きを書いた。

flutter.tnantoka.com