Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save creativepsyco/0421e8df26b607731f205730951bfe23 to your computer and use it in GitHub Desktop.

Select an option

Save creativepsyco/0421e8df26b607731f205730951bfe23 to your computer and use it in GitHub Desktop.
Swift UI compatible popup
import 'dart:ui';
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: IosMenuDemo(), debugShowCheckedModeBanner: false));
class IosMenuDemo extends StatelessWidget {
const IosMenuDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
// Background images/colors help see the blur effect
body: Stack(
children: [
Container(color: Colors.blueGrey[900]),
const Center(
child: Text("Content Behind Menu", style: TextStyle(color: Colors.white24, fontSize: 30)),
),
const Positioned(
top: 100,
right: 50,
child: CustomIosMenu(),
),
],
),
);
}
}
class CustomIosMenu extends StatefulWidget {
const CustomIosMenu({super.key});
@override
State<CustomIosMenu> createState() => _CustomIosMenuState();
}
class _CustomIosMenuState extends State<CustomIosMenu> with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scaleAnimation;
OverlayEntry? _overlayEntry;
final LayerLink _layerLink = LayerLink();
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 250), // Matches the snap of the GIF
);
// This curved animation provides the "pop" feel
// 'easeOutBack' provides that signature iOS "springy" pop-in effect
_scaleAnimation = CurvedAnimation(
parent: _controller,
curve: Curves.easeOutBack,
);
}
void _toggleMenu() {
if (_overlayEntry == null) {
_overlayEntry = _createOverlayEntry();
Overlay.of(context).insert(_overlayEntry!);
_controller.forward();
} else {
_controller.reverse().then((_) {
_overlayEntry?.remove();
_overlayEntry = null;
});
}
}
OverlayEntry _createOverlayEntry() {
return OverlayEntry(
builder: (context) => GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: _toggleMenu, // Close menu when tapping outside
child: Stack(
children: [
CompositedTransformFollower(
link: _layerLink,
showWhenUnlinked: false,
offset: const Offset(-120, 45), // Position relative to button
child: ScaleTransition(
scale: _scaleAnimation,
alignment: Alignment.topRight, // Menu grows from the button
child: ClipRRect(
borderRadius: BorderRadius.circular(14),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: Container(
width: 180,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.7),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: Colors.white.withOpacity(0.2)),
),
child: Material(
color: Colors.transparent,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildItem(Icons.ios_share, "Share"),
const Divider(height: 1, color: Colors.black12),
_buildItem(Icons.edit, "Edit"),
const Divider(height: 1, color: Colors.black12),
_buildItem(Icons.delete, "Delete", isDestructive: true),
],
),
),
),
),
),
),
),
],
),
),
);
}
Widget _buildItem(IconData icon, String label, {bool isDestructive = false}) {
return InkWell(
onTap: () => _toggleMenu(),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label, style: TextStyle(color: isDestructive ? Colors.red : Colors.black87, fontSize: 16)),
Icon(icon, color: isDestructive ? Colors.red : Colors.black87, size: 20),
],
),
),
);
}
@override
Widget build(BuildContext context) {
return CompositedTransformTarget(
link: _layerLink,
child: IconButton(
icon: const Icon(Icons.more_horiz, color: Colors.white, size: 30),
onPressed: _toggleMenu,
),
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment