はじめに
今回flutter_shaders
を用いて遷移アニメーションを実装しました。
その中で詰まったポイントを中心にまとめた記事になります。
対象読者
- flutter_shadersの基本的な使い方を理解してる方 (この記事に基本的な説明はありません)。
setImageSampler
を2件セットしたいがやり方がわからない方。
これを読めばこれが作れる!
参照元:https://www.shadertoy.com/view/MslyDN
環境
- Flutter: 3.16.9
- Xcode: 15.2
- flutter_shaders: 0.1.2
困ったこと
2つ目のsetImageSamplerを用いて動作した実績が世にない。
前提として、上記動画のShaderを動作させるためには以下ソースコードのiChannel0
(遷移元)用とiChannel1
(遷移先)用の素材が必要です。
それぞれの素材をFlutter側からShader側に変数を渡すことで最終的にShaderを表現できるのがflutter_shaders
の仕組みです。ここでは具体的な説明は省略します。
Shader側のコード
参照元:https://www.shadertoy.com/view/MslyDN
Flutter側のサンプルコード
import 'dart:ui'; import 'dart:ui' as ui; import 'package:flutter_shaders/flutter_shaders.dart'; ... ui.Image? nextImage; ... return ShaderBuilder( assetKey: 'shaders/transition.frag', // GLSLファイル child: widget.prev, // 遷移元素材 (context, shader, child) { return AnimatedSampler( child: child!, (ui.Image image, Size size, Canvas canvas) { shader.setFloat(0, size.width); shader.setFloat(1, size.height); shader.setFloat(2, _time); shader.setImageSampler(0, image); // ① shader.setImageSampler(1, nextImage!); // ② ここにセットしたい!!! final paint = Paint(); paint.shader = shader; canvas.drawRect(Offset.zero & size, paint); }, ); }, );
iChannel0
に対応するものが①のimage
、iChannel1
に対応するものが②のnextImage
になります。
ここで大きく詰まったのが、②の遷移先素材を単純にセットできないことでした。
setImageSampler
の第二引数はui.Image
型のみ受け入れ可能です。
①のsetImageSampler
の第二引数には、AnimatedSampler
のbuilder
のimage
をそのままセットできます。image
はShaderBuilder
のchild
で設定したものをAnimatedSampler
がui.Image
型に変えてくれています。
ここまでは基本的な使い方で問題ないです。
一方で、
②のsetImageSampler
の第二引数にセットするui.Image
型はどう作り出すのでしょうか。AnimatedSampler
がui.Image
型に変えてくれるのは①で使用するimage
だけです。
だとすると、Widget
やImage
の素材をui.Image
型に変えてなんとか持ってこないといけないわけです。
当然ですがas
で強制的に型を変えて動かすと怒られます。
調査
調査した時点で、flutter_shaders
でsetImageSampler
を2件セットしてる人はいませんでした。
flutter_shaders
に関する記事はいくつかあり、すべてに共通しているのが①にimage
をセットしたらshader動くよね!という内容です。
公式ドキュメントでもsetImageSampler
は1件で紹介されています。
(モバイルアプリでこんな凝ったアニメーションを実装しようという発想に至らないのはよくわかります。。)
試しにAnimatedSampler
を重ねて②用のui.Image
を作り出してみましたが、ShaderBuilder
がうまく動作せず断念しました。
対応
具体的なやり方が見つからなかったため、段階を踏んで型を変換し、最終的にui.Image
を作る方針で対応しました。
- 1. 遷移先の素材を
Uint8List
型へ変換 - 2.
Uint8List
型のデータからui.Image
型に変換
1. 遷移先の素材をUint8List型へ変換
Future<ui.Image?> _convertToUiImage(Image image) async { final byteData = await image.toByteData(format: ImageByteFormat.png); final bytes = byteData?.buffer.asUint8List(); return await _decodeImageFromList(bytes!); // 下記2の実装へ }
2. Uint8List型からui.Image型へ変換
image_decoder.dartの実装を参照。
Future<ui.Image?> _decodeImageFromList(Uint8List bytes) async { final ui.ImmutableBuffer buffer = await ui.ImmutableBuffer.fromUint8List(bytes); final ui.Codec codec = await PaintingBinding.instance.instantiateImageCodecWithSize(buffer); final ui.FrameInfo frameInfo = await codec.getNextFrame(); return frameInfo.image; }
全体フロー
以下イメージで実装しました。仕様によってカスタマイズしてみてください。
i) nextImage
がnull
の場合、_convertToUiImage()
を実行。
ii) i)の間は遷移元の素材を表示させておく。
iii) _convertToUiImage()
の戻り値がnullでなければそれをnextImage
に代入。
iiii) nextImage
が入ってきたらShaderBuilder
実行。
動作確認
↑遷移元の素材にもshaderを適用。
さいごに
情報がなくて時間を費やした対応になりましたが、実装できた時の喜びは非常に大きかったです。
少しでも同じ境遇の方の助けになれば幸いです。
参考文献
- https://pub.dev/packages/flutter_shaders
- https://www.shadertoy.com/
- https://github.com/JhonaCodes/simple_widget_snapshot/blob/e75fc890b5657d8b7071589c4f771920fd40b54b/lib/src/widget_snapshot.dart#L74
- https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/painting/image_decoder.dart
- https://api.flutter.dev/flutter/dart-ui/FragmentShader/setImageSampler.html?_gl=1*cz5d97*_ga*MTI4NzUwOTA4LjE3MjY0NzQ1NTQ.*_ga_04YGWK0175*MTcyNjUyOTI3Ny4yLjEuMTcyNjUyOTQ1Ni4wLjAuMA..