【Flutterやろうよ!】初級編 その4〜スクロール part2〜
こんにちは。
今日は、前回とは別パターンのスクーロールする画面を作って行きます!
ListViewという言葉を聞いた事はありますか?
前回のSingleChildScrollViewはchildrenに好きなViewをどんどん並べていくスタイルでしたね。
今回は同じ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( 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はアプリでよく見る画面かと思いますので、どんどん活用していってください!
今回は、リストのカラムに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,
),
],
)


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


これ、使った事ないのですが今までは横方向にしか作れなかったけど、directionが追加されて縦方向にもできるようになったっぽいです。
あとは、CupertinoWidget系の忠実度を上げたよって感じですかね。
Navigatorにも何か変更があったようですが、よく分からなかったので調査したらまた書くかもです。
Linux MintでEdgeを使う
Edgeとはドット絵を作成する為の便利なソフトの事です。
ここからEdgeをダウンロードし解凍しておきます。
Wineというソフトを使うとwin用のexeファイルが起動できるようになるらしいということで、インストール!
edge.exeをどこか適当なディレクトリにおいておき、そのディレクトリ上でwine edge.exeとすると、なんとEdgeが起動しました。
しかし、このままだと日本語が豆腐になっています。
なのでここを参考にwinetricksを使って日本語フォントをインストールします。
無事に日本語メニューが表示されました。 わーい。
これで色々作れそうですね!
ついでにウディタも入れてみたけど、フォントがないようで特殊文字が豆腐になってしまいました。。。 しかし、Linuxでウディタ製のゲームが遊べるのは嬉しい!
Linux Mint22で遊んでます
やったこと
- 外付けSSDにLinux Mint22をイ-ンストール
- Flutterの開発環境を構築
- Go langの開発環境を構築
- Ebitenginを動かす
LinuxMint22インストール
やり方は先輩方が色々ネット記事に落としてくださっているのでぐぐると簡単にできるかと思います。
書き込み先を内蔵の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を導入
ここを見て入れればいける!
感想
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にしなければいけません。

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

Allow anonymous sign-insのスイッチをONにして、ちょっとしたにある「Save」ボタンを押してください!
これで設定は完了です。
余談ですが。。。 最初はここを探していました。

firebaseののりでここを探していたのですが、ここでは一生見つからない。
匿名ログイン便利ですよね。
本格的にログインしなくてもお試しでちょっとアプリを使ってもらいたいとか、ゲームアプリとか色々使えそうです!
今までFirebaseを使っていたのですが、データ保存にやっぱりRDBを使いたいのでそのままユーザーデータかつDBも同じサービス内で使えるので嬉しいです。
久々にゲーム作ったので備忘録
こんにちは。
久しぶりにflameを使ってゲーム作ってみました。
とはいえ、こちらのチュートリアルのほぼコピーなのですが、一部そのままだとバグっているところがあったので、直しています。
久々でComponentの作り方から忘れていたので、色々思い出せました。
作ったものはGithub pagesで公開しています。
何度かリロードしないと動かないかも。
なおしたのは、壁の当たり判定。
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を作ったのでその話をしたいと思います。
OverlayPortalについては上のドキュメントを一度読んでいただけるとどんなものかがわかるかと思います。
今回作ったのはこういうものです。

こういう画面があります。
とある画面の上に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が出現するように書き換えてみましょう。

これは「+」のボタンを押すと左上に四角の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,
),
)
],
),
);
}
}
こう書き換えてみました。
するとここに表示されます。

もうちょっと上に表示したいです。
表示している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,
),
)
],
),
);
}
}
これで、こう!

だんだんいい感じになってきました。
ただ、この場合は縦、横のサイズのわかっている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を書き換えてみました。

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が消えるようにしてみました。



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