Flutter では showDialog
というグローバル関数で簡単にダイアログを表示することができる。画面にオーバーレイして表示されるダイアログのような独自のUIコンポーネントを作るにはどうしたらいいんだろう?と思って showDialog
の中身をざっくり見てみた。
結論
結論から言うと、PopupRoute
という抽象クラスを継承して独自のオーバーレイ Route を作ることができる。
Flutter のダイアログはどのように実装されているんだろうか?
showDialog
は内部的に showGeneralDialog
を呼び出している。showGeneralDialog
は pageBuilder
という引数で渡された builder を元にダイアログを作る。builder にはダイアログに表示したいなんらかの Widget を返す関数を指定する。
showGeneralDialog
の実装は実質これだけ:
return Navigator.of(context, rootNavigator: true).push(new _DialogRoute<T>(
pageBuilder: pageBuilder,
barrierDismissible: barrierDismissible,
barrierLabel: barrierLabel,
barrierColor: barrierColor,
transitionDuration: transitionDuration,
transitionBuilder: transitionBuilder,
));
引数として渡された context
を元に Navigator
を取得し、その Navigator
に _DialogRoute
というものを push している。こうやってダイアログの表示も Navigator
が一種のルーティングとして処理しているのか。だからダイアログを閉じる時に Navigator.pop(context);
とやるんだな。
_DialogRoute
は PopupRoute
という抽象クラスを継承したプライベートクラス。継承ツリーは最終的に Route という抽象クラスに行き着く。
_DialogRoute -> PopupRoute -> ModalRoute -> [TransitionRoute, LocalHistoryRoute] -> OverlayRoute -> Route
ポップアップメニューなんかも ModalRoute を使って実装されているっぽい。
通常の画面遷移のための Route
はどのように実現されているんだろうか?
通常の画面遷移には MaterialPageRoute
というクラスが使われる。MaterialApp
というアプリケーションの根っこのところに配置するコンポーネントの初期化のときに routes
という引数でルーティングの設定を渡せるんだけど、
MaterialApp(
title: 'Flutter Demo',
theme: theme(context),
initialRoute: '/',
routes: {
'/': (context) => FirstScreen(),
'/signup': (context) => SignUpScreen(),
'/login': (context) => LoginScreen(),
},
);
これらは最終的に MaterialPageRoute として Navigator に処理される。
Route<dynamic> _onGenerateRoute(RouteSettings settings) {
...
return new MaterialPageRoute<dynamic>(
builder: builder,
settings: settings,
);
...
}
WidgetsApp
クラスが内部で Navigator
を初期していて、そのときに onGenerateRoute
として routes
を作る Factory (RouteFactory)が渡される。WidgetsApp
というのは MaterialApp
や CupertinoApp
がその build
メソッドで返すウィジェットで、
A convenience class that wraps a number of widgets that are commonly required for an application.
とのこと。
MaterialPageRoute
は PageRoute
のサブクラスで、さらに継承ツリーをたどると ModalRoute
が PopupRoute
と共通の親クラスだということがわかる。以下のような感じ:
_DialogRoute -> PopupRoute -> ModalRoute -> ...
MaterialPageRoute -> PageRoute -> ModalRoute -> ...
PopupRoute
と PageRoute
の違い
PopupRoute
A modal route that overlays a widget over the current route.
PageRoute
A modal route that replaces the entire screen.
PopupRoute
と PageRoute
の実装上の一番大きな違いは opaque
というプロパティの値の違い。PopupRoute
の実装の一部をみて見ると、
abstract class PopupRoute<T> extends ModalRoute<T> {
...
@override
bool get opaque => false;
opaque
が false を返すようになっている。それだけで半透明のスクリーンを表示できるのか。
opaque
は TransitionRoute
で定義されているプロパティで、このフラグをもとに overlayEntries
というおそらくページの各レイヤーを保持しているっぽい配列の最初の要素の opaque
フラグを設定している。overlayEntries
は OverlayRoute
のプロパティで、OverlayEntry
の配列。OverlayEntry
は builder
というプロパティを持っていることから推測できるようにやっぱりページの各レイヤーを作る為のものなんだろう。(後で出てくるけど Overlay
ウィジェットで使われるデータらしい)
Navigator#build
は以下のような実装になっている:
@override
Widget build(BuildContext context) {
assert(!_debugLocked);
assert(_history.isNotEmpty);
return new Listener(
onPointerDown: _handlePointerDown,
onPointerUp: _handlePointerUpOrCancel,
onPointerCancel: _handlePointerUpOrCancel,
child: new AbsorbPointer(
absorbing: false, // it's mutated directly by _cancelActivePointers above
child: new FocusScope(
node: focusScopeNode,
autofocus: true,
child: new Overlay(
key: _overlayKey,
initialEntries: _initialOverlayEntries,
),
),
),
);
}
Overlay()
の引数として渡される _initialOverlayEntries
には Navigator
の _history
が保持している全ての route
から取得した overlayEntries
が全て入っている。つまり route
として表現されるダイアログやページは最終的には Overlay に Widget として積み上げられたものなんだな。まあ Flutter において全てのグラフィカル要素は Widget なのでそうなるよな。
ところで Overlay とは:
A [Stack] of entries that can be managed independently.
Overlays let independent child widgets "float" visual elements on top of
other widgets by inserting them into the overlay's [Stack]. The overlay lets
each of these widgets manage their participation in the overlay using
[OverlayEntry] objects.
上のコードに出てくる Listener
と AbsorbPointer
、FocusScope
についても調べないと。