Flutter: InteractiveViewerの拡大率「details.scale」を取得したいが、毎回1.0に戻ってしまう

※当サイトは、アフィリエイト広告を利用しています

結論:GlobalKeyを用いて座標を把握しつつ、「onInteractionStart:」で拡大率を補正 

2022/6/8 Flutter エラー・バグ日記

 

(2022.6.14更新)

画像などのWidgetを、ピンチイン・ピンチインアウト(拡大縮小)するのに「InteractiveViewer」は大変便利だが、現在の拡大率(スケール)を取得する方法が分からず、数日間、苦しむ。

 

結局、すべてGlobalKeyを用いて解決するに至ったので、忘れないために記録(他にもっと良い方法がある気がするが、、、)。

 

本記事はライトな日記思考で書いているので、詳細説明はしておらず、基本、テキストのみで画像とかは載せておりません。。m(_ _)m

解説記事ではないため、解決していない内容や、その時々の間違った解釈を述べてしまっている可能性が大いにありますので、何卒、ご了承ください。

 

なお、拡大率を取得したかった理由は、画像を拡大縮小した後、同じ画像を、同じ拡大率のまま、他の場所で再表示したかったため。

  

GestureDetectorとTransform.scaleを使う方法

ピンチイン・ピンチインアウトは、下記記事で分かりやすく解説されているとおり、「GestureDetector」「Transform.scale」を使っても実装できる。

 

 

この場合、「GestureDetector」の「onScaleUpdate:」属性引数(引数は「ScaleUpdateDetails」型。「details」という表記を用いることが多い模様)を用いて、

 

「引数名.scale」(例えば「details.scale」)

 

から、現在の拡大率を取得できる。

 

しかし、画像の拡大縮小状態を維持できず、再度、ピンチイン・ピンチアウトしようとすると、拡大率が1.0に戻ってしまう

 

「onScaleStart:」という属性があるので、ここで何とか設定できないか試したが、ソースコード内の説明に、初期の拡大率は1.0で固定と書かれており、変更できなかった。

 

また、拡大縮小と伴に、移動も可能にしようとすると、別途、実装が必要になる点も面倒。

 

そのため、上記の点(拡大縮小の維持、移動を可能にする実装が不要)を解消できる「InteractiveViewer」を用いることにした。

 

InteractiveViewerで拡大率を取得すると、値がおかしい

下記記事にあるとおり、「InteractiveViewer」における現在の拡大率の取得方法は、「GestureDetector」の場合と似ており、「onInteractionUpdate:」属性引数「ScaleUpdateDetails」型。これも「details」という表記を用いることが多い模様)を用いて、

 

「引数名.scale」(例えば「details.scale」)

 

から取得できる。

 

 

しかし、拡大縮小後、画像自体の拡大縮小状態は維持されるが、「引数名.scale」の値が必ず1.0に戻ってしまう。。。

 

そのため、大きく拡大した後、少しだけ縮小すると、実際の拡大率は1.0より大きいはずなのに、「引数名.scale」の値は1.0より小さくなってしまう。

 

いったんピンチイン・ピンチアウトが終了した時点で、「details.scale」の値を保持しておき、次にピンチイン・ピンチアウトするときに、保持していた拡大率で補正することも試みたが、期待どおりの値にならない。。

 

「InteractiveViewer」の解説記事は多いが、この部分(拡大率を正しくトレースする方法)に触れている記事は少なく、公式サイトの解説にも説明が見当たらない。。。

 

検討した対処法① TransformationControllerの利用→解決できず

「InteractiveViewer」には、変形状態をコントロールするための「transformationController:」という属性が用意されている。

 

そのため、「TextEditingController」からテキスト情報を取得するときと同じように、「TransformationControllerのインスタンス.value」(「Matrix4」型)から、拡大率の情報を取得できないか、試行錯誤したが、結局方法が分からず。。。

 

検討した対処法② onInteractionStartの設定→解決できず

こちらも「GestureDetector」を用いた場合と似ているが、「InteractiveViewer」には、「onInteractionStart:」という属性があるので、ここで拡大率(.scale)の初期設定ができないか試みるも、方法がわからず。。

 

※「ScaleUpdateDetails」クラス内の「scale」プロパティの初期値を変更したかったのだが、ソースコードを見ると、「GestureDetector」の場合と同様、「1.0」に固定されていることが理由っぽい。しかし、画像自体の拡大縮小状態は維持してくれるのだが。。

 

検討した対処法③ GlobalKeyでsizeを取得→解決できず

しかたなく、考え方を根本的に変える。

 

画像のウィジェットにGlobalKeyを設置し、

 

// クラス・メソッド(関数)・プロパティ(変数)について、筆者が作成したものには、末尾に大文字のオー「O」をつけて表記
// ※パッケージで予め決められているものと区別しやすくするため
RenderBox imageBoxO = imageGlobalKeyO.currentContext!.findRenderObject() as RenderBox;
double imageHeightO = imageBoxO.size.height;

 

のような感じで、画像のサイズ(ここでは縦幅)を取得し、これを拡大縮小の前後で測定すれば、「拡大後のサイズ÷拡大前のサイズ」から拡大率を算出できるのでは、という考え。

 

しかし、、、残念ながら「InteractiveViewer」で拡大縮小をした場合、この画像サイズの値(上記の例では「imageHeightO」の値)は一定のままで、変化はなかった。。。

 

検討した対処法④ ダミーのContainerにもGlobalKeyを設置→解決、と思ったが問題あり

そこで、次の手として、「Column」を使って画像ファイルの下側に、ダミーの「Container」を配置して、そこにもう一つの「GlobalKey」を設置し、2つの「GlobalKey」から取得した座標値(例えばY座標の値)の差分から、拡大縮小後のサイズを計算することにした(座標値は、拡大縮小とともに、変化してくれるため)。

 

結果、この方法で拡大率を取得できた。※但し、後で問題発覚(後述)

 

「InteractiveViewer」が持つプロパティは一切使わなかった。

 

以下、作成したサンプルコードを掲載(そのままコピペで動かせます)。

※Androidエミュレーターの場合は、Macだとcommandキーを押しながらタッチパッドをドラッグするとピンチイン・ピンチアウトできる。

 

なお、自分の場合は、拡大縮小の作業終了後の拡大率が分かれば良かったので、「onInteractionEnd:」属性を用いたが、リアルタイムで取得したい場合は、「onInteractionUpdate:」属性の方を用いれば可能。

 

// クラス・メソッド(関数)・プロパティ(変数)について、筆者が作成したものには、末尾に大文字のオー「O」をつけて表記
// ※パッケージで予め決められているものと区別しやすくするため
import 'package:flutter/material.dart';

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

class MyApp extends StatefulWidget {
  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {

  // 画像ウィジェットのY座標取得用のGlobalKey
  GlobalKey imageGlobalKeyO = GlobalKey();

  // ダミーContainer(画像ウィジェット下側)のY座標取得用のGlobalKey
  GlobalKey containerGlobalKeyO = GlobalKey();

  @override
  Widget build(BuildContext context) {

    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        appBar: AppBar(
          title: Text('InteractiveViewer Test'),
        ),
        body:
        Center(
          child: InteractiveViewer(
            
            // 何も設定しなければ、拡大率は最大2.5、最小0.8
            // 但し、縮小を可能にするには、この設定が必要
            boundaryMargin: EdgeInsets.all(double.infinity),

            // ピンチイン・ピンチアウト終了後の処理
            onInteractionEnd: (details) {

              // 画像ウィジェットのGlobalKeyからRenderBox型のインスタンスを作成
              RenderBox imageBoxO = imageGlobalKeyO.currentContext!.findRenderObject() as RenderBox;
              // 画像上端のy座標を取得
              double imageBoxPositionYO = imageBoxO.localToGlobal(Offset.zero).dy;
              // 画像の縦幅を取得 ※これはピンチイン・ピンチアウトしても変化しない
              double imageHeightO = imageBoxO.size.height;

              print("画像上端のy座標: $imageBoxPositionYO");
              print("画像の縦幅: $imageHeightO");

              // ダミーContainerのGlobalKeyからRenderBox型のインスタンスを作成
              RenderBox containerBoxO = containerGlobalKeyO.currentContext!.findRenderObject() as RenderBox;
              // ダミーContainerののy座標を取得
              double containerBoxPositionYO = containerBoxO.localToGlobal(Offset.zero).dy;

              print("画像下部のダミーコンテナのy座標: $containerBoxPositionYO}");

              // GlobalKeyから計算した拡大率
              print("GlobalKeyから計算した拡大率: ${(containerBoxPositionYO - imageBoxPositionYO) / imageHeightO}");

            },

            child: Column(
              children: [

                // 画像ウィジェット
                Image.network(
                  "https://picsum.photos/250?image=9",
                  key: imageGlobalKeyO,
                ),

                // 画像の下端位置把握のために設置するダミーContainer
                Container(
                  key: containerGlobalKeyO,
                ),

              ],
            ),
          ),
        ),
      ),
    );
  }
}

 

(2022.6.14追記)

しかし、後日、この方法だと、縦長の画像で、画面より縦幅の大きい画像を配置すると、下図のようにはみ出しエラーになってしまう事が判明。。

 

 

※なぜか横長の画像だと、横幅がオーバーしていても、自動調整されてエラーは起きない。

 

検討した対処法⑤ 上記④に加え、FittedBoxの設置、「onInteractionStart:」で拡大率の補正計算を導入→解決(のはず)

まず、はみ出しエラーを防ぐため、画像とダミーContainerから成る「Column」を「FittedBox」の「fit: BoxFit.contain」でラップし、さらに「Container」でラップして、範囲制約をつけることにした。

 

これではみ出しエラーは解消されたが、今度は2つのGlobalKeyから計算する拡大率が、最初に画面表示されるサイズからの相対比率ではなく、実際の画像サイズからの比率で計算されるようになってしまった。

 

これだと、例えば、「InteractiveViewer」の拡大率の範囲を、デフォルトの0.8〜2.5としていた場合、計算される拡大率は、これよりもかなり小さくなる(Max拡大した場合でも、1より小さい数値になる)。。。

 

そこで、ピンチイン・ピンチアウトを開始したときにイベントを発動できる「onInteractionStart:」属性を利用し、起動後、初めてピンチイン・ピンチアウトするときに、実際の画像サイズからどれだけ縮小(または拡大)して表示されているか、を補正係数として取得することにした。

 

上記④の方法で算出した拡大率を、この補正係数で割ることで、拡大率の範囲を0.8〜2.5に補正することができた。

 

※当初、補正係数を「initState」メソッド内で計算することも試みたが、「WidgetsBinding.instance!.addPostFrameCallback」メソッドを使って、「build」メソッド実行後に「GlobalKey」から画像サイズを測定しているにも拘わらず、画像サイズがゼロになってしまった。そのため、やむを得ず「onInteractionStart:」の方で把握することにした(恐らく「Image.network」を使っていて、画像読み込みにタイムラグがあるから?)。

 

これでようやく、やりたいことを実現できた、、、はず。

 

改めて修正版のコード全体像を掲載。

 

※上記④のバージョンからの変更・追加箇所には、「***」をつけたコメントを記載。

 

// クラス・メソッド(関数)・プロパティ(変数)について、筆者が作成したものには、末尾に大文字のオー「O」をつけて表記
// ※パッケージで予め決められているものと区別しやすくするため
import 'package:flutter/material.dart';

void main() => runApp(

  // *** MediaQuery を使えるようにするため、冒頭に MaterialApp を設置
  MaterialApp(
    debugShowCheckedModeBanner: false,
    home:MyApp(),
  ),
);

class MyApp extends StatefulWidget {
  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {

  // 画像ウィジェットのY座標取得用のGlobalKey
  GlobalKey imageGlobalKeyO = GlobalKey();

  // ダミーContainer(画像ウィジェット下側)のY座標取得用のGlobalKey
  GlobalKey containerGlobalKeyO = GlobalKey();

  // *** 初回ピンチイン・ピンチアウトであることを示すフラグ
  bool firstZoomO = true;

  // *** 拡大率を最初の表示状態からの倍率にしたい場合の補正係数
  double correctionFactorO = 1.0;


  @override
  Widget build(BuildContext context) {

    return Scaffold(
      appBar: AppBar(
        title: Text('InteractiveViewer Test'),
      ),
      body:
      Center(
        child: InteractiveViewer(

          // 何も設定しなければ、拡大率は最大2.5、最小0.8
          // 但し、縮小を可能にするには、この設定が必要
          boundaryMargin: EdgeInsets.all(double.infinity),

          // *** 拡大率を最初の表示状態からの倍率にしたい場合は、以下の onInteractionStart を設定
          //     起動後初回のピンチイン・ピンチアウト時だけ、
          //     実際の画像の縦幅と画面上の画像の縦幅の比から、補正係数を算出する
          onInteractionStart: (details) {
            if (firstZoomO) {

              RenderBox imageBoxO = imageGlobalKeyO.currentContext!.findRenderObject() as RenderBox;
              RenderBox containerBoxO = containerGlobalKeyO.currentContext!.findRenderObject() as RenderBox;

              // *** ①実際の画像の縦幅
              double imageHeightO = imageBoxO.size.height;

              // *** ②起動後初回の画面上の画像の縦幅(ダミーContainer上端のy座標と画像上端のy座標の差分から計算)
              double initialImageHeightO = containerBoxO.localToGlobal(Offset.zero).dy - imageBoxO.localToGlobal(Offset.zero).dy;

              // *** ②÷①で補正係数を算出
              correctionFactorO = initialImageHeightO / imageHeightO;
              print("補正係数: $correctionFactorO");

              // *** 初回ピンチイン・ピンチアウトを示すフラグを変更する
              firstZoomO = false;
            }
          },

          // ピンチイン・ピンチアウト終了後の処理
          onInteractionEnd: (details) {

            // 画像ウィジェットのGlobalKeyからRenderBox型のインスタンスを作成
            RenderBox imageBoxO = imageGlobalKeyO.currentContext!.findRenderObject() as RenderBox;
            // 画像上端のy座標を取得
            double imageBoxPositionYO = imageBoxO.localToGlobal(Offset.zero).dy;
            // 画像の縦幅を取得 ※これはピンチイン・ピンチアウトしても変化しない
            double imageHeightO = imageBoxO.size.height;

            print("画像上端のy座標: $imageBoxPositionYO");
            print("画像の縦幅: $imageHeightO");

            // ダミーContainerのGlobalKeyからRenderBox型のインスタンスを作成
            RenderBox containerBoxO = containerGlobalKeyO.currentContext!.findRenderObject() as RenderBox;
            // ダミーContainerののy座標を取得
            double containerBoxPositionYO = containerBoxO.localToGlobal(Offset.zero).dy;

            print("画像下部のダミーコンテナのy座標: $containerBoxPositionYO}");

            // GlobalKeyから計算した拡大率
            // *** 補正係数で割ることで、拡大率を最初の表示状態からの倍率にする
            //     上記 onInteractionStart の処理をしなければ、補正係数は1.0なので、実際の画像サイズからの拡大率になる
            print("GlobalKeyから計算した拡大率: ${((containerBoxPositionYO - imageBoxPositionYO) / imageHeightO) / correctionFactorO}");

          },

          child:
          // *** 範囲を制限するため Container でラップ
          Container(
            // *** 以下は適宜範囲を決めて設定
            // *** ここでは、横幅は、画面いっぱいに設定
            //     縦幅は、画面の縦幅からAppBarの縦幅を引いた値に設定
            width: MediaQuery.of(context).size.width,
            height: MediaQuery.of(context).size.height - AppBar().preferredSize.height,

            // *** 親Containerの範囲内に収まるように BoxFit.contain でラップ
            child: FittedBox(
              fit: BoxFit.contain,

              child: Column(
                children: [

                  // 画像ウィジェット
                  Image.network(
                    // *** サイズの大きい画像にした場合
                    "https://halzoblog.com/wp-content/uploads/2021/06/image001.png",
                    key: imageGlobalKeyO,
                  ),

                  // 画像の下端位置把握のために設置するダミーContainer
                  Container(
                    key: containerGlobalKeyO,
                  ),

                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}

 

日記のつもりが、かなりの長文記事になってしまった。。

 

リリースしたアプリ(全てFlutterで開発)

 

個人アプリ開発で役立ったもの

おすすめの学習教材

超初心者向けでオススメな元Udemyの講座/

 

 \キャンペーン時を狙えば安価で網羅的な内容が学べる(日本語訳あり)/

 

\Gitの基礎について無料で学べる/

 

おすすめの学習書籍

実用的image_pickerに関してかなり助けられた/

 

Dartの基礎文法を素早くインプットできる/


Dart入門 - Dartの要点をつかむためのクイックツアー

タイトルとURLをコピーしました