🐥

CachedNetworkImageProviderのキャッシュ機能とGif制御を両立させる

2024/09/27に公開

目的

cached_network_image package にあるCachedNetworkImageProvider で実現されているキャッシュ機能を用いてGifをループさせずに表示させたい。

背景

cached_network_image packageのキャッシュ機能を利用したWidgetとして
CachedNetworkImageが用意されている。これを用いてGifを表示自体は可能だが、Gifを制御する機能を持たないためGifをループさせずに表示させることができない。

ループ制御などが可能な既存のGif表示パッケージには
https://pub.dev/packages/gif
https://pub.dev/packages/gif_view
https://pub.dev/packages/flutter_gif/
などがあるがいずれもcached_network_imageと競合していてうまくいかない

概要

・CachedNetworkImageProviderを拡張して総フレーム数を返すframeCountプロパティを生やす
・標準のImage Widgetが引数に持つframeBuilderで、現在表示中のフレーム番号と総フレーム数を比較して、終了タイミングを検知させる

詳細設計

cached_network_image からcached_network_image_provider.dartと_image_loader.dartを持ってきます。
cached_network_image_provider.dartはgif用に以下のような変更を加えます

cached_network_gif_provider.dart
  int? _frameCount;
  int? get frameCount => _frameCount;

  Stream<ui.Codec> _loadImageAsync(
    CachedNetworkGifProvider key,
    StreamController<ImageChunkEvent> chunkEvents,
    ImageDecoderCallback decode,
  ) {
    assert(key == this);
    final codec = ImageLoader()
        .loadImageAsync(
          url,
          cacheKey,
          chunkEvents,
          decode,
          cacheManager ?? defaultCacheManager,
          maxHeight,
          maxWidth,
          headers,
          imageRenderMethodForWeb,
          () => PaintingBinding.instance.imageCache.evict(key),
        )
        .asBroadcastStream()
      ..listen((event) {
        _frameCount = event.frameCount;
      });
    return codec;
  }

続けてキャッシュ機能を利用しつつ、Gifをループさせず表示させるためのクラスを作成します

cached_gif.dart
/// 一度だけ再生するGif
class CachedGif extends StatelessWidget {
  CachedGif(
    this.path, {
    this.width,
    this.height,
    this.color,
    this.fit,
    this.onFinish,
    super.key,
  });
  final String path;
  final double? width;
  final double? height;
  final BoxFit? fit;
  final Color? color;
  bool isFinished = false;
  final void Function()? onFinish;
  @override
  Widget build(BuildContext context) {
    final _cachedNetworkGifProvider = CachedNetworkGifProvider(path);
    return Image(
      image: _cachedNetworkGifProvider,
      width: width,
      height: height,
      fit: fit,
      color: color,
      frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
        final frameCount = _cachedNetworkGifProvider.frameCount;
        if (frame != null && frameCount != null && frame >= frameCount - 1) {
          if (!isFinished) {
            onFinish?.call();
            isFinished = true;
          }
          return const SizedBox.shrink();
        }
        return child;
      },
      errorBuilder: (context, error, stackTrace) {
        return const Icon(Icons.error);
      },
    );
  }
}

コード全体はhttps://github.com/yamacrypt/Cached-Gif に乗せています

最後に

CachedNetworkImageProviderを使いながらGifを制御することに関する有益な情報が見つからなくて、実装アイデアを出すまでが大変だったので記事にしてみました。
誰かの役にたてば幸いです。
後もっといい実装アイデアあれば教えてほしいです。

Discussion