Overlayのススメ

# はじめに
こんな画面を作ることありませんか？

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/384422/4167ee5a-e9c1-a0d2-a34f-7a0c620c45ed.png" width="50%"/>

簡略化していますが、ヘルプのアイコンをタップするとアイコンの上にポップアップが表示される画面です。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/384422/713b27cf-3d7d-ccba-8527-83b0ab96a6a7.png" width="50%"/>

今年、業務の中でもう少し複雑な画面（アイコンの位置や表示するポップアップの形式はもう少し複雑でした😅）を
実装しましたので、その中でも一番つまずいたオーバーレイの表示方法についてススメを残そうと思います。

# 実装するにあたって調べたもの、NG集

https://pub.dev/packages/showcaseview/
コーチマークを実現するライブラリ、UIとしては完全踏襲できないことと表示が難しかったためNG

https://pub.dev/packages/popover
できることのイメージは近い感じだったが、表示位置の調整に手間取り却下

あとは標準的にSimpleDialogをshowDialogすることも試みましたが、
showDialogは表示位置がデフォルト中心なので、特定のオブジェクトのちょっと上みたいなことをすると難しかったです（画面サイズで描画位置が変わってしまっていた）

# やっと見つけたOverlay

こちらの記事が大変参考になりました:bow:
https://qiita.com/ling350181/items/cb3623b04a3328f2b125

決められたオブジェクトの位置に浮かせるように表示することができる。
というのを一番シンプルに実現することができました。

# コード

```dart
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Overlay Sample'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});
  
  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final _helpIconKey = const GlobalObjectKey("_helpIconKey_");

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: GestureDetector(
          onTap: () {
            _showHelpDescriptionOverlay(context);
          },
          child: Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Icon(Icons.help, key: _helpIconKey),
              const SizedBox(width: 4),
              const Text('Help'),
            ],
          ),
        ),
      ),
    );
  }

  void _showHelpDescriptionOverlay(BuildContext context) {
    // GlobalKeyでIconのBuildContextを取得
    final BuildContext? targetContext = _helpIconKey.currentContext;

    // targetContextからRenderBoxの取得。RenderObjectなのでcastが必要。
    final RenderBox? renderBox = targetContext?.findRenderObject() as RenderBox?;

    // Widgetの位置: ローカルでの位置から画面全体での位置に変換
    final Offset? offset = renderBox?.localToGlobal(Offset.zero);

    OverlayEntry? menuOverlayEntry;
    // メニューを表示するためのOverlayEntryを作成
    menuOverlayEntry = OverlayEntry(builder: (context) {
      return Stack(
        children: [
          // メニュー外をタップした時にもメニューを閉じれるように透明な背景を画面全体に表示
          Positioned.fill(
            child: GestureDetector(
              onTap: () {
                menuOverlayEntry?.remove();
                menuOverlayEntry = null;
              },
              // Colors.black54 は showDialog で設定されている背景色
              child: Container(color: Colors.black54),
            ),
          ),
          Positioned(
            left: offset!.dx - 80,
            top: offset.dy - 60,
            child: const HelpDescription(),
          ),
        ],
      );
    });
    // メニューを表示
    Overlay.of(context).insert(menuOverlayEntry!);
  }
}

class HelpDescription extends StatelessWidget {
  const HelpDescription({super.key});

  @override
  Widget build(BuildContext context) {
    return Container(
      alignment: Alignment.center,
      width: 200,
      height: 50,
      color: Colors.white,
      child: const Material(
        color: Colors.white,
        child: Text('これはヘルプマークの説明です', style: TextStyle(fontSize: 10, color: Colors.blue)),
      ),
    );
  }
}
```

# 最後に

実装時から若干時間が経ってしまい、記憶や感動が曖昧になってしまったのですが、
忘れるよりは残したいと思い、記事にさせていただきました。
こういったWidget系は知っているか否かで試行錯誤の時間が変わるので、
どなたかの助けになればと思います:bow: