ウィジェットを自由な位置に表示したい場合、Overlayを使うと便利です。
-
PopupMenuButton
では文字列リストの表示は簡単ですが、デザインに制約があります。 -
Dialog
を使用すればデザインに柔軟性を持たせることができますが、画面の中央に表示されてしまい、任意の位置に配置することはできません。
そんな場合には、Overlay機能を活用しましょう!
Overlayを使うと、ウィジェットを自由な位置に重ねて表示することができます。
以下では、具体的なコードの説明を行います。
Overlayの実装コード
home.dart
class _MyHomePageState extends State<MyHomePage> {
final GlobalKey _actionKey = GlobalKey();
OverlayEntry? _menuOverlayEntry;
void _openMenu() {
// _actionKeyが関連付けられたウィジェット(この場合はAppBar)のサイズや座標情報を持ったRenderBoxを取得
final renderBox = _actionKey.currentContext?.findRenderObject() as RenderBox;
const menuWidth = 200.0;
// メニューを表示するためのOverlayEntryを作成
_menuOverlayEntry = OverlayEntry(builder: (context) {
return Stack(
children: [
// メニュー外をタップした時にもメニューを閉じれるように透明な背景を画面全体に表示
Positioned.fill(
child: GestureDetector(
onTap: _closeMenu,
child: Container(color: Colors.transparent),
),
),
// メニューを表示するWidget
Positioned(
// デバイスの幅からメニューの幅と余白を引いた位置に表示
left: MediaQuery.of(context).size.width - menuWidth - 24,
// AppBarの高さの半分の位置に表示
top: renderBox.size.height / 2,
width: menuWidth,
child: AccountMenu(
onClose: _closeMenu,
),
),
],
);
});
// メニューを表示
Overlay.of(context).insert(_menuOverlayEntry!);
}
void _closeMenu() {
// メニューを閉じる
_menuOverlayEntry?.remove();
_menuOverlayEntry = null;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
key: _actionKey, // _openMenu()内でWidgetサイズを取得するためにkeyを設定
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text('Sample'),
actions: [
IconButton(
icon: const Icon(Icons.account_circle),
onPressed: () => _openMenu(),
),
],
),
body: const SizedBox(),
);
}
}
- Dialogと同じように、Overlay表示したメニュー以外の部分をタップしたときにも閉じれるように
Stackで透明なContainerを設置している点がポイントです。 - 表示位置は仕様に合わせて調整してください。下記のようにrenderBoxから座標も取得できるのでこれを活用してもいいかもです。
// グローバル座標を取得。この場合はAppBarの左上の座標。
final appBarTopLeftPosition = renderBox.localToGlobal(Offset.zero);
// グローバル座標を取得。この場合はAppBarの右下の座標。
final appBarBottomRightPosition = renderBox
.localToGlobal(renderBox.size.bottomRight(Offset.zero));
全文コード 呼び出し側
home.dart
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final GlobalKey _actionKey = GlobalKey();
OverlayEntry? _menuOverlayEntry;
void _openMenu() {
// _actionKeyが関連付けられたウィジェット(この場合はAppBar)のサイズや座標情報を持ったRenderBoxを取得
final renderBox = _actionKey.currentContext?.findRenderObject() as RenderBox;
const menuWidth = 200.0;
// メニューを表示するためのOverlayEntryを作成
_menuOverlayEntry = OverlayEntry(builder: (context) {
return Stack(
children: [
// メニュー外をタップした時にもメニューを閉じれるように透明な背景を画面全体に表示
Positioned.fill(
child: GestureDetector(
onTap: _closeMenu,
child: Container(color: Colors.transparent),
),
),
// メニューを表示するWidget
Positioned(
left: MediaQuery.of(context).size.width - menuWidth - 24, // デバイスの幅からメニューの幅と余白を引いた位置に表示
top: renderBox.size.height / 2, // AppBarの高さの半分の位置に表示
width: menuWidth,
child: AccountMenu(
onClose: _closeMenu,
),
),
],
);
});
// メニューを表示
Overlay.of(context).insert(_menuOverlayEntry!);
}
void _closeMenu() {
// メニューを閉じる
_menuOverlayEntry?.remove();
_menuOverlayEntry = null;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
key: _actionKey, // _openMenu()内でWidgetサイズを取得するためにkeyを設定
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text('Sample'),
actions: [
IconButton(
icon: const Icon(Icons.account_circle),
onPressed: () => _openMenu(),
),
],
),
body: const SizedBox(),
);
}
}
全文コード Menuクラス
account_menu.dart
class AccountMenu extends StatelessWidget {
const AccountMenu({
Key? key,
required this.onClose,
}) : super(key: key);
final VoidCallback onClose;
@override
Widget build(BuildContext context) {
return Material(
elevation: 3,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const CircleAvatar(
backgroundColor: Colors.blue,
radius: 40,
child: Text(
'T',
style: TextStyle(
color: Colors.white,
fontSize: 30,
),
),
),
const SizedBox(height: 6),
const Text(
'田中 太郎',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 2),
Text(
'tanaka@gmail.co.jp',
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
const SizedBox(height: 8),
ElevatedButton(
onPressed: () {
onClose();
},
child: const Text('アカウント設定'),
),
],
),
),
],
),
);
}
}
Overlay取り扱い時の注意点
Overlayで表示したウィジェット(A)の上に更に新たなウィジェット(B)を重ねるときは注意が必要です。
(A)が常に最上位のウィジェットであると認識されているので、(B)を表示しても(A)の下に隠れてしまいます。
そのため、(A)を閉じてから(B)を表示するか、(A)の上から更に(B)をOverlay表示するなどの対策が必要になってきます。
他にもっといい方法があればぜひ教えてください!
参考