Visibility ウィジェットを深掘りしてみる(Flutter)
はじめに
Flutterの Visibility
ウィジェットのリファレンスを見てみると、結構使ったことのないプロパティがあったので以下について実際に触りながら調べてみました
- いつ使うのか、それぞれの役割
-
Visibility
を使うタイミング
Visibility
とは?
公式には以下のように記載されています。
Whether to show or hide a child.
By default, the visible property controls whether the child is included in the subtree or not; when it is not visible, the replacement child (typically a zero-sized box) is included instead.
A variety of flags can be used to tweak exactly how the child is hidden. (Changing the flags dynamically is discouraged, as it can cause the child subtree to be rebuilt, with any state in the subtree being discarded. Typically, only the visible flag is changed dynamically.)
(DeepL翻訳)
子供を見せるか隠すか。
デフォルトでは、visibleプロパティは、子プロパティがサブツリーに含まれるかどうかを制御します; 子プロパティが表示されない場合、代わりに置換子(通常はゼロサイズのボックス)が含まれます。
さまざまなフラグを使用して、子がどのように隠されるかを正確に調整することができます。(フラグを動的に変更することは推奨されません。子サブツリーが再構築され、サブツリーの状態が破棄される可能性があるからです。通常、可視フラグだけが動的に変更されます)。
様々な記事で紹介されていますので詳細は割愛させていただきますが、以下のような特徴を持つWidgetであることが読み取れます。
-
child
で指定したウィジェットの表示/非表示をvisible
プロパティで切り替える - 非表示は指定がなければ
zero-sized box
で表現される - フラグの指定によって、様々な「非表示の方法」を実現できる
今回は フラグの指定によって、様々な「非表示の方法」を実現できる
点について深掘りしていきます。
今回取り上げるプロパティ
今回取り上げるプロパティは以下です。「設定したらどうなるの?」と疑問に思ったものに絞りました。
maintainState
maintainInteractivity
maintainSemantics
maintainAnimation
それぞれについて、デモアプリを作ってみました。
デモアプリは公開しています。
maintainState
maintainState
プロパティは、Visibility
ウィジェットが非表示にされている時でも、その子ウィジェットの状態を保持するかどうかを制御します。これが true
に設定されている場合、子ウィジェットは画面からは消えますが、内部の状態(例えばテキストフィールドの内容やスクロール位置)は保持されます。これは、ユーザーが操作を再開した際に前の状態から容易に再開できるようにするために有用です。
使用例: 利用規約画面などの長いスクロール画面で、表示・非表示を切り替えたい時
maintainState: false
の場合
非表示時にスクロール位置が保持されていないのがわかります。
maintainState: true
の場合
非表示時にスクロール位置が保持されているのがわかります。
実装
Expanded(
child: Visibility(
maintainState: _maintainState, // スクロール位置を保持する
visible: _isVisible,
child: ListView.builder(
itemCount: 100, // 長いリストを生成
itemBuilder: (context, index) {
return ListTile(
title: Text('利用規約的な文章 $index'),
);
},
),
),
),
maintainInteractivity
maintainInteractivity
プロパティは、ウィジェットが視覚的には非表示であっても、ユーザーの操作(タップやスクロールなど)を受け付けるかどうかを制御します。
ちなみに、詳細ページを読んでみると、maintainInteractivity
を true
にしたいときは、 maintainSize
を true
にする必要があります。そして maintainSize
を true
にするためには maintainAnimation
が true
である必要があったりと芋づる式に true
が必要になっています。
Visibility
は、それぞれの maintain
プロパティによって最終的に使用するウィジェットが分かれており、それによって必須要素が変わるのでこのような制約がいくつかあります。
使用例: いい案が思いつかなかったです。実際にこんな用途で使っているよ!という方がいたら教えて欲しいです!
maintainInteractivity: true
にすると、非表示時でもボタンのタップができることがわかります。
実装
Visibility(
maintainSize: true, // maintainAnimation: trueが必要
maintainAnimation: true, // maintainState: trueが必要
maintainState: true,
maintainInteractivity: true, // maintainSize: trueが必要
visible: _isButtonVisible,
child: ElevatedButton(
onPressed: _incrementCounter,
child: const Text('Increment'),
),
),
maintainSemantics
maintainSemantics
プロパティは、ウィジェットが非表示になっても、アクセシビリティツール(スクリーンリーダーなど)によるそのウィジェットの認識を維持するかどうかを制御します。
使用例: 視覚障害のあるユーザーが使用するアプリで、一時的にコンテンツを非表示にするが、その情報をスクリーンリーダーを通じて引き続き提供したい場合
find.bySemanticsLabel()
でテストすることで、 maintainSemantics: true
が想定通りに動くことが確認できました。
実装
Widget build(BuildContext context) {
return Visibility(
visible: false,
maintainSemantics: true,
maintainSize: true,
maintainAnimation: true,
maintainState: true,
child: Text(
text,
semanticsLabel: text,
),
);
}
expect(find.bySemanticsLabel('Semantics!'), findsOneWidget);
maintainAnimation
maintainAnimation
プロパティは、ウィジェットが非表示になっている間も、その子のアニメーションを維持し続けるかを制御します。
AnimationController
を使って何か別のロジックを動かす際、アニメーションが非表示の場合にも継続して実行したい場合には maintainAnimation: true
を設定してあげる必要があります。
例えばanimationController.addListener()
の中で実行する処理などが該当します。
使用例: アニメーションの経過時間を非表示状態でも保持し続けたい場合?いい例が思いつかなかったです...
true
の方が挙動が直感的でわかりやすい気がするので true
から紹介します。
maintainAnimation: true
の場合
アニメーションが非表示の時にカウンターの数値が動き続けていることがわかります。
maintainAnimation: false
の場合
ちょっとわかりづらいのですが、アニメーションが非表示の時にカウンターの数値が止まっていることがわかります。
アニメーション停止時のカウンターの数値が true
の時と異なります。
実装
Visibility(
visible: _isVisible,
maintainState: true,
maintainAnimation: true,
child: const FadeLogo(),
),
class _FadeLogoState extends State<FadeLogo>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
int _counter = 0;
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: const Duration(seconds: 10),
);
_animationController.forward();
_animationController.addListener(() {
setState(() {
_counter++;
});
});
}
void dispose() {
_animationController.dispose();
super.dispose();
}
void _restartAnimation() {
setState(() {
_counter = 0;
});
_animationController.reset();
_animationController.forward();
}
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
FadeTransition(
opacity: _animationController,
child: const FlutterLogo(size: 200.0),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _restartAnimation,
child: const Text('Restart Animation'),
),
const SizedBox(height: 20),
Text(
'_counter: $_counter',
style: Theme.of(context).textTheme.displaySmall,
),
],
);
}
}
Visibility
を使うのか
いつ 私は、 Visibility
ウィジェットは「ウィジェットの非表示に何か別の制御を加えたい時」に使用するのが適切かと考えます。
公式にもそのようなニュアンスの内容が記載されています。
Using this widget is not necessary to hide children. The simplest way to hide a child is just to not include it, or, if a child must be given (e.g. because the parent is a StatelessWidget) then to use SizedBox.shrink instead of the child that would otherwise be included.
ウィジェットの表示・非表示を制御しながら、自分の思い通りに状態を管理する際には考慮するべきことがたくさんあります。
その手助けをしてくれるのが Visibility
ウィジェットということですね。
まとめ
内部実装にも入り込んで紹介したい点もいくつかありましたが、思ったより長くなったのでそれぞれのプロパティ紹介に留めさせていただきました。
こういったタイミングで一度深掘りしておくと、いざという時に「今このウィジェット使えるかも?」となるのがいいですよね。
個人的には maintainAnimation
周りは想定していた挙動と異なっていたので学びがありました。
Discussion