【Flutter】ズームやドラッグ楽々対応!InteractiveViewer解説

対象者

  • Flutterを使用した開発経験はあるが、InteractiveViewerについての具体的な知識や経験が少ない方
  • プロジェクトの中でInteractiveViewerの実装が必要となり、迅速に知識を取得したい方
  • Flutterに関する新しい技術やウィジェットを学び、スキルアップを目指している方

はじめに

Flutterの魅力的なウィジェット、InteractiveViewerを知っていますか?このウィジェットは、画像を取り扱うFlutterアプリのユーザーエクスペリエンスを大幅に向上させる可能性を秘めています。しかし、その機能や設定方法がまだ不明確である場合、ここにある情報があなたの力になります。もしInteractiveViewerについて具体的な知識や経験が少ないと感じているなら、この記事はまさにあなたのために書かれました。

InteractiveViewerとは

InteractiveViewerは、子ウィジェットに対してズームやパンを可能にするウィジェットです。このウィジェットの登場により、特定の部分を拡大したり、全体を移動させるといったインタラクションが手軽に実装できるようになりました。

  • 実例:
    InteractiveViewer(
      child: Image.asset('assets/sample_image.png'),
    )

このように、簡単なコードで画像などのウィジェットに対してインタラクティブな動きを追加することができます。

使用のメリット

InteractiveViewerの導入には、いくつかの明確なメリットがあります。

まず、従来の方法でズームやパンの動作を実装しようとすると、多くの手間とコードが必要でした。しかし、InteractiveViewerを使用することで、これらのインタラクションを簡単かつ効率的に実装することができます。

さらに、このウィジェットはFlutterフレームワークの中核部分として提供されているため、安定性やパフォーマンスの面でも信頼性が高いです。

InteractiveViewerの基本的な設定

基本のプロパティ

FlutterのInteractiveViewerは、多彩なプロパティを備えており、それによって細やかな調整が可能です。

最も基本的なプロパティとしては、maxScaleminScaleがあります。これにより、ズームの最大・最小範囲を指定することができます。さらにpanEnabledプロパティを使用することで、パン(ドラッグによる移動)の有効・無効を切り替えることも可能です。

  • 実例:
    InteractiveViewer(
      minScale: 0.5,
      maxScale: 2.0,
      panEnabled: true,
      child: Image.asset('assets/sample_image.png'),
    )

上記の設定では、画像は最小で元の50%のサイズ、最大で200%のサイズにまで拡大することができます。そして、画像上でのドラッグによる移動も可能となっています。

Childのサイズとインタラクティブ領域の関係

InteractiveViewerの子ウィジェットのサイズは、インタラクティブな操作の範囲に直接的な影響を与えます。例えば、子ウィジェットのサイズが大きすぎると、全体を表示することが難しくなる可能性があります。

逆に、サイズが小さい場合、必要以上にズームアウトされてしまうことも考えられます。これを避けるためには、boundaryMarginプロパティを利用して、ウィジェットの周囲に余白を追加することが推奨されます。

  • 実例:
    InteractiveViewer(
      boundaryMargin: EdgeInsets.all(20.0),
      child: Container(
        width: 200.0,
        height: 200.0,
        color: Colors.blue,
      ),
    )

この例では、青色のコンテナに周囲に20ピクセルの余白を持たせることで、インタラクティブな操作時の適切な表示を保っています。

プロパティの詳細

  • alignPanAxis:

    • これがtrueの場合、ユーザーがドラッグを開始すると、動きは水平軸または垂直軸のいずれかに制限されます。主に、一つの方向にのみスクロールやドラッグをさせたい場合に使用します。
  • boundaryMargin:

    • ビューの境界の外側に追加のマージンスペースを提供します。これにより、子ウィジェットをビューポートの外側にドラッグできるようになります。
  • constrained:

    • このプロパティがtrueの場合、子はビューポートに合わせてサイズが変更されます。
    • falseに設定すると、子は自身のサイズを維持します。
  • scaleEnabled:

    • このプロパティがtrueの場合、ピンチジェスチャを使用してズームイン・ズームアウトが可能になります。
    • デフォルトではtrueです。
  • maxScaleminScale:

    • これらのプロパティは、ズームの最大・最小のスケール値を指定します。
    • 例えば、maxScale: 2.5と設定すると、最大2.5倍まで拡大できます。
  • onInteractionEnd, onInteractionStart, onInteractionUpdate:

    • これらはインタラクティブな操作(ズーム、パンなど)が開始、更新、終了したときに呼び出されるコールバック関数です。
  • panEnabled:

    • このプロパティがtrueの場合、ドラッグジェスチャを使用してパン操作が可能になります。
    • デフォルトではtrueです。
  • transformationController:

    • これはMatrix4の変換を管理するためのコントローラーです。
    • プログラム的に変換をリセットする場合や特定の変換を適用する場合に使用します。

これらのプロパティを適切に組み合わせることで、InteractiveViewerの動作や外観をカスタマイズできます。

ズームとパンに関する詳細

ズームモード時のペインター作成に関する問題と対応策

InteractiveViewerを使用するとき、ズームモードにすることで繊細な描画のためのペインターの動作に問題が発生することがあります。具体的には、高いズームレベルでの描画の正確さが失われることがあるのです。
この問題への対応策として、transformationControllerを利用し、変換の状態を監視して、必要な描画を更新する方法が効果的です。

  • 実例:
    TransformationController _controller = TransformationController();
    
    InteractiveViewer(
      transformationController: _controller,
      onInteractionUpdate: (details) {
        if (_controller.value.getMaxScaleOnAxis() > 2.0) {
          // 何らかの再描画ロジック
        }
      },
      child: CustomPainterWidget(), 
    )

この実例では、変換が更新されるたびに、ズームレベルが2.0を超えたら特定の再描画ロジックをトリガーしています。

GestureDetectorの利用とonPanUpdateメソッドの活用

InteractiveViewerの内部ではGestureDetectorが用いられていますが、自身でGestureDetectorを組み込むことで、よりカスタマイズしたインタラクションを実現することもできます。特に、onPanUpdateメソッドを利用することで、ユーザーのドラッグ操作を細かくハンドリングできます。

  • 実例:
    InteractiveViewer(
      child: GestureDetector(
        onPanUpdate: (details) {
          // ドラッグ中の動作をカスタマイズ
          print(details.localPosition);
        },
        child: Image.asset('assets/sample_image.png'),
      ),
    )

この例では、ユーザーがドラッグする度に、その位置情報をコンソールに表示しています。このようにして、特定の位置や動きに応じたカスタムアクションをトリガーすることも可能です。


上記は一般的な解説をベースにした内容です。具体的なプロジェクトや使用ケースに合わせて、さらなる詳細や調整が必要な場合があります。

Q&A

Q1: InteractiveViewerはどのような目的で使用されるのでしょうか?

A1: InteractiveViewerはFlutterでインタラクティブなUIを実装するためのウィジェットです。主に画像や地図などの要素にズームやパンの操作を追加するために使用されます。

Q2: InteractiveViewerのメリットは何でしょうか?

A2: InteractiveViewerのメリットとして、直感的な操作性、高いカスタマイズ性、そして効率的な実装が可能である点が挙げられます。これにより、ユーザー体験を向上させることができます。

Q3: InteractiveViewerでの基本的な画像表示のコード例はどのようになるのでしょうか?

A3: 基本的な画像表示のコード例は以下の通りです。

InteractiveViewer(
    child: Image.asset('assets/map.png'),
)

このコードにより、ピンチイン・ピンチアウトのジェスチャでのズームや、指のスワイプでのパン操作が可能になります。

まとめ

Flutter開発者として、ユーザーエクスペリエンスを向上させるために、さまざまなウィジェットを活用することが求められます。その中で、画像やウィジェットのズームやパン操作を簡単に実装できるInteractiveViewerは非常に重要です。

この記事では、InteractiveViewerの基本的な使用方法を解説しました。記事の最後に、ネットワークから画像を取得して、その画像の上にFlutterLogoを表示するサンプルアプリを作成しました。画像をズームしてもFlutterLogoのサイズが変わらないような工夫をしてます。

これにより、読者はInteractiveViewerの役割や活用方法を学び、具体的なアプリケーションでの使用例を理解しました。特に、動的なサイズ調整を行いながらのズームイン・アウトの操作は、多くのアプリで役立つテクニックと言えるでしょう。

覚えておきたいポイント:

  • InteractiveViewerはズームやパン操作を簡単に追加できる。
  • TransformationControllerでズームやパンの状態を制御できる。
  • ズーム時の特定ウィジェットのサイズ調整は、動的にサイズを変更することで実現できる。

これらの知識を活かし、さらに豊かでユーザーフレンドリーなFlutterアプリを作っていきましょう!

参考

ソース(main.dartにコピペして動作確認用)

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'InteractiveViewer Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  TransformationController _controller = TransformationController();
  double _scale = 1;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('InteractiveViewer Sample'),
      ),
      body: Column(
        children: [
          ColoredBox(
            color: Colors.grey,
            child: InteractiveViewer(
              transformationController: _controller,
              constrained: true,
              panEnabled: true,
              scaleEnabled: true,
              boundaryMargin: const EdgeInsets.all(32.0),
              minScale: 0.5,
              maxScale: 3.0,
              onInteractionUpdate: (details) {
                setState(() {
                  _scale = _controller.value.getMaxScaleOnAxis();
                });
              },
              child: Stack(
                children: [
                  SizedBox(
                    height: 300,
                    child: Image.network(
                        'https://flutter.salon/wp-content/uploads/2022/11/IMGP0818-768x508.jpg'),
                  ),
                  FlutterLogo(size: 64 / _scale),
                ],
              ),
            ),
          ),
          Text('Scale: $_scale'),
          FilledButton(
              onPressed: () {
                setState(() {
                  _controller.value = Matrix4.identity();
                });
              },
              child: Text('Reset')),
        ],
      ),
    );
  }
}

このサンプルアプリでは、ネットワークから取得した画像とFlutterLogoを表示します。その上で、InteractiveViewerを使用して画像のズームやパンができるようにしています。

特筆すべき点として、画像をズームしてもFlutterLogoのサイズは一定に保たれるようにしています。これは_scaleという変数を利用して、FlutterLogoのサイズを動的に調整することで実現しています。

コードの詳細

以下に、サンプルアプリの主要部分の解説をします。

  1. TransformationControllerの使用:
    _controllerというTransformationControllerを使用して、現在のズームレベルやパンの位置を管理しています。

  2. InteractiveViewerの設定:
    boundaryMarginを設定して境界のマージンを追加、minScalemaxScaleでズームの範囲を指定しています。

  3. ズーム時のFlutterLogoのサイズ調整:
    onInteractionUpdateプロパティで、ズームやパンの操作が更新されるたびに_scaleの値を更新しています。この_scaleを利用してFlutterLogoのサイズを動的に調整しています。

  4. リセットボタンの実装:
    FilledButtonを使ってリセットボタンを実装し、押すとInteractiveViewerの状態が初期化されるようにしています。