Created
April 21, 2026 17:40
-
-
Save creativepsyco/0421e8df26b607731f205730951bfe23 to your computer and use it in GitHub Desktop.
Swift UI compatible popup
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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