1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
|
import 'package:flutter/material.dart';
void main() {
runApp(const MainApp());
}
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
final _infoPopupController = OverlayPortalController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: false,
title: const Text('Upgrade Plan'),
actions: [_HelpButton(_infoPopupController)],
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Hello World'),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _infoPopupController.toggle,
child: const Text('Toggle Popup'),
),
],
),
),
);
}
}
class _HelpButton extends StatelessWidget {
const _HelpButton(this.controller);
final OverlayPortalController controller;
@override
Widget build(BuildContext context) {
return Popup(
follower: _HelpOverlay(controller.hide),
followerAnchor: Alignment.topRight,
targetAnchor: Alignment.topRight,
controller: controller,
child: OutlinedButton(
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 8),
),
onPressed: controller.show,
child: const Text('Help'),
),
);
}
}
class _HelpOverlay extends StatelessWidget {
const _HelpOverlay(this.hide);
final VoidCallback hide;
@override
Widget build(BuildContext context) {
return SizedBox(
width: 200,
child: Card(
margin: EdgeInsets.zero,
surfaceTintColor: Colors.white,
elevation: 4,
shape: const ContinuousRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(28)),
),
child: OutlinedButton(
onPressed: hide,
child: const Text('Close'),
),
),
);
}
}
/// A widget that shows a popup relative to a target widget.
///
/// The popup is declaratively shown/hidden using an [OverlayPortalController].
///
/// It is positioned relative to the target widget using the [followerAnchor] and [targetAnchor] properties.
class Popup extends StatefulWidget {
const Popup({
required this.child,
required this.follower,
required this.controller,
this.offset = Offset.zero,
this.followerAnchor = Alignment.topCenter,
this.targetAnchor = Alignment.bottomCenter,
super.key,
});
/// The target widget that will be used to position the follower widget.
final Widget child;
/// The widget that will be positioned relative to the target widget.
final Widget follower;
/// The controller that will be used to show/hide the overlay.
final OverlayPortalController controller;
/// The alignment of the follower widget relative to the target widget.
///
/// Defaults to [Alignment.topCenter].
final Alignment followerAnchor;
/// The alignment of the target widget relative to the follower widget.
///
/// Defaults to [Alignment.bottomCenter].
final Alignment targetAnchor;
/// The offset of the follower widget relative to the target widget.
/// This is useful for fine-tuning the position of the follower widget.
///
/// Defaults to [Offset.zero].
final Offset offset;
@override
State<Popup> createState() => _PopupState();
}
class _PopupState extends State<Popup> {
/// The link between the target widget and the follower widget.
final _layerLink = LayerLink();
@override
Widget build(BuildContext context) {
return CompositedTransformTarget(
// Link the target widget to the follower widget.
link: _layerLink,
child: OverlayPortal(
controller: widget.controller,
child: widget.child,
overlayChildBuilder: (BuildContext context) {
// It is needed to wrap the follower widget in a widget that fills the space of the overlay.
// This is needed to make sure that the follower widget is positioned relative to the target widget.
// If not wrapped, the follower widget will fill the screen and be positioned incorrectly.
return Align(
child: CompositedTransformFollower(
// Link the follower widget to the target widget.
link: _layerLink,
// The follower widget should not be shown when the link is broken.
showWhenUnlinked: false,
followerAnchor: widget.followerAnchor,
targetAnchor: widget.targetAnchor,
offset: widget.offset,
child: widget.follower,
),
);
},
),
);
}
}
|