🌅

Flutter 向け画像ライブラリ photo_viewer をつくった

2025/03/01に公開1

Flutter 向け画像ライブラリ photo_viewer をつくって pub.dev に公開しました。

経緯

  • SNSアプリなどで、全画面で画像が開けて、拡大縮小やスワイプで閉じる機能がよくある
    • photo_view というライブラリを使う方法があるが、独自実装が多い、Xなどのネイティブの実装と比べると、アニメーションなどに差異が大きい
  • いろいろなエンタープライズアプリが独自実装している
  • 気軽に使える InteractiveViewer を使った実装がほしい

Demo

import 'package:photo_viewer/photo_viewer.dart';

PhotoViewerImage(
  imageUrl: 'assets/your_image.jpg',
)

ソースコード

https://github.com/kumamotone/photo_viewer

pub.dev

https://pub.dev/packages/photo_viewer

機能

  • 基本的な機能
    • ピンチジェスチャーによる画像ズーム
    • ダブルタップによるズーム機能
    • 垂直スワイプによる閉じる機能
    • WidgetにURLを渡すだけの簡単なインターフェイス
  • 複数枚表示(Beta)
    • ページネーション付き複数画像表示
  • ネイティブライクな表示
    • PageRouteによる透明度の画面遷移
    • Heroアニメーション
  • 自由度の高いカスタマイズのための機能
    • カスタムオーバーレイ
    • ページジャンプ用コールバック

アピールポイント

  • 簡単に使えるシンプルな設計
  • 豊富なサンプル

使い方

一番シンプルな使い方

冒頭のスクリーンショットを実現するコードです。 PhotoViewerImage というWidgetを使い、 imageUrl を指定します。

PhotoViewerImage(
  imageUrl: 'assets/your_image.jpg',
)

これで、

  • ピンチジェスチャーによる画像ズーム
  • ダブルタップによるズーム機能
  • 垂直スワイプによる閉じる機能

ができるようになります。

指定されたURLが http もしくは https からはじまるURLの場合、 CachedNetworkImage を使います。(できるだけ何も考えずに使えるようにしたかったのでこのようにしましたが、もしこれだと困りそうなケースがあればぜひご指摘いただければです)

複数枚画像

PhotoViewerMultipleImage というWidgetを使います。

PhotoViewerMultipleImage(
  imageUrls: imagePaths,
  index: i,
  id: postId,
)

同じ id が指定されているものは、横スワイプで切り替えることができます。

画像以外の要素を開く

画像以外の要素を指定したい場合は、showPhotoViewer で Widget を builders に指定します。

showPhotoViewer(
  context: context,
  builders: reversedPaths.map<WidgetBuilder>((url) {
    return (BuildContext context) => Image.asset(...);
  }).toList(),
  overlayBuilder: (context) => Stack(...),
)

想定しているユースケース

  • SNSアプリの画像表示
  • 不動産アプリの詳細ページ
  • 漫画ビューアー

技術的詳細解説

https://github.com/kumamotone/photo_viewer/blob/main/lib/photo_viewer.dart

まずピンチジェスチャーによる画像ズーム、ダブルタップによるズーム機能、垂直スワイプによる閉じる機能について説明します。

InteractivePhotoPage

class InteractivePhotoPage extends StatefulWidget {
  // ...
  final TransformationController transformationController;
  final AnimationController animationController;
  // ...
}

このクラスで、画像の拡大・縮小とダブルタップのズーム機能を実装しています。
InteractiveViewerで直感的なピンチズーム操作を実現するほか、ダブルタップ時にMatrix4Tweenアニメーションを使用して自然なズーム処理を行います。

参考にした実装: https://stackoverflow.com/questions/65408346/flutter-enable-image-zoom-in-out-on-double-tap-using-interactiveviewer/65634589#65634589

VerticalSwipeDismissible

class VerticalSwipeDismissible extends StatefulWidget {
  const VerticalSwipeDismissible({
    required this.child,
    super.key,
    this.enabled = true,
    this.dismissThreshold = 0.2,
    this.onDismissed,
  });

このウィジェットは垂直方向のスワイプジェスチャーを検出し、閾値を超えた場合にビューアを閉じる機能を実装しています。
ポインターの移動量を追跡し、あるしきい値を超えるとonDismissedを呼び出します。

onPointerMove: widget.enabled
    ? (event) {
        if (pointersCount < 2) {
          handleDragUpdate(event);
        } else {
          handleDragEnd();
          dragAmount = 0;
          animateController.value = 0.0;
        }
      }
    : null,

工夫したところとして、ポインターカウントを使用して複数の指で操作された場合の挙動を制御し、ピンチズームとスワイプ操作の干渉を防いでいます。

ページ遷移のアニメーション

次に、ページ遷移のアニメーションについて説明します。ページ遷移のアニメーションは、 TransparentPageRouteHero をベースに実装しています。

TransparentPageRoute

class TransparentPageRoute<T> extends PageRoute<T> {
  TransparentPageRoute({
    required this.builder,
  }) : super();

  final WidgetBuilder builder;

  
  bool get opaque => false;

  
  bool get barrierDismissible => true;

このクラスは標準のPageRouteを拡張し、透明な背景を持つルートを実現しています。opaqueプロパティをfalseに設定することで、下層のウィジェットが透けて見えるようになります。これにより、フォトビューアが開く際に、自然に透明度が変化していくトランジションが可能になります。

Hero

String _heroTag(int index) => 'photo_viewer_${id}_${index}_${imageUrls[index]}';

遷移アニメーション部分には Hero を使っています。これを使うことで、画像が広がっていくアニメーションを簡単に実装できます。リソースのパスやURLが同じでも id がユニークになるように、自動的にIDを付与するようにしています。

カスタマイズのための機能

overlayBuilder

画像ビューアの上に Stack で上に重ねるための overlayBuilder というプロパティを追加しています。これによって、以下のようなユースケースに対応します。

  • カスタム閉じるボタン
  • コメント入力
  • 画面下部のギャラリー表示
  • いいね数やリポスト数の表示
overlayBuilder: (context) => Stack(
  children: [
    _GalleryThumbnails(
      imagePaths: GallerySamplePage._imagePaths,
      selectedIndex: _currentIndex,
      onTap: _handleThumbnailTap,
    ),
  ],
),

ビルダーパターンを採用しているため、サムネイル、コメント入力、カスタムコントロールなど様々なUIをオーバーレイとして柔軟に遅延ロードで追加できます。

onPageChanged / onJumpToPage

onPageChanged / onJumpToPage は、現在見ているページがどこか変わったときに返されるコールバック、および特定のページに飛ぶためのコールバックです。

onJumpToPage: (jump) {
  _jumpToPage = jump;
},

サンプルでは画面下部のギャラリー表示で使っています。これにより、 overlayBuilder で表示したWidgetをタップしたらそのページに飛ぶ、といったことが可能になります。

これから

  • 挙動の改善
    • Flutterのビューア実装の中ではきれいに動く方だと感じるが、複数枚画像のとき、垂直スワイプとPageViewの処理にひっかかりを感じることがある
    • 現在スワイプ量だけで dismiss するようにしているが、 iOSっぽくはじくようなスワイプの仕方でも dismiss できるようにしたい
  • フィードバックを取り入れてより良いインターフェイスにしたい

おわりに

今回はじめて pub.dev にパッケージを公開しました。至らない点もあるかもしれませんが、ぜひ改善できる点などあればコメント等いただけると幸いです。また、気に入ったらGitHub のスターとpub.devのLikeもぜひお願いします!

Discussion