Skip to content

Instantly share code, notes, and snippets.

@Luckey-Elijah
Last active July 12, 2024 19:38
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/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/material.dart' show Card;
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 = Card(
clipBehavior: Clip.hardEdge,
elevation: 20,
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