Skip to content

Instantly share code, notes, and snippets.

@stevenosse
Last active August 9, 2024 07:57
Show Gist options
  • Save stevenosse/107390424ede8c60f75e4e316ee15ecc to your computer and use it in GitHub Desktop.
Save stevenosse/107390424ede8c60f75e4e316ee15ecc to your computer and use it in GitHub Desktop.
AdaptativeMenuButton (Uses Popup menu button on Android & CupertinoActionSheet on iOS)
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
class AdaptivePopupMenu extends StatelessWidget {
final List<AdaptiveMenuItem> menuItems;
final Widget child;
final String? iosCancelButtonLabel;
final BorderRadius? androidButtonRadius;
final double spacing;
final EdgeInsets padding;
const AdaptivePopupMenu({
super.key,
required this.menuItems,
required this.child,
this.iosCancelButtonLabel,
this.androidButtonRadius,
this.spacing = 10.0,
this.padding = const EdgeInsets.all(16.0),
});
@override
Widget build(BuildContext context) {
return defaultTargetPlatform == TargetPlatform.iOS
? _CupertinoPopupMenu(
menuItems: menuItems,
iosCancelButtonLabel: iosCancelButtonLabel ?? 'Cancel',
padding: padding,
spacing: spacing,
child: child,
)
: _MaterialPopupMenu(
menuItems: menuItems,
padding: padding,
spacing: spacing,
androidButtonRadius: androidButtonRadius,
child: child,
);
}
}
class _MaterialPopupMenu extends StatelessWidget {
final List<AdaptiveMenuItem> menuItems;
final Widget child;
final BorderRadius? androidButtonRadius;
final double? spacing;
final EdgeInsets padding;
const _MaterialPopupMenu({
required this.menuItems,
required this.child,
required this.padding,
this.androidButtonRadius,
this.spacing,
});
@override
Widget build(BuildContext context) {
return PopupMenuButton<String>(
shape: RoundedRectangleBorder(borderRadius: androidButtonRadius ?? BorderRadius.circular(12.0)),
itemBuilder: (BuildContext context) => menuItems
.map((item) => PopupMenuItem<String>(
value: item.value,
child: _MaterialMenuItem(
item: item,
spacing: spacing,
),
))
.toList(),
onSelected: (String value) {
final selectedItem = menuItems.firstWhere((item) => item.value == value);
selectedItem.onTap();
},
child: child,
);
}
}
class _MaterialMenuItem extends StatelessWidget {
final AdaptiveMenuItem item;
final double? spacing;
const _MaterialMenuItem({
required this.item,
required this.spacing,
});
@override
Widget build(BuildContext context) {
return Row(
children: [
if (item.icon != null) ...[
IconTheme(
data: IconThemeData(color: item.color ?? Theme.of(context).colorScheme.onSurface),
child: item.icon!,
),
SizedBox(width: spacing ?? 10.0),
],
Text(item.label, style: TextStyle(color: item.color ?? Theme.of(context).colorScheme.onSurface)),
],
);
}
}
class _CupertinoPopupMenu extends StatelessWidget {
final List<AdaptiveMenuItem> menuItems;
final Widget child;
final String iosCancelButtonLabel;
final EdgeInsets padding;
final double spacing;
const _CupertinoPopupMenu({
required this.menuItems,
required this.child,
required this.iosCancelButtonLabel,
required this.padding,
required this.spacing,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => showCupertinoModalPopup(
context: context,
builder: (BuildContext context) => _CupertinoActionSheet(
menuItems: menuItems,
iosCancelButtonLabel: iosCancelButtonLabel,
padding: padding,
spacing: spacing,
),
),
child: child,
);
}
}
class _CupertinoActionSheet extends StatelessWidget {
final List<AdaptiveMenuItem> menuItems;
final double spacing;
final EdgeInsets? padding;
final String iosCancelButtonLabel;
const _CupertinoActionSheet({
required this.menuItems,
required this.iosCancelButtonLabel,
this.spacing = 10.0,
this.padding,
});
@override
Widget build(BuildContext context) {
return CupertinoActionSheet(
actions: menuItems
.map(
(item) => _CupertinoActionSheetItem(
item: item,
spacing: spacing,
padding: padding,
),
)
.toList(),
cancelButton: _CupertinoCancelButton(iosCancelButtonLabel: iosCancelButtonLabel),
);
}
}
class _CupertinoActionSheetItem extends StatelessWidget {
final AdaptiveMenuItem item;
final double spacing;
final EdgeInsets? padding;
const _CupertinoActionSheetItem({
required this.item,
required this.spacing,
this.padding,
});
@override
Widget build(BuildContext context) {
return CupertinoActionSheetAction(
child: Padding(
padding: padding ?? const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
children: [
Expanded(
child: Text(
item.label,
textAlign: TextAlign.start,
style: TextStyle(color: item.color ?? Theme.of(context).colorScheme.onSurface),
),
),
if (item.icon != null) ...[
SizedBox(width: spacing),
IconTheme(
data: IconThemeData(color: item.color ?? Theme.of(context).colorScheme.onSurface),
child: item.icon!,
),
],
],
),
),
onPressed: () {
Navigator.pop(context);
item.onTap();
},
);
}
}
class _CupertinoCancelButton extends StatelessWidget {
const _CupertinoCancelButton({required this.iosCancelButtonLabel});
final String iosCancelButtonLabel;
@override
Widget build(BuildContext context) {
return CupertinoActionSheetAction(
child: Text(
iosCancelButtonLabel,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: Theme.of(context).colorScheme.onSurface),
),
onPressed: () => Navigator.pop(context),
);
}
}
class AdaptiveMenuItem {
final Widget? icon;
final String label;
final String value;
final Color? color;
final VoidCallback onTap;
AdaptiveMenuItem({
this.icon,
required this.label,
required this.value,
this.color,
required this.onTap,
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment