FlutterのWidgetをコードを動かしながら学ぶ: Opacity & AnimatedOpacity編
FLutter は動画コンテンツも充実していて週毎にウィジェットを紹介しているFlutter Widget of the Weekでは短い動画でウィジェットの概要を掴むことができる動画を視聴できます。
Flutter Widget of the Week を見るだけでも勉強になるのですが、記憶力が良くないので、視聴によるインプットだけでなく手元でコードを動かしてそれをブログ記事にする所までやって後から思い出しやすいようにしてみるシリーズです。
これまでの記事は以下のリンクから参照できます。
今回取り上げるのは Opacity です。
※この記事ではウィジェットの動作とコードの確認を同時に行えるようにするために最近Flutter対応が発表されたcodepenの埋め込みを使用しています。codepenの閲覧及び表示されているウィジェットのインタラクティブな操作をスムーズに行う場合はChromeで閲覧することを推奨します。
Opacity とは
ドキュメントによると子Widgetを部分的に透明化するWidgetとあります。
ドキュメントは以下になります。
このクラスの特徴は動画にも説明があった通り、透明化する時に対象のWidget分のスペース(intermediate buffer
)を維持します。コンストラクタ引数のopacityにdouble型の0.0から1.0を指定します。
様々なUIフレームワークでopacityを設定する時と同じでOpacityを1.0にする不透明に0.0にすると完全に透明になります。以下のコードは縦に並んでいる内の中央のWidgetをOpacityでwrapしています。ただ、constructor引数のopacityには1.0を指定しているので表示されたままです。
opacityを0.0にしてみます。完全に非表示になりました。
See the Pen flutter_sample_opacity_2 by Nobuyuki Tanabe (@nabeatsu) on CodePen.
主要なプロパティ
opacityプロパティ
constructor引数opacityはOpacityのプロパティopacityに代入されますが、このopacityプロパティのソースコードを読むと1.0と0.0に関してはfast pathで描画されます。それ以外には描画にコストがかかります。
fast pathについてはドキュメントに記載はありませんでしたがFlutter EngineのソースコードやFlutter Engineのコア技術のSkiaという2Dレンダリングエンジンのソースコードにfast pathに関する記述が見られます。
Use fast path in Skia for rendering shadows. · Issue #6807 · flutter/flutterを見ると描画高速化のための概念であることが伺えますが、ここまでいくとC++で書かれたSkiaのソースコードを読むことになり、正確に紹介できるか怪しいのでここまでにとどめます。知見が溜まってきたら記事にしたいと思います。
OpacityのcreateRenderObjectメソッドの返り値の型はRenderOpacityです。RenderOpacityのドキュメンテーションコメントを読むと1.0と0.0の時とそれ以外の時の違いが詳細に記述されています。
/// For values of opacity other than 0.0 and 1.0, this class is relatively /// expensive because it requires painting the child into an intermediate /// buffer. For the value 0.0, the child is simply not painted at all. For the /// value 1.0, the child is painted immediately without an intermediate buffer. class RenderOpacity extends RenderProxyBox {
opacityが1.0だとinermediate buffer不要ですぐに描画され、一方0.0だと描画されないのでコストそれ以外と比較して少なく済むと記載されています。拙訳なのでわかりにくい場合は引用した部分を読んでください。
alwaysIncludeSemantics プロパティ
子のsemanticsに関する情報を含めるかどうかを決めるプロパティです。デフォルト値はfalseです。このプロパティをtrueにしておくとopacityが0.0で透明化した時などもsemanticsを含めたままにしてくれます。これによりopacityプロパティを0.0にして非表示になっていてもiOSのVoiceOverで選択できるようになります。
なおデフォルト値はfalseなので0.0の場合以下の画像のように空白部分をタップしても選択対象になりません。
semanticsというのがドキュメンテーションコメントから何を指すのかわからなかった上、trueとfalseを入れ替えても見かけ上の変化は何も起こらなかったので最初は困りました。
alwaysIncludeSemanticsに関するPRは以下になります。
アニメーションさせる場合
コンストラクタ引数に親Widgetから変数を渡してアニメーションさせることはユースケースとしてありがちです。
しかしこのWidgetを直接アニメーションさせると、Opacity と場合によってはそのサブツリーがフレームを再構築されます。そのような使い方をする場合にはAnimatedOpacityを使用を検討するように勧められています。
AnimatedOpacityの仕様例です。Opacity Widgetに比べてdurationというコンストラクタ引数が追加されていてアニメーションする間隔を指定する必要があります。
実際の仕様例です。更新ボタンを推すとロゴの表示非表示がアニメーションで切り替わります。
See the Pen flutter_animated_opacity_sample by Nobuyuki Tanabe (@nabeatsu) on CodePen.
Opacityを使ったワークアラウンド
Opacity Widgetが透明化した上でスペースを維持してくれるのを利用して画像の左寄せとテキストの中央寄せを実装するワークアラウンドを他の人が書いたアプリのコードを読んでいる時に見かけたのでそれを紹介して終わりにしたいと思います。
先にコードを示します。4つのビューのうち一番下が画像の左寄せとテキストの中央寄せをOpacityを使って実装したものです。
See the Pen vYNjpLr by Nobuyuki Tanabe (@nabeatsu) on CodePen.
上から順に説明します。
1つ目のウィジェットがテキストの中央よせです。ただ画像を加えることを想定してContainer WidgetのchildにCenter Widget、そのchildにText Widgetを渡していたこれまでの実装から、Container WidgetのchildにRow Widgetを渡してwidgetの配列をchildrenに渡せるようにしました。その上でmainAxisAlignmentをMainAxisAlignment.centerにして今までと同じ表示にしています。
2番目のウィジェットは1つ目のウィジェットのRow WidgetのchildrenにFlutterLogo Widgetを追加しました。contructor引数mainAxisAlighmentに渡した値が効いて中央寄せになっています。
3番目のウィジェットは2つ目のウィジェットのRow Widgetのconstructor引数mainAxisAlignmentにMainAxisAlignment.spaceBetweenを渡した上でRow Widgetのchildrenにもう一つFlutterLogo Widgetを追加しています。そうると右側のロゴが邪魔ですが画像の左寄せとテキストの中央寄せが簡単に実現できているのがわかります。
4番目のウィジェットは3つ目のウィジェットのRow Widgetのchildrenの末端のFlutterLogo WidgetをOpacity Widgetでwrapしています。Opacity Widgetのconstructor引数opacityには0.0を渡しています。先述の通り0.0の時の描画のコストは大したことないのでパフォーマンスで重大な問題になることもないと考えています。
まとめ
最後のワークアラウンドは他の人のアプリの実装を眺めている時に見つけたレイアウトの実装方法です。Opacityについて記事を書いているタイミングだったので丁度よいので紹介しました。最近は自分が使うためのアプリをFlutterで作り始めていますが引き続きこちらのシリーズも進めていきたいです。先にFlutterを始めてアウトプットを続けてくださっている人たちのおかげで様々な疑問がスムーズに解決されているので自分もコミュニティの一助となるよう丁寧に記事を書いていきたいと思います。
記事やその裏の認識に誤りがあるかもしれません。なにかお気づきの際はコメントかTwitterなら反応できるのでお知らせください。