Skip to content

Instantly share code, notes, and snippets.

@kspo
Created October 25, 2023 20:49
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kspo/0fd8f36449f0931750b61ff5f86dcc23 to your computer and use it in GitHub Desktop.
Save kspo/0fd8f36449f0931750b61ff5f86dcc23 to your computer and use it in GitHub Desktop.
Custom Dropdown Button
import 'dart:ui';
import 'package:flutter/material.dart';
class CustomDropdown<T> extends StatefulWidget {
/// the child widget for the button, this will be ignored if text is supplied
final Widget child;
/// onChange is called when the selected option is changed.;
/// It will pass back the value and the index of the option.
final void Function(T, int)? onChange;
/// list of DropdownItems
final List<DropdownItem<T>> items;
final DropdownStyle dropdownStyle;
/// dropdownButtonStyles passes styles to OutlineButton.styleFrom()
final DropdownButtonStyle dropdownButtonStyle;
/// dropdown button icon defaults to caret
final Icon? icon;
final bool hideIcon;
/// if true the dropdown icon will as a leading icon, default to false
final bool leadingIcon;
const CustomDropdown({
Key? key,
this.hideIcon = false,
required this.child,
required this.items,
this.dropdownStyle = const DropdownStyle(),
this.dropdownButtonStyle = const DropdownButtonStyle(),
this.icon,
this.leadingIcon = false,
this.onChange,
}) : super(key: key);
@override
_CustomDropdownState<T> createState() => _CustomDropdownState<T>();
}
class _CustomDropdownState<T> extends State<CustomDropdown<T>>
with TickerProviderStateMixin {
final LayerLink _layerLink = LayerLink();
late OverlayEntry _overlayEntry;
bool _isOpen = false;
int _currentIndex = -1;
late AnimationController _animationController;
Animation<double>? _expandAnimation;
late Animation<double> _rotateAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this, duration: const Duration(milliseconds: 200));
_expandAnimation = CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
);
_rotateAnimation = Tween(begin: 0.0, end: 0.5).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
));
}
@override
Widget build(BuildContext context) {
var style = widget.dropdownButtonStyle;
// link the overlay to the button
return CompositedTransformTarget(
link: this._layerLink,
child: SizedBox(
width: style.width,
height: style.height,
child: OutlinedButton(
style: OutlinedButton.styleFrom(
foregroundColor: style.primaryColor,
padding: style.padding,
backgroundColor: style.backgroundColor,
elevation: style.elevation,
),
onPressed: _toggleDropdown,
child: Row(
mainAxisAlignment:
style.mainAxisAlignment ?? MainAxisAlignment.center,
textDirection:
widget.leadingIcon ? TextDirection.rtl : TextDirection.ltr,
mainAxisSize: MainAxisSize.min,
children: [
if (_currentIndex == -1) ...[
widget.child,
] else ...[
widget.items[_currentIndex],
],
if (!widget.hideIcon)
RotationTransition(
turns: _rotateAnimation,
child:
widget.icon ?? const Icon(Icons.arrow_drop_down_rounded),
),
],
),
),
),
);
}
OverlayEntry _createOverlayEntry() {
// find the size and position of the current widget
RenderBox? renderBox = context.findRenderObject() as RenderBox;
var size = renderBox.size;
var offset = renderBox.localToGlobal(Offset.zero);
var topOffset = offset.dy + size.height + 5;
return OverlayEntry(
// full screen GestureDetector to register when a
// user has clicked away from the dropdown
builder: (context) => GestureDetector(
onTap: () => _toggleDropdown(close: true),
behavior: HitTestBehavior.translucent,
// full screen container to register taps anywhere and close drop down
child: SizedBox(
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
child: Stack(
children: [
Positioned(
left: offset.dx,
top: topOffset,
width: widget.dropdownStyle.width ?? size.width,
child: CompositedTransformFollower(
offset:
widget.dropdownStyle.offset ?? Offset(0, size.height + 5),
link: this._layerLink,
showWhenUnlinked: false,
child: ClipRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 30.0, sigmaY: 30.0),
child: Material(
elevation: 0,
color: Colors.transparent,
child: Container(
decoration: BoxDecoration(
borderRadius: widget.dropdownStyle.borderRadius ??
BorderRadius.zero,
color: widget.dropdownStyle.color,
),
child: SizeTransition(
axisAlignment: 1,
sizeFactor: _expandAnimation!,
child: ConstrainedBox(
constraints: widget.dropdownStyle.constraints ??
BoxConstraints(
maxHeight:
MediaQuery.of(context).size.height -
topOffset -
15,
),
child: ListView(
padding: widget.dropdownStyle.padding ??
EdgeInsets.zero,
shrinkWrap: true,
children:
widget.items.asMap().entries.map((item) {
return InkWell(
onTap: () {
setState(() => _currentIndex = item.key);
widget.onChange!(
item.value.value as T, item.key);
_toggleDropdown();
},
child: item.value,
);
}).toList(),
),
),
),
),
),
),
),
),
),
],
),
),
),
);
}
void _toggleDropdown({bool close = false}) async {
if (_isOpen || close) {
await _animationController.reverse();
this._overlayEntry.remove();
setState(() {
_isOpen = false;
});
} else {
this._overlayEntry = this._createOverlayEntry();
Overlay.of(context).insert(this._overlayEntry);
setState(() => _isOpen = true);
_animationController.forward();
}
}
}
/// DropdownItem is just a wrapper for each child in the dropdown list.\n
/// It holds the value of the item.
class DropdownItem<T> extends StatelessWidget {
final T? value;
final Widget child;
const DropdownItem({Key? key, this.value, required this.child})
: super(key: key);
@override
Widget build(BuildContext context) {
return child;
}
}
class DropdownButtonStyle {
final MainAxisAlignment? mainAxisAlignment;
final ShapeBorder? shape;
final double? elevation;
final Color? backgroundColor;
final EdgeInsets? padding;
final BoxConstraints? constraints;
final double? width;
final double? height;
final Color? primaryColor;
const DropdownButtonStyle({
this.mainAxisAlignment,
this.backgroundColor,
this.primaryColor,
this.constraints,
this.height,
this.width,
this.elevation,
this.padding,
this.shape,
});
}
class DropdownStyle {
final BorderRadius? borderRadius;
final double? elevation;
final Color? color;
final EdgeInsets? padding;
final BoxConstraints? constraints;
/// position of the top left of the dropdown relative to the top left of the button
final Offset? offset;
///button width must be set for this to take effect
final double? width;
const DropdownStyle({
this.constraints,
this.offset,
this.width,
this.elevation,
this.color,
this.padding,
this.borderRadius,
});
}
@kspo
Copy link
Author

kspo commented Oct 25, 2023

Usage Example

import 'package:flutter/material.dart';

import 'custom_dropdown.dart';

/// Flutter code sample for [DropdownMenu]s. The first dropdown menu has an outlined border.

void main() => runApp(const DropdownMenuExample());

class DropdownMenuExample extends StatefulWidget {
  const DropdownMenuExample({super.key});

  @override
  State createState() => _DropdownMenuExampleState();
}

class _DropdownMenuExampleState extends State {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        useMaterial3: true,
        colorSchemeSeed: Colors.green,
      ),
      home: Scaffold(
        body: SafeArea(
          child: Column(
            children: [
              Padding(
                padding: const EdgeInsets.symmetric(vertical: 20),
                child: Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    CustomDropdown(
                      hideIcon: true,
                      onChange: (int value, int index) => print(value),
                      dropdownButtonStyle: const DropdownButtonStyle(
                        width: 170,
                        height: 40,
                        elevation: 1,
                        backgroundColor: Colors.white,
                        primaryColor: Colors.black87,
                      ),
                      dropdownStyle: DropdownStyle(
                        borderRadius: BorderRadius.circular(20),
                        elevation: 6,
                        color: Colors.grey.withOpacity(0.1),
                        padding: const EdgeInsets.all(5),
                      ),
                      items: [
                        'item 1',
                        'item 2',
                        'item 3',
                        'item 4',
                        'item 1',
                        'item 2',
                        'item 3',
                        'item 4',
                        'item 1',
                        'item 2',
                        'item 3',
                        'item 4',
                      ]
                          .asMap()
                          .entries
                          .map(
                            (item) => DropdownItem(
                              value: item.key + 1,
                              child: Padding(
                                padding: const EdgeInsets.all(8.0),
                                child: Text(
                                  item.value,
                                  textAlign: TextAlign.center,
                                  style: TextStyle(fontWeight: FontWeight.w700),
                                ),
                              ),
                            ),
                          )
                          .toList(),
                      child: const Row(
                        children: [
                          Icon(Icons.filter_list),
                          Text(
                            'dropdown',
                          ),
                        ],
                      ),
                    ),
                  ],
                ),
              ),
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Container(
                    width: 50,
                    height: 50,
                    color: Colors.amber,
                  ),
                ],
              )
            ],
          ),
        ),
      ),
    );
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment