LoginSignup
9
3

More than 1 year has passed since last update.

Flutter ウィジェットを自由に重ねるOverlayを活用しよう

Last updated at Posted at 2023-05-31

ウィジェットを自由な位置に表示したい場合、Overlayを使うと便利です。

  • PopupMenuButtonでは文字列リストの表示は簡単ですが、デザインに制約があります。
  • Dialogを使用すればデザインに柔軟性を持たせることができますが、画面の中央に表示されてしまい、任意の位置に配置することはできません。

そんな場合には、Overlay機能を活用しましょう!
Overlayを使うと、ウィジェットを自由な位置に重ねて表示することができます。

↓ Googleのアカウント画面を模したサンプル

以下では、具体的なコードの説明を行います。

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表示するなどの対策が必要になってきます。
他にもっといい方法があればぜひ教えてください!

参考

9
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
3