Skip to content

Instantly share code, notes, and snippets.

@Luckey-Elijah
Last active December 26, 2024 14:32
Show Gist options
  • Save Luckey-Elijah/eddb92a2c5ee76bfb679758e10bcd28f to your computer and use it in GitHub Desktop.
Save Luckey-Elijah/eddb92a2c5ee76bfb679758e10bcd28f to your computer and use it in GitHub Desktop.
A useful OverlayPortal widget wrapper for building based on the anchor/target's position.
import 'positioned_overlay_builder.dart';
import 'package:flutter/cupertino.dart';
class Example extends StatefulWidget {
const Example({super.key});
@override
State<Example> createState() => _ExampleState();
}
class _ExampleState extends State<Example> {
Brightness? value;
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
child: Center(
child: CupertinoPicker<Brightness?>(
value: value,
values: const [null, ...Brightness.values],
onSelect: (value) => setState(() => this.value = value),
buildString: (value) => value.label,
),
),
);
}
}
class CupertinoPicker<T> extends StatelessWidget {
final List<T> values;
final T? value;
final ValueSetter<T> onSelect;
final String Function(T?) buildString;
const CupertinoPicker({
super.key,
required this.value,
required this.values,
required this.onSelect,
required this.buildString,
});
@override
Widget build(BuildContext context) {
final color = CupertinoDynamicColor.resolve(
CupertinoColors.systemGrey,
context,
);
return PositionedOverlayBuilder(
overlayConstraints: const BoxConstraints(maxWidth: 120),
dismissible: true,
overlayChildBuilder: (_, __) => CupertinoPopupSurface(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
for (var i = 0; i < values.length; i++) ...[
if (i > 0)
SizedBox(
height: 0.25,
width: double.infinity,
child: ColoredBox(color: color),
),
CupertinoButton(
onPressed: () => onSelect(values[i]),
padding: EdgeInsets.zero,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Visibility.maintain(
visible: value == values,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Icon(
CupertinoIcons.check_mark,
size: CupertinoTheme.of(context)
.textTheme
.actionTextStyle
.fontSize,
),
),
),
Text(buildString(values[i])),
],
),
),
]
],
),
),
anchorBuilder: (_, controller) => CupertinoButton(
onPressed: () => controller.toggle(),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.only(left: 4.0),
child: Icon(
CupertinoIcons.chevron_up_chevron_down,
size: CupertinoTheme.of(context)
.textTheme
.actionTextStyle
.fontSize! +
0,
),
),
Text(buildString(value)),
],
),
),
);
}
}
import 'positioned_overlay_builder.dart';
import 'package:flutter/material.dart';
class LogoutButton extends StatelessWidget {
const LogoutButton({super.key});
@override
Widget build(BuildContext context) {
return PositionedOverlayBuilder(
anchorBuilder: (context, controller) {
return TextButton.icon(
iconAlignment: IconAlignment.end,
onPressed: controller.toggle,
label: const Text('Logout'),
icon: const Icon(Icons.logout),
);
},
overlayChildBuilder: (context, controller) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const Padding(
padding: EdgeInsets.all(8),
child: Text('Are you sure?'),
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
TextButton(
onPressed: () {},
child: const Text('Logout'),
),
TextButton(
style: TextButton.styleFrom(foregroundColor: Theme.of(context).colorScheme.error),
onPressed: controller.hide,
child: const Text('Cancel'),
),
],
),
],
);
},
);
}
}
import 'package:flutter/widgets.dart';
class PositionedOverlayBuilder extends StatefulWidget {
const PositionedOverlayBuilder({
required this.anchorBuilder,
required this.overlayChildBuilder,
this.onHide,
this.overlayConstraints,
this.dismissible = true,
this.barrierColor,
this.debugLabel,
super.key,
});
final String? debugLabel;
final Color? barrierColor;
final bool dismissible;
final BoxConstraints? overlayConstraints;
final void Function()? onHide;
final Widget Function(
BuildContext context,
OverlayPortalController controller,
) anchorBuilder;
final Widget Function(
BuildContext context,
OverlayPortalController controller,
) overlayChildBuilder;
@override
State<PositionedOverlayBuilder> createState() =>
_PositionedOverlayBuilderState();
}
class _PositionedOverlayBuilderState extends State<PositionedOverlayBuilder>
with WidgetsBindingObserver {
late final controller =
OverlayPortalController(debugLabel: widget.debugLabel);
late final key = GlobalKey(debugLabel: widget.debugLabel);
Offset widgetOrigin(RenderBox renderBox) {
return renderBox.localToGlobal(Offset.zero);
}
RenderBox getRenderBox() =>
key.currentContext!.findRenderObject()! as RenderBox;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
late Size _lastSize =
View.of(context).physicalSize / View.of(context).devicePixelRatio;
@override
void didChangeMetrics() {
if (!controller.isShowing) return;
final view = View.of(context);
final size = view.physicalSize / view.devicePixelRatio;
if (_lastSize == size) return;
setState(() => _lastSize = size);
}
@override
Widget build(BuildContext context) {
return OverlayPortal.targetsRootOverlay(
key: key,
controller: controller,
overlayChildBuilder: (context) {
final renderBox = getRenderBox();
final origin = widgetOrigin(renderBox);
final isTop = _lastSize.height / 2 > origin.dy;
final isLeft = _lastSize.width / 2 > origin.dx;
final position = switch ((isTop, isLeft)) {
(true, true) => renderBox.size.bottomLeft(origin),
(true, false) => renderBox.size.bottomRight(origin),
(false, true) => renderBox.size.topLeft(origin),
(false, false) => renderBox.size.topRight(origin),
};
final left = isLeft ? position.dx : null;
final right = isLeft ? null : _lastSize.width - position.dx;
final top = isTop ? position.dy : null;
final bottom = isTop ? null : _lastSize.height - origin.dy;
final constraints = widget.overlayConstraints;
late final child = widget.overlayChildBuilder(
context,
controller,
);
return Stack(
children: [
if (widget.dismissible)
Positioned.fill(
child: GestureDetector(
// behavior: HitTestBehavior.translucent,
onTap: () {
widget.onHide?.call();
controller.hide();
},
),
),
Positioned(
left: left,
right: right,
top: top,
bottom: bottom,
child: constraints == null
? child
: ConstrainedBox(constraints: constraints, child: child),
),
],
);
},
child: widget.anchorBuilder(
context,
controller,
),
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment