Flutter備忘録: Tansform.scaleでは親Widgetの大きさが変わらなかった件
開発中のスマホアプリ『コメ欄』のFlutter版の配信画面に、任意の画像を配置出来る機能を追加するのに、FlutterのTransformウイジェットの「Tansform.scale」コンストラクタを使ってImageウイジェットを縮小・拡大表示させる仕組みにした処、画像は設定した通りの大きさで表示されましたが、画像を表示している親ウイジェットの縦横が元の画像の大きさのままでした。。。
結果として、親ウイジェットの縦横も同様に縮小・拡大させるにはTansform.scaleではなく、SizedBoxやContainerウイジェットで縦横の大きさを指定しないといけなかったので、解決までの道のりをここにメモしておきます。
Tansform.scaleで画像サイズの変更
右は、例として383x400のサイズに編集した「いらすとや」からのフリー素材の画像「パスをするサッカー選手のイラスト(男性)」を、アプリ内でTansform.scaleを使って0.45倍に縮小して、画面解像度が1080x2340のPixel 5エミュレータ上に表示した際のスクショになります。
ウイジェットの大きさが分かり易い様に、BoxDecorationを使ってウイジェットに緑の枠を付けてありますが、イラストは0.45倍に縮小されて表示されているものの、ウイジェットの枠は元の画像の大きさのままの383x400で表示されて。。。
いや、1080x2340の画面上に388x400の画像を表示した場合なら、
画像の幅は画面の幅の半分位になるはず。。。
理論画素数と物理画素数
何かおかしいと思って検索したところ、次の質問をスタックオーバーフローで見つけました。
「Flutter MediaQuery.of(context).size.width values are different than real screen resolution」
(FlutterでMediaQuery.of(context).size.widthが返す値が実際の画面解像度と違う)
Pixel 5の画面解像度が「1080x2340」と聞いて、横1080ドッド、縦2340ドッドの画像を1画面に表示出来るのかと(勝手に)思っていましたが、これは『物理画素数 (Physical Pixel)』と呼ばれるもので、そこに実際に表示出来る画像の画素は『理論画素数 (Logical Pixel)』と呼ばれるそうです。
物理画素数と理論画素数が同じ場合は、画像の1ピクセルの表示に画面の1ピクセルが使われる事になりますが、Pixel 5の場合は画像の1ピクセルの表示に複数のピクセルが使われる事になるそうです。。 (ややっこしい・・、でも違うデバイスでも実際に画面表示の大きさを均一にする仕組みなので重要です。)
上の質問の正解に選ばれた答えが引用している文には、「MediaQuery.of(context).devicePixelRatio」が理論画素1ピクセルに相当する物理画素数を返すとあったので、MediaQuery.of(context)で返される値をいくつか同じエミュレーターで確認してみました。
| MediaQuery.of(context).devicePixelRatio = | 2.75 |
| MediaQuery.of(context).size.width = | 392.7272727272727 |
| MediaQuery.of(context).size.height = | 850.9090909090909 |
理論画素数は1080x2340 (=物理画素数)ではなく、ほぼ393x851でした。
「Viewport」や「CSSピクセル」と呼ばれる値だと思いますが、画素数や画面サイズが違う機種が沢山ある中、画面上に表示される文字や画像がほぼ同じ大きさで表示される様に使われる仮想の画素数 (~座標軸)になります。
なので、上のスクショでは、393x851の理論画素の画面上に383x400の緑の枠が画面の横幅いっぱいに表示されている事になります。
Transform.scaleで縮小した場合の余白
話は戻りますが、この例だと、Stackウイジェットを使って配置をしているので画像の余白は配置にそこまで悪影響はありませんが、レイアウトの一部としてColumnや、Rowを使って他のウイジェットと一緒に配置する場合はウイジェットの大きさを基準に配置される為、縮小表示した画像の周りに余白が出てしまいます。
因みに表示に使ったコードは次の様になります:
String filepath = ...... ;
final double ratio = 0.45;
Widget build (BuildContext context) {
final image = Image.file(
File(filepath),
errorBuilder: (_, __, ___) {
return Icon(Icons.warning_amber);
},
);
return Transform.scale(
scale: ratio,
child: image,
);
}
スタックオーバーフローにあったQ&A
スタックオーバーフローに次のスレッドを見つけましたが:
「Flutter - Size of Transform.scale widget does not change when its child is scaled」
(FlutterのTransform.scaleで子ウイジェットを縮小/拡大した時にウイジェットの(縦横の)サイズが変わらない)
正解となっていた答えには、「Transform.scale」で画像を縮小しても出力されるWidgetの大きさは元の画像のままになってしまうので、違う方法を使わないといけないという事で、次の様な解決例が挙げられていました:
_buildWidgetA(width, height) {
return Container(
width: width,
height: height,
child: FittedBox(
child: A(),
),
);
}
出力されるWidgetの大きさも縮小したい場合は、縮小された画像の縦横の大きさをContainerやSizedBoxウイジェットで指定した後、その中にFittedBoxウイジェットで囲んだImageウイジェットで画像を表示さないといけない様です。
別の言い方をすると、Image.fileを使った場合、読み込まれた画像の縦横のサイズは内部で処理されて、実際の値を取り出す事が出来ないので、別の方法で画像の縦横のサイズを手に入れる方法が必要になります。
コード変更
早速、これを実際に試してみたのですが、残念ながら画像は縮小されませんでした。。
String filepath = ...... ;
final double ratio = 0.45;
Widget build (BuildContext context) {
final image = Image.file(
File(filepath),
errorBuilder: (_, __, ___) {
return Icon(Icons.warning_amber);
},
);
final size = image.size;
final height = (null==size.height)? null: ratio * image.size.height!;
final width = (null==size.width)? null : ratio * image.size.width!;
return Container(
height: height,
width: width,
child: FittedBox(
child: image,
),
);
}
というのもImageから返される値をimageとした場合、image.heightとimage.widthの値のタイプは double?でnullの場合があります。
Image.file Image.networkだと、画像の読み込みに時間がかかるため、実際に読み込まれるまではnullが返ってきます。
なので、次の様にImageStreamListenerを使って読みが終わった時点で画像の縦横の値を参照する様にします。
import 'dart:ui' as ui;
: : :
//「filepath」は表示したい画像の保存パスになります (String)
@override
Widget build(BuildContext context) {
final image = Image.file(File(filepath),
errorBuilder: (_, error, trace) {
//読み込み時にエラーがあった場合は警告アイコンを表示
return Icon(Icons.warning_amber_rounded);
},
);
Completer<ui.Image> completer = Completer<ui.Image>();
image.image
.resolve(const ImageConfiguration())
.addListener(ImageStreamListener((ImageInfo info, bool syncCall)
=> completer.complete(info.image))
);
return FutureBuilder<ui.Image>(
future: completer.future,
builder: (BuildContext context, AsyncSnapshot<ui.Image> snapshot) {
if (snapshot.hasData) {
final uiImage = snapshot.data!;
final double height = uiImage.height.toDouble();
final double width = uiImage.width.toDouble();
return SizedBox(
height: height;
width: width;
child: RowImage(
image: uiImage,
fit: BoxFit.contain,
),
);
}
if (snapshot.hasError) {
//Future処理中にエラーがあった場合は警告アイコンを表示
return Icon(Icons.warning_amber_rounded);
}
//エラーもなくデータがまだ無い場合は読み込み中のクルクル回る円を表示
return ConstrainedBox(
constraints: const BoxConstraints(
maxHeight: 60.0, maxWidth: 60.0,
),
child: const CircularProgressIndicator(),
);
},
);
}
但し、読み込まれる画像がui.ImageなのでRowImageウイジェットを使っての表示になります。
また、RowImageにはfitプロパティがあったので、FittedBoxを使う代わりに、fitプロパティにBoxFit.containを指定しました。
Futureを使って画像を読み込まないといけないのでコードは長くなりますが、これで画像の縦横は無事に縮小されます。
追記
RowImageウイジェットだとGIF86aのアニメーションが有効にならない為、アニメーションが必要な場合はImageウイジェットを使う必要があります。


コメント
コメントを投稿