From 4d083fd3bc42b220cf4d03ed7907fc50b06ffb5a Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 14 Mar 2023 16:27:34 -0600 Subject: [PATCH] custom dropdown button --- .../custom_buttons/dropdown_button.dart | 231 ++++++++++++++++++ lib/widgets/desktop/secondary_button.dart | 7 + 2 files changed, 238 insertions(+) create mode 100644 lib/widgets/custom_buttons/dropdown_button.dart diff --git a/lib/widgets/custom_buttons/dropdown_button.dart b/lib/widgets/custom_buttons/dropdown_button.dart new file mode 100644 index 000000000..ba8894a1d --- /dev/null +++ b/lib/widgets/custom_buttons/dropdown_button.dart @@ -0,0 +1,231 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/animated_widgets/rotate_icon.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class JDropdownButton extends StatefulWidget { + const JDropdownButton({ + Key? key, + this.label, + required this.items, + this.width, + this.onSelectionChanged, + this.groupValue, + this.redrawOnScreenSizeChanged = false, + this.showIcon = false, + }) : super(key: key); + + final String? label; + final double? width; + final void Function(T?)? onSelectionChanged; + final T? groupValue; + final Set items; + final bool showIcon; + + /// setting this to true should be done carefully + final bool redrawOnScreenSizeChanged; + + @override + State> createState() => _JDropdownButtonState(); +} + +class _JDropdownButtonState extends State> { + final _key = GlobalKey(); + final _rotateIconController = RotateIconController(); + + bool _isOpen = false; + + OverlayEntry? _entry; + + void close() { + if (_isOpen) { + _rotateIconController.reverse?.call(); + _entry?.remove(); + _isOpen = false; + } + } + + void open() { + final size = (_key.currentContext!.findRenderObject() as RenderBox).size; + _entry = OverlayEntry( + builder: (_) { + final position = (_key.currentContext!.findRenderObject() as RenderBox) + .localToGlobal(Offset.zero); + + if (widget.redrawOnScreenSizeChanged) { + // trigger rebuild + MediaQuery.of(context).size; + } + + return GestureDetector( + onTap: close, + child: _JDropdownButtonMenu( + size: size, + position: position, + items: widget.items + .map( + (e) => _JDropdownButtonItem( + value: e, + groupValue: widget.groupValue, + onSelected: (T value) { + widget.onSelectionChanged?.call(value); + close(); + }, + ), + ) + .toList(), + ), + ); + }, + ); + _rotateIconController.forward?.call(); + Overlay.of(context, rootOverlay: true).insert(_entry!); + _isOpen = true; + } + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + if (widget.redrawOnScreenSizeChanged && _isOpen) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _entry?.markNeedsBuild(); + }); + } + return SecondaryButton( + key: _key, + buttonHeight: ButtonHeight.l, + trailingIcon: widget.showIcon + ? RotateIcon( + icon: SvgPicture.asset( + Assets.svg.chevronDown, + width: 10, + color: Theme.of(context) + .extension()! + .buttonTextSecondary, + ), + curve: Curves.easeInOutCubic, + controller: _rotateIconController, + animationDurationMultiplier: 0.1, + ) + : null, + width: widget.width, + label: widget.label ?? widget.groupValue.toString(), + onPressed: _isOpen ? close : open, + ); + } +} + +// ============================================================================= + +class _JDropdownButtonMenu extends StatefulWidget { + const _JDropdownButtonMenu( + {Key? key, + required this.items, + required this.size, + required this.position}) + : super(key: key); + + final List<_JDropdownButtonItem> items; + final Size size; + final Offset position; + + @override + State<_JDropdownButtonMenu> createState() => _JDropdownButtonMenuState(); +} + +class _JDropdownButtonMenuState extends State<_JDropdownButtonMenu> { + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: Stack( + children: [ + Container( + color: Colors.black.withOpacity(0.2), + // child: widget.content, + ), + Positioned( + top: widget.size.height + widget.position.dy + 10, + left: widget.position.dx, + width: widget.size.width, + child: RoundedWhiteContainer( + padding: EdgeInsets.zero, + radiusMultiplier: 2.5, + boxShadow: [ + Theme.of(context).extension()!.standardBoxShadow, + ], + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox( + height: 20, + ), + ...widget.items, + const SizedBox( + height: 20, + ), + ], + ), + ), + ), + ], + ), + ); + } +} + +// ============================================================================= + +class _JDropdownButtonItem extends StatelessWidget { + const _JDropdownButtonItem({ + Key? key, + required this.value, + required this.groupValue, + required this.onSelected, + this.height = 53, + }) : super(key: key); + + final T value; + final T? groupValue; + final double height; + final void Function(T) onSelected; + + @override + Widget build(BuildContext context) { + return RawMaterialButton( + fillColor: groupValue == value + ? Theme.of(context).extension()!.textFieldDefaultBG + : Colors.transparent, + elevation: 0, + focusElevation: 0, + hoverElevation: 0, + highlightElevation: 0, + disabledElevation: 0, + padding: EdgeInsets.zero, + onPressed: () => onSelected(value), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + value.toString(), + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context).extension()!.textDark, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/desktop/secondary_button.dart b/lib/widgets/desktop/secondary_button.dart index ef2b564ac..5d1ba1970 100644 --- a/lib/widgets/desktop/secondary_button.dart +++ b/lib/widgets/desktop/secondary_button.dart @@ -13,6 +13,7 @@ class SecondaryButton extends StatelessWidget { this.height, this.label, this.icon, + this.trailingIcon, this.onPressed, this.enabled = true, this.buttonHeight, @@ -25,6 +26,7 @@ class SecondaryButton extends StatelessWidget { final VoidCallback? onPressed; final bool enabled; final Widget? icon; + final Widget? trailingIcon; final ButtonHeight? buttonHeight; final double iconSpacing; @@ -178,6 +180,11 @@ class SecondaryButton extends StatelessWidget { ), ], ), + if (trailingIcon != null) + SizedBox( + width: iconSpacing, + ), + if (trailingIcon != null) trailingIcon!, ], ), ),