省エネ

Flutter、vue3修行中。

【Flutterやろうよ!】初級編 その4〜スクロール part2〜

こんにちは。
今日は、前回とは別パターンのスクーロールする画面を作って行きます!

ListViewという言葉を聞いた事はありますか?
前回のSingleChildScrollViewはchildrenに好きなViewをどんどん並べていくスタイルでしたね。

qkuronekop.hatenablog.jp

今回は同じViewを並べるスタイルになります。

コードはこれです!
どんっ!!

import 'package:flutter/material.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('【Flutterやろうよ!】初級編 その4〜スクロール part2〜'),
      ),
      body: ListView.builder(
          itemCount: 100,
          itemBuilder: (context, index) {
            return ListTile(
              title: Text('Item $index'),
            );
          }),
    );
  }
}

これを書くとこういう画面になります。

ListViewを使うとこうなります

ListView(
  children: []
)

という使い方もできますが、これだとSingleChildScrollView()と同じ感じになってしまいます。
ListView.builder()を使う事によって、連続するViewを作る事ができます。

しかも、これは今画面に見えているところのViewを作ってくれるので大変経済的(?)です。
データの多いリストを表示したい時にはこれを使ってください!

さらに、ListViewのカラムとカラムの間に罫線を入れたい場合には、こんな機能もあります。

import 'package:flutter/material.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('【Flutterやろうよ!】初級編 その4〜スクロール part2〜'),
      ),
      body: ListView.separated(
        itemCount: 100,
        itemBuilder: (context, index) {
          return ListTile(
            title: Text('Item $index'),
          );
        },
        separatorBuilder: (context, index) {
          return const Divider();
        },
      ),
    );
  }
}

ListViewに罫線をつける

という感じで、今回はListViewのご紹介でした。
ListViewはアプリでよく見る画面かと思いますので、どんどん活用していってください!
今回は、リストのカラムにListTile()というWIdgetを使っていますが、ここはどんなWidgetでも大丈夫ですので、お好きにカスタムしてみてくださいー!
同じようにセパレートもDivider()を使っていますが、ここも好きなWidgetが使えますので、いろいろ試してみてください。
それでは、今回はここまでです。

flutter3.27.1使ってみた

こんにちは。
flutterの3.27.1が出ましたので、使ってみました。
いろいろ機能が追加されているみたいですね。

すぐ使えそうな便利機能といえばこれ。

Column(
          spacing: 16,
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        )

 befor after

こんな感じで等間隔にスペースを入れる事ができるようになりました!
今でだとSizedBox()とかPadding()とかで間隔を入れてたんじゃないでしょうか。
これを使うと等間隔のUIが作りやすくなりますね!


お次はSegmentedButton()

SegmentedButton

これ、使った事ないのですが今までは横方向にしか作れなかったけど、directionが追加されて縦方向にもできるようになったっぽいです。


あとは、CupertinoWidget系の忠実度を上げたよって感じですかね。
Navigatorにも何か変更があったようですが、よく分からなかったので調査したらまた書くかもです。

Linux MintでEdgeを使う

Edgeとはドット絵を作成する為の便利なソフトの事です。

takabosoft.com

ここからEdgeをダウンロードし解凍しておきます。
Wineというソフトを使うとwin用のexeファイルが起動できるようになるらしいということで、インストール!

edge.exeをどこか適当なディレクトリにおいておき、そのディレクトリ上でwine edge.exeとすると、なんとEdgeが起動しました。

しかし、このままだと日本語が豆腐になっています。
なのでここを参考にwinetricksを使って日本語フォントをインストールします。

panomegon.com

無事に日本語メニューが表示されました。 わーい。

これで色々作れそうですね!

ついでにウディタも入れてみたけど、フォントがないようで特殊文字が豆腐になってしまいました。。。 しかし、Linuxでウディタ製のゲームが遊べるのは嬉しい!

Linux Mint22で遊んでます

やったこと

  • 外付けSSDにLinux Mint22をイ-ンストール
  • Flutterの開発環境を構築
  • Go langの開発環境を構築
  • Ebitenginを動かす

LinuxMint22インストール

やり方は先輩方が色々ネット記事に落としてくださっているのでぐぐると簡単にできるかと思います。

our-happyhome.com

書き込み先を内蔵のSSDではなく、外付けのSSDにする方法はこちらの記事を参考にさせていただきました。

Flutterのインストール

  • まずはHomebrewをインストール
  • 公式サイトにインストール方法が書いているので簡単に入ります。
  • Homebrewでfvmをインストール
  • fvmでflutter3.24.5(現時点で最新)をインストール
  • VSCodeをインストール
  • flutterコマンドで新規アプリを作成する
  • fvm flutter runしてみる
  • Linuxアプリを起動しようとするも失敗するのだが、コンソールにこれもいれてっていうのを色々表示してくれるので案内通りにインストールする
  • Linuxアプリ起動成功
  • Chromeをインストールする
  • Webアプリ起動成功

今ここです。
AndroidSDKを入れればAndroidアプリも作れそうですが、AndroidStudioのインストールやりかけで躓いております。

Go Langの開発観光構築

  • HomebrewでGo langをインストール!
  • Hello Worldに成功
  • ebitenginを導入

ebitengine.org

ここを見て入れればいける!

感想

FlutetrとGoがあればなんでも作れそうですね!
インストールはLinux初心者の私でも簡単にできました。
ちょっと面倒なのは、日本語と英字の切り替えがちょっと面倒ですね。
普段ボタン1つでやっているので。。。
今後はFlutterでAndroidアプリも動かせるようにしていきたいです。

【Flutter】Supabaseで匿名ログイン

こんにちは. Supabaseに匿名ログインの機能が搭載されたときいて、早速使ってみました。
いつの間にこんな便利な機能が搭載されていたんだ!!?
つい最近、Firebaseの匿名ログインを使って作ったアプリをSupabaseに乗り換えたい。

そんなこんなで早速、アプリに投入していきます。
まずはsupabase_flutterの最新バージョンを追加します。

supabase_flutter: ^2.8.0

Supabaseの諸々セットアップはここでは割愛します。
あとは、もうこれだけで匿名ログインが使えます。

final response = await Supabase.instance.client.auth.signInAnonymously();

次は、Supabaseコンソール側の設定。
匿名ログインをONにしなければいけません。

supabase console
左側のメニューからProject Settingsを選びます。

表示されたメニューからAuthenticationを選びます。

sipabase console

Allow anonymous sign-insのスイッチをONにして、ちょっとしたにある「Save」ボタンを押してください!

これで設定は完了です。

余談ですが。。。 最初はここを探していました。

supabase コンソール

firebaseののりでここを探していたのですが、ここでは一生見つからない。

匿名ログイン便利ですよね。
本格的にログインしなくてもお試しでちょっとアプリを使ってもらいたいとか、ゲームアプリとか色々使えそうです!
今までFirebaseを使っていたのですが、データ保存にやっぱりRDBを使いたいのでそのままユーザーデータかつDBも同じサービス内で使えるので嬉しいです。

久々にゲーム作ったので備忘録

こんにちは。
久しぶりにflameを使ってゲーム作ってみました。
とはいえ、こちらのチュートリアルのほぼコピーなのですが、一部そのままだとバグっているところがあったので、直しています。
久々でComponentの作り方から忘れていたので、色々思い出せました。

blog.flutteruniv.com

作ったものはGithub pagesで公開しています。

qkuronekop.github.io

何度かリロードしないと動かないかも。

なおしたのは、壁の当たり判定。

ball.dart

  void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {
    if (other is ScreenHitbox) {
      final screenHitBoxRect = other.toAbsoluteRect();
      for (final point in intersectionPoints) {
        if (point.x == screenHitBoxRect.left && !isCollidedScreenHitBoxX) {
          velocity.x = -velocity.x;
          isCollidedScreenHitBoxX = true;
        }
        if (point.x.toInt() == screenHitBoxRect.right.toInt() &&
            !isCollidedScreenHitBoxX) {
          velocity.x = -velocity.x;
          isCollidedScreenHitBoxX = true;
        }
        if (point.y == screenHitBoxRect.top && !isCollidedScreenHitBoxY) {
          velocity.y = -velocity.y;
          isCollidedScreenHitBoxY = true;
        }
        if (point.y.toInt() == screenHitBoxRect.bottom.toInt() &&
            !isCollidedScreenHitBoxY) {
          removeFromParent();
        }
      }
      super.onCollision(intersectionPoints, other);
    }
  }

壁の右端と下部の境界がイコールなら壁にあたったものとするという判定をdoubleのままでしていたのですが、小数点以下で微妙に値が違っていて、if文をすり抜けてしまいました。
なので、intに直してから判定しています。

もう一つはここです。

my_text_button.dart

  MyTextButton(String text,
      {required this.onTapDownMyTextButton, required this.renderMyTextButton})
      : super(
          text: text,
          size: Vector2(300, 60),
          anchor: Anchor.centerLeft,
        );
anchor: Anchor.center

だと、テキストに枠をつけると左にずれていってしまうのでcenterLeftにしました。

実はもう一つバグっているところがあったのですが、それは直せませんでした。
いけてる解決方法があったら教えて欲しいです。

GameOverなると、『GameOver』ボタンが出てくるのですが、そのボタンを押すと一旦ブロックを全消しします。
isClearedの判定条件が、ブロックが全部なくなることなのでそのタイミングで『Clear!』ボタンも出てしまうんですよね。
なんとか大きく変更せずに直せないかと、判定条件をあれこれ模索してみたのですが、結局直せませんでした。

根本的にロジックを変えればいけると思うのですが、修正コードを極力少なくしたいんですよね。
まぁ、解決しなかったんですが。

それでは、次のゲームに取り掛かろうかと思います。

【Flutter】 OverlayPortalの使い方

こんにちは。
FlutterのOverlayPortalを使って、画面の上にかぶせるようなUIを作ったのでその話をしたいと思います。

api.flutter.dev

OverlayPortalについては上のドキュメントを一度読んでいただけるとどんなものかがわかるかと思います。

今回作ったのはこういうものです。

画面サンプル1

こういう画面があります。
とある画面の上にBottomSheetが出ています。
そのBottomSheetの下の方にIconButtonが3つ並んでいます。

現在のコードがこれです。

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

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        const Expanded(child: SizedBox.shrink()),
        Container(
          margin:
              const EdgeInsets.symmetric(horizontal: 16).copyWith(bottom: 16),
          padding: const EdgeInsets.symmetric(vertical: 8),
          decoration: BoxDecoration(
            color: Colors.white,
            borderRadius: BorderRadius.circular(24),
          ),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              IconButton(
                  onPressed: () {},
                  icon: const Icon(
                    Icons.add,
                    color: Colors.purple,
                  )),
              IconButton(
                  onPressed: () {},
                  icon: const Icon(
                    Icons.remove,
                    color: Colors.amber,
                  )),
              IconButton(
                  onPressed: () {},
                  icon: const Icon(
                    Icons.close,
                    color: Colors.blueAccent,
                  )),
            ],
          ),
        )
      ],
    );
  }
}

ボタンを押すと、画面上にOverlayされたWidgetが出現するように書き換えてみましょう。

画面サンプル2

これは「+」のボタンを押すと左上に四角のOverlayを表示します。

import 'package:flutter/material.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('OverlayPortal'),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            showModalBottomSheet(
                context: context,
                builder: (context) => const OverlayPortalModalScreen());
          },
          child: const Text('Open Overlay Portal'),
        ),
      ),
    );
  }
}

class OverlayPortalModalScreen extends StatefulWidget {
  const OverlayPortalModalScreen({super.key});

  @override
  State createState() => _OverlayPortalModalScreenState();
}

class _OverlayPortalModalScreenState extends State<OverlayPortalModalScreen> {
  final _addButtonOverlayController = OverlayPortalController();

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        const Expanded(child: SizedBox.shrink()),
        Container(
          margin:
              const EdgeInsets.symmetric(horizontal: 16).copyWith(bottom: 16),
          padding: const EdgeInsets.symmetric(vertical: 8),
          decoration: BoxDecoration(
            color: Colors.white,
            borderRadius: BorderRadius.circular(24),
          ),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              OverlayPortal(
                controller: _addButtonOverlayController,
                overlayChildBuilder: (context) => _OverlayView(),
                child: IconButton(
                    onPressed: () {
                      _addButtonOverlayController.toggle();
                    },
                    icon: const Icon(
                      Icons.add,
                      color: Colors.purple,
                    )),
              ),
              IconButton(
                  onPressed: () {},
                  icon: const Icon(
                    Icons.remove,
                    color: Colors.amber,
                  )),
              IconButton(
                  onPressed: () {},
                  icon: const Icon(
                    Icons.close,
                    color: Colors.blueAccent,
                  )),
            ],
          ),
        )
      ],
    );
  }
}

class _OverlayView extends StatefulWidget {
  @override
  State createState() => _OverlayViewState();
}

class _OverlayViewState extends State<_OverlayView> {
  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
        color: Colors.transparent,
        borderRadius: BorderRadius.circular(24),
      ),
      child: Stack(
        children: [
          Positioned(
            top: 0,
            left: 0,
            child: Container(
              width: 100,
              height: 100,
              color: Colors.purple,
            ),
          )
        ],
      ),
    );
  }
}
Positioned(
  top: 0,
  left: 0,
  child: Container(
    width: 100,
    height: 100,
    color: Colors.purple,
  ),
)

四角を描画しているのはこの部分です。
top: 0、left: 0というのは画面の一番左上を指していますね。

これをボタンのすぐ上に表示したいと思います。
実際はボタンの上ではなく、ボタンの親Widgetである白いContainerの上に出したいです。

どうするかと言いますと、まずはこの白いContainerの位置を特定したいと思います。

class _OverlayPortalModalScreenState extends State<OverlayPortalModalScreen> {
  final _addButtonOverlayController = OverlayPortalController();

  final GlobalKey _parentWidgetKey = GlobalKey();

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        const Expanded(child: SizedBox.shrink()),
        Container(
          key: _parentWidgetKey,
          margin:
              const EdgeInsets.symmetric(horizontal: 16).copyWith(bottom: 16),
          padding: const EdgeInsets.symmetric(vertical: 8),
          decoration: BoxDecoration(
            color: Colors.white,
            borderRadius: BorderRadius.circular(24),
          ),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              OverlayPortal(
                controller: _addButtonOverlayController,
                overlayChildBuilder: (context) => _OverlayView(),
                child: IconButton(
                    onPressed: () {
                      _addButtonOverlayController.toggle();
                    },
                    icon: const Icon(
                      Icons.add,
                      color: Colors.purple,
                    )),
              ),
              IconButton(
                  onPressed: () {},
                  icon: const Icon(
                    Icons.remove,
                    color: Colors.amber,
                  )),
              IconButton(
                  onPressed: () {},
                  icon: const Icon(
                    Icons.close,
                    color: Colors.blueAccent,
                  )),
            ],
          ),
        )
      ],
    );
  }
}

こんな具合にGlobalKeyを設定します。

RenderBox? renderBox = _parentWidgetKey.currentContext?.findRenderObject() as RenderBox?;
final topPosition = renderBox?.localToGlobal(Offset.zero).dy;

こんな計算をするとGlobalKeyを設定したWidgetの画面全体からみた位置を取得できます。

ただし、位置は画面描画後にしか取得できないので気をつけてください。

class _OverlayPortalModalScreenState extends State<OverlayPortalModalScreen> {
  final _addButtonOverlayController = OverlayPortalController();

  final GlobalKey _parentWidgetKey = GlobalKey();

  double top = 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        const Expanded(child: SizedBox.shrink()),
        Container(
          key: _parentWidgetKey,
          margin:
              const EdgeInsets.symmetric(horizontal: 16).copyWith(bottom: 16),
          padding: const EdgeInsets.symmetric(vertical: 8),
          decoration: BoxDecoration(
            color: Colors.white,
            borderRadius: BorderRadius.circular(24),
          ),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              OverlayPortal(
                controller: _addButtonOverlayController,
                overlayChildBuilder: (context) => _OverlayView(
                  top: top,
                ),
                child: IconButton(
                    onPressed: () {
                      RenderBox? renderBox = _parentWidgetLey.currentContext
                          ?.findRenderObject() as RenderBox?;
                      final topPosition =
                          renderBox?.localToGlobal(Offset.zero).dy;
                      setState(() {
                        top = topPosition ?? 0;
                      });
                      _addButtonOverlayController.toggle();
                    },
                    icon: const Icon(
                      Icons.add,
                      color: Colors.purple,
                    )),
              ),
              IconButton(
                  onPressed: () {},
                  icon: const Icon(
                    Icons.remove,
                    color: Colors.amber,
                  )),
              IconButton(
                  onPressed: () {},
                  icon: const Icon(
                    Icons.close,
                    color: Colors.blueAccent,
                  )),
            ],
          ),
        )
      ],
    );
  }
}

class _OverlayView extends StatefulWidget {
  const _OverlayView({required this.top});

  final double top;

  @override
  State createState() => _OverlayViewState();
}

class _OverlayViewState extends State<_OverlayView> {
  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
        color: Colors.transparent,
        borderRadius: BorderRadius.circular(24),
      ),
      child: Stack(
        children: [
          Positioned(
            top: widget.top,
            left: 0,
            child: Container(
              width: 100,
              height: 100,
              color: Colors.purple,
            ),
          )
        ],
      ),
    );
  }
}

こう書き換えてみました。
するとここに表示されます。

画面サンプル3
ちょっと思ってたのと違いますね。
もうちょっと上に表示したいです。

表示しているWIdget分の高さ+余白分を引いてあげます。

class _OverlayViewState extends State<_OverlayView> {
  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
        color: Colors.transparent,
        borderRadius: BorderRadius.circular(24),
      ),
      child: Stack(
        children: [
          Positioned(
            top: widget.top - 100 - 8,
            left: 0,
            child: Container(
              width: 100,
              height: 100,
              color: Colors.purple,
            ),
          )
        ],
      ),
    );
  }
}

これで、こう!

画面サンプル4

だんだんいい感じになってきました。
ただ、この場合は縦、横のサイズのわかっているWidgetを表示しているので計算がしやすいです。
実際は、高さの分からないWidgetを表示する事が多いのかなと思います。
たとえばContainerの中に文字やアイコン(画像)を表示するWidgetとかね。
その場合は、OverlayさせるWidgetのサイズも計算します。

class _OverlayViewState extends State<_OverlayView> {
  final GlobalKey _overlayWidgetKey = GlobalKey();

  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
        color: Colors.transparent,
        borderRadius: BorderRadius.circular(24),
      ),
      child: Stack(
        children: [
          Positioned(
            top: widget.top - 100 - 8,
            left: 0,
            child: Container(
              key: _overlayWidgetKey,
              padding: const EdgeInsets.all(4),
              decoration: BoxDecoration(
                color: Colors.white,
                borderRadius: BorderRadius.circular(24),
              ),
              child: const Text('プラスボタンが押されました。'),
            ),
          )
        ],
      ),
    );
  }
}

まずはこんな感じでOverlayするWidgetを書き換えてみました。

画面サンプル5
いい感じの位置に表示する為にOverlayしたWidgetの高さを計算してみましょう。

class _OverlayViewState extends State<_OverlayView> {
  final GlobalKey _overlayWidgetKey = GlobalKey();

  double overlayHeight = 0;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
      RenderBox? renderBox =
          _overlayWidgetKey.currentContext?.findRenderObject() as RenderBox?;
      final height = renderBox?.size.height;
      setState(() {
        overlayHeight = height ?? 0;
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
        color: Colors.transparent,
        borderRadius: BorderRadius.circular(24),
      ),
      child: Stack(
        children: [
          Positioned(
            top: widget.top - overlayHeight - 8,
            left: 0,
            child: Container(
              key: _overlayWidgetKey,
              padding: const EdgeInsets.all(4),
              decoration: BoxDecoration(
                color: Colors.white,
                borderRadius: BorderRadius.circular(24),
              ),
              child: const Text('プラスボタンが押されました。'),
            ),
          )
        ],
      ),
    );
  }
}

こうです!
左右の位置もいい感じにしたいですよね。
それには、タップしたボタンの位置と幅を取得して、 Overlayの横幅とごにょごにょ計算してあげればいい感じにできます。
かつOverlayしたWidgetが画面からはみ出た場合には、表示位置を調整して画面内に収まるようにしてあげます。
ここからは一気にやってしまいます。
はい、どんっ。

class OverlayPortalModalScreen extends StatefulWidget {
  const OverlayPortalModalScreen({super.key});

  @override
  State createState() => _OverlayPortalModalScreenState();
}

class _OverlayPortalModalScreenState extends State<OverlayPortalModalScreen> {
  final _plusButtonOverlayController = OverlayPortalController();
  final _minusButtonOverlayController = OverlayPortalController();
  final _closeButtonOverlayController = OverlayPortalController();

  final GlobalKey _parentWidgetKey = GlobalKey();
  final GlobalKey _plusButtonKey = GlobalKey();
  final GlobalKey _minusButtonKey = GlobalKey();
  final GlobalKey _closeButtonKey = GlobalKey();

  double top = 0;
  double buttonWidth = 0;
  double overlayLeft = 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        const Expanded(child: SizedBox.shrink()),
        Container(
          key: _parentWidgetKey,
          margin:
              const EdgeInsets.symmetric(horizontal: 16).copyWith(bottom: 16),
          padding: const EdgeInsets.symmetric(vertical: 8),
          decoration: BoxDecoration(
            color: Colors.white,
            borderRadius: BorderRadius.circular(24),
          ),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              OverlayPortal(
                controller: _plusButtonOverlayController,
                overlayChildBuilder: (context) => _OverlayView(
                  controller: _plusButtonOverlayController,
                  top: top,
                  buttonWidth: buttonWidth,
                  buttonLeft: overlayLeft,
                  label: 'プラスボタンが押されました',
                ),
                child: IconButton(
                    key: _plusButtonKey,
                    onPressed: () {
                      RenderBox? renderBox = _parentWidgetKey.currentContext
                          ?.findRenderObject() as RenderBox?;
                      final topPosition =
                          renderBox?.localToGlobal(Offset.zero).dy;

                      RenderBox? buttonBox = _plusButtonKey.currentContext
                          ?.findRenderObject() as RenderBox?;
                      setState(() {
                        top = topPosition ?? 0;
                        buttonWidth = buttonBox?.size.width ?? 0;
                        overlayLeft =
                            buttonBox?.localToGlobal(Offset.zero).dx ?? 0;
                      });
                      _plusButtonOverlayController.toggle();
                    },
                    icon: const Icon(
                      Icons.add,
                      color: Colors.purple,
                    )),
              ),
              OverlayPortal(
                controller: _minusButtonOverlayController,
                overlayChildBuilder: (context) => _OverlayView(
                  controller: _minusButtonOverlayController,
                  top: top,
                  buttonWidth: buttonWidth,
                  buttonLeft: overlayLeft,
                  label: 'マイナスボタンが押されました',
                ),
                child: IconButton(
                    key: _minusButtonKey,
                    onPressed: () {
                      RenderBox? renderBox = _parentWidgetKey.currentContext
                          ?.findRenderObject() as RenderBox?;
                      final topPosition =
                          renderBox?.localToGlobal(Offset.zero).dy;

                      RenderBox? buttonBox = _minusButtonKey.currentContext
                          ?.findRenderObject() as RenderBox?;
                      setState(() {
                        top = topPosition ?? 0;
                        buttonWidth = buttonBox?.size.width ?? 0;
                        overlayLeft =
                            buttonBox?.localToGlobal(Offset.zero).dx ?? 0;
                      });
                      _minusButtonOverlayController.toggle();
                    },
                    icon: const Icon(
                      Icons.remove,
                      color: Colors.red,
                    )),
              ),
              OverlayPortal(
                  controller: _closeButtonOverlayController,
                  overlayChildBuilder: (context) {
                    return _OverlayView(
                      controller: _closeButtonOverlayController,
                      top: top,
                      buttonWidth: buttonWidth,
                      buttonLeft: overlayLeft,
                      label: '閉じるボタンが押されました',
                    );
                  },
                  child: IconButton(
                      key: _closeButtonKey,
                      onPressed: () {
                        RenderBox? renderBox = _parentWidgetKey.currentContext
                            ?.findRenderObject() as RenderBox?;
                        final topPosition =
                            renderBox?.localToGlobal(Offset.zero).dy;

                        RenderBox? buttonBox = _closeButtonKey.currentContext
                            ?.findRenderObject() as RenderBox?;
                        setState(() {
                          top = topPosition ?? 0;
                          buttonWidth = buttonBox?.size.width ?? 0;
                          overlayLeft =
                              buttonBox?.localToGlobal(Offset.zero).dx ?? 0;
                        });
                        _closeButtonOverlayController.toggle();
                      },
                      icon: const Icon(
                        Icons.close,
                        color: Colors.blueAccent,
                      ))),
            ],
          ),
        )
      ],
    );
  }
}

class _OverlayView extends StatefulWidget {
  const _OverlayView(
      {required this.top,
      required this.buttonWidth,
      required this.buttonLeft,
      required this.controller,
      required this.label});

  final double top;
  final double buttonWidth;
  final double buttonLeft;
  final OverlayPortalController controller;
  final String label;

  @override
  State createState() => _OverlayViewState();
}

class _OverlayViewState extends State<_OverlayView> {
  final GlobalKey _overlayWidgetKey = GlobalKey();

  double overlayHeight = 0;
  double overlayWidth = 0;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
      RenderBox? renderBox =
          _overlayWidgetKey.currentContext?.findRenderObject() as RenderBox?;
      setState(() {
        overlayHeight = renderBox?.size.height ?? 0;
        overlayWidth = renderBox?.size.width ?? 0;
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        widget.controller.toggle();
      },
      child: Container(
        decoration: BoxDecoration(
          color: Colors.transparent,
          borderRadius: BorderRadius.circular(24),
        ),
        child: Stack(
          children: [
            Positioned(
              top: widget.top - overlayHeight - 8,
              left: (widget.buttonLeft -
                          overlayWidth / 2 +
                          widget.buttonWidth / 2) <=
                      0
                  ? 16
                  : (widget.buttonLeft -
                              overlayWidth / 2 +
                              widget.buttonWidth / 2) >=
                          MediaQuery.of(context).size.width
                      ? null
                      : (widget.buttonLeft -
                          overlayWidth / 2 +
                          widget.buttonWidth / 2),
              right: (widget.buttonLeft -
                          overlayWidth / 2 +
                          widget.buttonWidth / 2) <=
                      0
                  ? null
                  : (widget.buttonLeft -
                              overlayWidth / 2 +
                              widget.buttonWidth / 2) >=
                          MediaQuery.of(context).size.width
                      ? MediaQuery.of(context).size.width - 16
                      : null,
              child: Container(
                key: _overlayWidgetKey,
                padding: const EdgeInsets.all(4),
                decoration: BoxDecoration(
                  color: Colors.white,
                  borderRadius: BorderRadius.circular(24),
                ),
                child: Text(widget.label),
              ),
            )
          ],
        ),
      ),
    );
  }
}

コードが汚いのは見逃してください。 実際のアプリで使う時には共通処理をまとめるなどして綺麗に描き直してくださいね!
さて、こうると各ボタンを押した時には、こうなります。
かつ、画面のどこかをタップするとOverlayが消えるようにしてみました。

画面サンプル6
という感じで今回はOverlayPotalとWidgetの位置、サイズの取り方でしたー!

コードはGithubにも上げています。
よろしければ参考にしてみてくださいー!

github.com