Flutter: InteractiveViewerの拡大率「details.scale」を取得したいが、毎回1.0に戻ってしまう
結論:GlobalKeyを用いて座標を把握しつつ、「onInteractionStart:」で拡大率を補正
2022/6/8 Flutter エラー・バグ日記
(2022.6.14更新)
画像などのWidgetを、ピンチイン・ピンチインアウト(拡大縮小)するのに「InteractiveViewer」は大変便利だが、現在の拡大率(スケール)を取得する方法が分からず、数日間、苦しむ。
結局、すべてGlobalKeyを用いて解決するに至ったので、忘れないために記録(他にもっと良い方法がある気がするが、、、)。
なお、拡大率を取得したかった理由は、画像を拡大縮小した後、同じ画像を、同じ拡大率のまま、他の場所で再表示したかったため。
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の基礎文法を素早くインプットできる/





