Flutterで2つの画像を合成する
Flutterで2枚の画像をこんな感じで合成したかったのですが、試行錯誤することになったので備忘録として残します。
画像表示おさらい
Flutterはこんな感じにするだけで簡単に画像を表示することができるのでした。
Image.asset("assets/image.png")
また画像に対して色を合成することもできます。 BlendModeに関しては一般的なものなので今回は詳しく触れませんが、公式ドキュメント が詳しいです。
Image.asset( "assets/gear.png", color: Colors.green, colorBlendMode: BlendMode.srcIn, )
正直ここまではAndoirdStudioのサジェストに従っていけば難なくこなせるでしょう。
画像どうしを合成するには
UIに凝ってくるとある画像に対して単色ではなくテクスチャを合成したい要求が出てくるかもしれません。先の例でいう歯車に対して緑色を合成するように以下のテクスチャの合成方法を考えます。
画像と色の合成に関しはFlutterのWidgetクラスの内部で行われているため、単純に考えると自前でCustomPainterなどを実装しCanvasに対する描画するロジックを書かねばなりません。 これははっきり言って面倒で運用コストが掛かるので他の方法を考える必要があります。
そこでFlutterが公式に提供しているShaderMaskというクラスを利用します。このクラスではシェーダを使ってマスク処理を行ってくれます。以下の動画の例ではカラーグラデーションやアニメーションなどを定義し、シェーダに変換することでWidgetに対してマスク処理を適用しています。 www.youtube.com
つまり画像データもカラーグラデーションなどのベクタデータ同様に、シェーダに変換できればShaderMaskクラスを利用して2つの画像を合成することができます。
画像を合成方法と結果
まず画像をbyteデータとして読み込み、画像としてデコードします。ここではassets配下にあるpng画像と取り込んでいます。
Future<ui.Image> _loadPngImage(String assetPath) async { final byteData = await rootBundle.load(assetPath); return decodeImageFromList(byteData.buffer.asUint8List()); }
画像がSVGの場合はflutter_svg を利用し以下のように画像を読み込むことができます。
Future<ui.Image> _loadSvgImage(String assetPath) async { final rawSvg = await rootBundle.loadString(assetPath); final DrawableRoot svgRoot = await svg.fromSvgString(rawSvg, rawSvg); final Picture picture = svgRoot.toPicture(); // 元画像のサイズに合わせる return picture.toImage(32, 32); }
画像をFutureクラスで受けとり、画像の読み込みに成功した場合はシェーダに変換し、ShaderMaskに設定します。ShaderMaskのblendModeは単色合成のときと同様に扱えます。シェーダに変換されたテクスチャが単色に相当します。
FutureBuilder( future: _loadPngImage("assets/texture.png"), builder: (context, AsyncSnapshot<ui.Image> image) { return (image.hasData) ? ShaderMask( child: Image.asset("assets/image.png"), shaderCallback: (bounds) => ImageShader( image.data, TileMode.repeated, TileMode.repeated, Matrix4.identity().storage, ), blendMode: BlendMode.srcIn, ) : Container(); }),
このように画像をシェーダに変換することで2つの画像を合成することが可能です。
全体のサンプルプロジェクトはこちらに置いていますので、参考にしていただければと思います。