Skip to content

Instantly share code, notes, and snippets.

@tudor07
Created September 2, 2019 10:15
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tudor07/9f886102f3cb2f69314e159ea10572e1 to your computer and use it in GitHub Desktop.
Save tudor07/9f886102f3cb2f69314e159ea10572e1 to your computer and use it in GitHub Desktop.
DropdownButton with custom height for list of options
/// DropdownButton from material.dart does not allow to set a height
/// for the list of options inside it.
/// This is a copy of the Flutter's code which allows setting height also.
/// Once Flutter adds this in the framework this should be removed.
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math' as math;
import 'package:flutter/material.dart';
const Duration _kDropdownMenuDuration = Duration(milliseconds: 300);
const double _kMenuItemHeight = 48.0;
const double _kDenseButtonHeight = 24.0;
const EdgeInsets _kMenuItemPadding = EdgeInsets.symmetric(horizontal: 16.0);
const EdgeInsetsGeometry _kAlignedButtonPadding =
EdgeInsetsDirectional.only(start: 16.0, end: 4.0);
const EdgeInsets _kUnalignedButtonPadding = EdgeInsets.zero;
const EdgeInsets _kAlignedMenuMargin = EdgeInsets.zero;
const EdgeInsetsGeometry _kUnalignedMenuMargin =
EdgeInsetsDirectional.only(start: 16.0, end: 24.0);
class _DropdownMenuPainter extends CustomPainter {
_DropdownMenuPainter({
this.color,
this.elevation,
this.selectedIndex,
this.resize,
}) : _painter = BoxDecoration(
// If you add an image here, you must provide a real
// configuration in the paint() function and you must provide some sort
// of onChanged callback here.
color: color,
borderRadius: BorderRadius.circular(2.0),
boxShadow: kElevationToShadow[elevation],
).createBoxPainter(),
super(repaint: resize);
final Color color;
final int elevation;
final int selectedIndex;
final Animation<double> resize;
final BoxPainter _painter;
@override
void paint(Canvas canvas, Size size) {
final double selectedItemOffset =
selectedIndex * _kMenuItemHeight + kMaterialListPadding.top;
final Tween<double> top = Tween<double>(
begin: selectedItemOffset.clamp(0.0, size.height - _kMenuItemHeight),
end: 0.0,
);
final Tween<double> bottom = Tween<double>(
begin:
(top.begin + _kMenuItemHeight).clamp(_kMenuItemHeight, size.height),
end: size.height,
);
final Rect rect = Rect.fromLTRB(
0.0, top.evaluate(resize), size.width, bottom.evaluate(resize));
_painter.paint(canvas, rect.topLeft, ImageConfiguration(size: rect.size));
}
@override
bool shouldRepaint(_DropdownMenuPainter oldPainter) {
return oldPainter.color != color ||
oldPainter.elevation != elevation ||
oldPainter.selectedIndex != selectedIndex ||
oldPainter.resize != resize;
}
}
// Do not use the platform-specific default scroll configuration.
// Dropdown menus should never overscroll or display an overscroll indicator.
class _DropdownScrollBehavior extends ScrollBehavior {
const _DropdownScrollBehavior();
@override
TargetPlatform getPlatform(BuildContext context) =>
Theme.of(context).platform;
@override
Widget buildViewportChrome(
BuildContext context, Widget child, AxisDirection axisDirection) =>
child;
@override
ScrollPhysics getScrollPhysics(BuildContext context) =>
const ClampingScrollPhysics();
}
class _DropdownMenu<T> extends StatefulWidget {
const _DropdownMenu({
Key key,
this.padding,
this.route,
this.height,
}) : super(key: key);
final _DropdownRoute<T> route;
final EdgeInsets padding;
final double height;
@override
_DropdownMenuState<T> createState() => _DropdownMenuState<T>();
}
class _DropdownMenuState<T> extends State<_DropdownMenu<T>> {
CurvedAnimation _fadeOpacity;
CurvedAnimation _resize;
@override
void initState() {
super.initState();
// We need to hold these animations as state because of their curve
// direction. When the route's animation reverses, if we were to recreate
// the CurvedAnimation objects in build, we'd lose
// CurvedAnimation._curveDirection.
_fadeOpacity = CurvedAnimation(
parent: widget.route.animation,
curve: const Interval(0.0, 0.25),
reverseCurve: const Interval(0.75, 1.0),
);
_resize = CurvedAnimation(
parent: widget.route.animation,
curve: const Interval(0.25, 0.5),
reverseCurve: const Threshold(0.0),
);
}
@override
Widget build(BuildContext context) {
// The menu is shown in three stages (unit timing in brackets):
// [0s - 0.25s] - Fade in a rect-sized menu container with the selected item.
// [0.25s - 0.5s] - Grow the otherwise empty menu container from the center
// until it's big enough for as many items as we're going to show.
// [0.5s - 1.0s] Fade in the remaining visible items from top to bottom.
//
// When the menu is dismissed we just fade the entire thing out
// in the first 0.25s.
assert(debugCheckHasMaterialLocalizations(context));
final MaterialLocalizations localizations =
MaterialLocalizations.of(context);
final _DropdownRoute<T> route = widget.route;
final double unit = 0.5 / (route.items.length + 1.5);
final List<Widget> children = <Widget>[];
for (int itemIndex = 0; itemIndex < route.items.length; ++itemIndex) {
CurvedAnimation opacity;
if (itemIndex == route.selectedIndex) {
opacity = CurvedAnimation(
parent: route.animation, curve: const Threshold(0.0));
} else {
final double start = (0.5 + (itemIndex + 1) * unit).clamp(0.0, 1.0);
final double end = (start + 1.5 * unit).clamp(0.0, 1.0);
opacity = CurvedAnimation(
parent: route.animation, curve: Interval(start, end));
}
children.add(FadeTransition(
opacity: opacity,
child: InkWell(
child: Container(
padding: widget.padding,
child: route.items[itemIndex],
),
onTap: () => Navigator.pop(
context,
_DropdownRouteResult<T>(route.items[itemIndex].value),
),
),
));
}
return FadeTransition(
opacity: _fadeOpacity,
child: CustomPaint(
painter: _DropdownMenuPainter(
color: Theme.of(context).canvasColor,
elevation: route.elevation,
selectedIndex: route.selectedIndex,
resize: _resize,
),
child: Semantics(
scopesRoute: true,
namesRoute: true,
explicitChildNodes: true,
label: localizations.popupMenuLabel,
child: Material(
type: MaterialType.transparency,
textStyle: route.style,
child: ScrollConfiguration(
behavior: const _DropdownScrollBehavior(),
child: Scrollbar(
child: widget.height != null
? SizedBox(
height: widget.height,
child: ListView(
controller: widget.route.scrollController,
padding: kMaterialListPadding,
itemExtent: _kMenuItemHeight,
shrinkWrap: true,
children: children,
),
)
: ListView(
controller: widget.route.scrollController,
padding: kMaterialListPadding,
itemExtent: _kMenuItemHeight,
shrinkWrap: true,
children: children,
),
),
),
),
),
),
);
}
}
class _DropdownMenuRouteLayout<T> extends SingleChildLayoutDelegate {
_DropdownMenuRouteLayout({
@required this.buttonRect,
@required this.menuTop,
@required this.menuHeight,
@required this.textDirection,
});
final Rect buttonRect;
final double menuTop;
final double menuHeight;
final TextDirection textDirection;
@override
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
// The maximum height of a simple menu should be one or more rows less than
// the view height. This ensures a tappable area outside of the simple menu
// with which to dismiss the menu.
// -- https://material.io/design/components/menus.html#usage
final double maxHeight =
math.max(0.0, constraints.maxHeight - 2 * _kMenuItemHeight);
// The width of a menu should be at most the view width. This ensures that
// the menu does not extend past the left and right edges of the screen.
final double width = math.min(constraints.maxWidth, buttonRect.width);
return BoxConstraints(
minWidth: width,
maxWidth: width,
minHeight: 0.0,
maxHeight: maxHeight,
);
}
@override
Offset getPositionForChild(Size size, Size childSize) {
assert(() {
final Rect container = Offset.zero & size;
if (container.intersect(buttonRect) == buttonRect) {
// If the button was entirely on-screen, then verify
// that the menu is also on-screen.
// If the button was a bit off-screen, then, oh well.
assert(menuTop >= 0.0);
assert(menuTop + menuHeight <= size.height);
}
return true;
}());
assert(textDirection != null);
double left;
switch (textDirection) {
case TextDirection.rtl:
left = buttonRect.right.clamp(0.0, size.width) - childSize.width;
break;
case TextDirection.ltr:
left = buttonRect.left.clamp(0.0, size.width - childSize.width);
break;
}
return Offset(left, menuTop);
}
@override
bool shouldRelayout(_DropdownMenuRouteLayout<T> oldDelegate) {
return buttonRect != oldDelegate.buttonRect ||
menuTop != oldDelegate.menuTop ||
menuHeight != oldDelegate.menuHeight ||
textDirection != oldDelegate.textDirection;
}
}
// We box the return value so that the return value can be null. Otherwise,
// canceling the route (which returns null) would get confused with actually
// returning a real null value.
class _DropdownRouteResult<T> {
const _DropdownRouteResult(this.result);
final T result;
@override
bool operator ==(dynamic other) {
if (other is! _DropdownRouteResult<T>) return false;
final _DropdownRouteResult<T> typedOther = other;
return result == typedOther.result;
}
@override
int get hashCode => result.hashCode;
}
class _DropdownRoute<T> extends PopupRoute<_DropdownRouteResult<T>> {
_DropdownRoute({
this.items,
this.padding,
this.buttonRect,
this.selectedIndex,
this.elevation = 8,
this.theme,
this.height,
@required this.style,
this.barrierLabel,
}) : assert(style != null);
final double height;
final List<DropdownMenuItem<T>> items;
final EdgeInsetsGeometry padding;
final Rect buttonRect;
final int selectedIndex;
final int elevation;
final ThemeData theme;
final TextStyle style;
ScrollController scrollController;
@override
Duration get transitionDuration => _kDropdownMenuDuration;
@override
bool get barrierDismissible => true;
@override
Color get barrierColor => null;
@override
final String barrierLabel;
@override
Widget buildPage(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) {
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return _DropdownRoutePage<T>(
route: this,
constraints: constraints,
items: items,
padding: padding,
buttonRect: buttonRect,
selectedIndex: selectedIndex,
elevation: elevation,
theme: theme,
style: style,
height: height,
);
});
}
void _dismiss() {
navigator?.removeRoute(this);
}
}
class _DropdownRoutePage<T> extends StatelessWidget {
const _DropdownRoutePage({
Key key,
this.route,
this.constraints,
this.items,
this.padding,
this.buttonRect,
this.selectedIndex,
this.elevation = 8,
this.theme,
this.style,
this.height,
}) : super(key: key);
final _DropdownRoute<T> route;
final BoxConstraints constraints;
final List<DropdownMenuItem<T>> items;
final EdgeInsetsGeometry padding;
final Rect buttonRect;
final int selectedIndex;
final int elevation;
final ThemeData theme;
final TextStyle style;
final double height;
@override
Widget build(BuildContext context) {
assert(debugCheckHasDirectionality(context));
final double availableHeight = constraints.maxHeight;
final double maxMenuHeight = availableHeight - 2.0 * _kMenuItemHeight;
final double buttonTop = buttonRect.top;
final double buttonBottom = math.min(buttonRect.bottom, availableHeight);
// If the button is placed on the bottom or top of the screen, its top or
// bottom may be less than [_kMenuItemHeight] from the edge of the screen.
// In this case, we want to change the menu limits to align with the top
// or bottom edge of the button.
final double topLimit = math.min(_kMenuItemHeight, buttonTop);
final double bottomLimit =
math.max(availableHeight - _kMenuItemHeight, buttonBottom);
final double selectedItemOffset =
selectedIndex * _kMenuItemHeight + kMaterialListPadding.top;
double menuTop = (buttonTop - selectedItemOffset) -
(_kMenuItemHeight - buttonRect.height) / 2.0;
final double preferredMenuHeight =
(items.length * _kMenuItemHeight) + kMaterialListPadding.vertical;
// If there are too many elements in the menu, we need to shrink it down
// so it is at most the maxMenuHeight.
final double menuHeight = math.min(maxMenuHeight, preferredMenuHeight);
double menuBottom = menuTop + menuHeight;
// If the computed top or bottom of the menu are outside of the range
// specified, we need to bring them into range. If the item height is larger
// than the button height and the button is at the very bottom or top of the
// screen, the menu will be aligned with the bottom or top of the button
// respectively.
if (menuTop < topLimit) menuTop = math.min(buttonTop, topLimit);
if (menuBottom > bottomLimit) {
menuBottom = math.max(buttonBottom, bottomLimit);
menuTop = menuBottom - menuHeight;
}
if (route.scrollController == null) {
// The limit is asymmetrical because we do not care how far positive the
// limit goes. We are only concerned about the case where the value of
// [buttonTop - menuTop] is larger than selectedItemOffset, ie. when
// the button is close to the bottom of the screen and the selected item
// is close to 0.
final double scrollOffset = preferredMenuHeight > maxMenuHeight
? math.max(0.0, selectedItemOffset - (buttonTop - menuTop))
: 0.0;
route.scrollController =
ScrollController(initialScrollOffset: scrollOffset);
}
final TextDirection textDirection = Directionality.of(context);
Widget menu = _DropdownMenu<T>(
route: route,
height: height,
padding: padding.resolve(textDirection),
);
if (theme != null) menu = Theme(data: theme, child: menu);
return MediaQuery.removePadding(
context: context,
removeTop: true,
removeBottom: true,
removeLeft: true,
removeRight: true,
child: Builder(
builder: (BuildContext context) {
return CustomSingleChildLayout(
delegate: _DropdownMenuRouteLayout<T>(
buttonRect: buttonRect,
menuTop: menuTop,
menuHeight: menuHeight,
textDirection: textDirection,
),
child: menu,
);
},
),
);
}
}
/// An item in a menu created by a [DropdownButton].
///
/// The type `T` is the type of the value the entry represents. All the entries
/// in a given menu must represent values with consistent types.
class DropdownMenuItem<T> extends StatelessWidget {
/// Creates an item for a dropdown menu.
///
/// The [child] argument is required.
const DropdownMenuItem({
Key key,
this.value,
@required this.child,
}) : assert(child != null),
super(key: key);
/// The widget below this widget in the tree.
///
/// Typically a [Text] widget.
final Widget child;
/// The value to return if the user selects this menu item.
///
/// Eventually returned in a call to [DropdownButton.onChanged].
final T value;
@override
Widget build(BuildContext context) {
return Container(
height: _kMenuItemHeight,
alignment: AlignmentDirectional.centerStart,
child: child,
);
}
}
/// An inherited widget that causes any descendant [DropdownButton]
/// widgets to not include their regular underline.
///
/// This is used by [DataTable] to remove the underline from any
/// [DropdownButton] widgets placed within material data tables, as
/// required by the material design specification.
class DropdownButtonHideUnderline extends InheritedWidget {
/// Creates a [DropdownButtonHideUnderline]. A non-null [child] must
/// be given.
const DropdownButtonHideUnderline({
Key key,
@required Widget child,
}) : assert(child != null),
super(key: key, child: child);
/// Returns whether the underline of [DropdownButton] widgets should
/// be hidden.
static bool at(BuildContext context) {
return context.inheritFromWidgetOfExactType(DropdownButtonHideUnderline) !=
null;
}
@override
bool updateShouldNotify(DropdownButtonHideUnderline oldWidget) => false;
}
/// A material design button for selecting from a list of items.
///
/// A dropdown button lets the user select from a number of items. The button
/// shows the currently selected item as well as an arrow that opens a menu for
/// selecting another item.
///
/// The type `T` is the type of the [value] that each dropdown item represents.
/// All the entries in a given menu must represent values with consistent types.
/// Typically, an enum is used. Each [DropdownMenuItem] in [items] must be
/// specialized with that same type argument.
///
/// The [onChanged] callback should update a state variable that defines the
/// dropdown's value. It should also call [State.setState] to rebuild the
/// dropdown with the new value.
///
/// {@tool snippet --template=stateful_widget_scaffold}
///
/// This sample shows a `DropdownButton` whose value is one of
/// "One", "Two", "Free", or "Four".
///
/// ```dart
/// String dropdownValue = 'One';
///
/// @override
/// Widget build(BuildContext context) {
/// return Scaffold(
/// body: Center(
/// child: DropdownButton<String>(
/// value: dropdownValue,
/// onChanged: (String newValue) {
/// setState(() {
/// dropdownValue = newValue;
/// });
/// },
/// items: <String>['One', 'Two', 'Free', 'Four']
/// .map<DropdownMenuItem<String>>((String value) {
/// return DropdownMenuItem<String>(
/// value: value,
/// child: Text(value),
/// );
/// })
/// .toList(),
/// ),
/// ),
/// );
/// }
/// ```
/// {@end-tool}
///
/// If the [onChanged] callback is null or the list of [items] is null
/// then the dropdown button will be disabled, i.e. its arrow will be
/// displayed in grey and it will not respond to input. A disabled button
/// will display the [disabledHint] widget if it is non-null.
///
/// Requires one of its ancestors to be a [Material] widget.
///
/// See also:
///
/// * [DropdownMenuItem], the class used to represent the [items].
/// * [DropdownButtonHideUnderline], which prevents its descendant dropdown buttons
/// from displaying their underlines.
/// * [RaisedButton], [FlatButton], ordinary buttons that trigger a single action.
/// * <https://material.io/design/components/menus.html#dropdown-menu>
class DropdownButton<T> extends StatefulWidget {
/// Creates a dropdown button.
///
/// The [items] must have distinct values. If [value] isn't null then it
/// must be equal to one of the [DropDownMenuItem] values. If [items] or
/// [onChanged] is null, the button will be disabled, the down arrow
/// will be greyed out, and the [disabledHint] will be shown (if provided).
///
/// The [elevation] and [iconSize] arguments must not be null (they both have
/// defaults, so do not need to be specified). The boolean [isDense] and
/// [isExpanded] arguments must not be null.
DropdownButton({
Key key,
@required this.items,
this.value,
this.hint,
this.disabledHint,
@required this.onChanged,
this.elevation = 8,
this.style,
this.underline,
this.icon,
this.iconDisabledColor,
this.iconEnabledColor,
this.iconSize = 24.0,
this.isDense = false,
this.isExpanded = false,
this.height,
}) : assert(items == null ||
items.isEmpty ||
value == null ||
items
.where((DropdownMenuItem<T> item) => item.value == value)
.length ==
1),
assert(elevation != null),
assert(iconSize != null),
assert(isDense != null),
assert(isExpanded != null),
super(key: key);
final double height;
/// The list of items the user can select.
///
/// If the [onChanged] callback is null or the list of items is null
/// then the dropdown button will be disabled, i.e. its arrow will be
/// displayed in grey and it will not respond to input. A disabled button
/// will display the [disabledHint] widget if it is non-null.
final List<DropdownMenuItem<T>> items;
/// The value of the currently selected [DropdownMenuItem], or null if no
/// item has been selected. If `value` is null then the menu is popped up as
/// if the first item were selected.
final T value;
/// Displayed if [value] is null.
final Widget hint;
/// A message to show when the dropdown is disabled.
///
/// Displayed if [items] or [onChanged] is null.
final Widget disabledHint;
/// Called when the user selects an item.
///
/// If the [onChanged] callback is null or the list of [items] is null
/// then the dropdown button will be disabled, i.e. its arrow will be
/// displayed in grey and it will not respond to input. A disabled button
/// will display the [disabledHint] widget if it is non-null.
final ValueChanged<T> onChanged;
/// The z-coordinate at which to place the menu when open.
///
/// The following elevations have defined shadows: 1, 2, 3, 4, 6, 8, 9, 12,
/// 16, and 24. See [kElevationToShadow].
///
/// Defaults to 8, the appropriate elevation for dropdown buttons.
final int elevation;
/// The text style to use for text in the dropdown button and the dropdown
/// menu that appears when you tap the button.
///
/// Defaults to the [TextTheme.subhead] value of the current
/// [ThemeData.textTheme] of the current [Theme].
final TextStyle style;
/// The widget to use for drawing the drop-down button's underline.
///
/// Defaults to a 0.0 width bottom border with color 0xFFBDBDBD.
final Widget underline;
/// The widget to use for the drop-down button's icon.
///
/// Defaults to an [Icon] with the [Icons.arrow_drop_down] glyph.
final Widget icon;
/// The color of any [Icon] descendant of [icon] if this button is disabled,
/// i.e. if [onChanged] is null.
///
/// Defaults to [Colors.grey.shade400] when the theme's
/// [ThemeData.brightness] is [Brightness.light] and to
/// [Colors.white10] when it is [Brightness.dark]
final Color iconDisabledColor;
/// The color of any [Icon] descendant of [icon] if this button is enabled,
/// i.e. if [onChanged] is defined.
///
/// Defaults to [Colors.grey.shade700] when the theme's
/// [ThemeData.brightness] is [Brightness.light] and to
/// [Colors.white70] when it is [Brightness.dark]
final Color iconEnabledColor;
/// The size to use for the drop-down button's down arrow icon button.
///
/// Defaults to 24.0.
final double iconSize;
/// Reduce the button's height.
///
/// By default this button's height is the same as its menu items' heights.
/// If isDense is true, the button's height is reduced by about half. This
/// can be useful when the button is embedded in a container that adds
/// its own decorations, like [InputDecorator].
final bool isDense;
/// Set the dropdown's inner contents to horizontally fill its parent.
///
/// By default this button's inner width is the minimum size of its contents.
/// If [isExpanded] is true, the inner width is expanded to fill its
/// surrounding container.
final bool isExpanded;
@override
_DropdownButtonState<T> createState() => _DropdownButtonState<T>();
}
class _DropdownButtonState<T> extends State<DropdownButton<T>>
with WidgetsBindingObserver {
int _selectedIndex;
_DropdownRoute<T> _dropdownRoute;
@override
void initState() {
super.initState();
_updateSelectedIndex();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_removeDropdownRoute();
super.dispose();
}
// Typically called because the device's orientation has changed.
// Defined by WidgetsBindingObserver
@override
void didChangeMetrics() {
_removeDropdownRoute();
}
void _removeDropdownRoute() {
_dropdownRoute?._dismiss();
_dropdownRoute = null;
}
@override
void didUpdateWidget(DropdownButton<T> oldWidget) {
super.didUpdateWidget(oldWidget);
_updateSelectedIndex();
}
void _updateSelectedIndex() {
if (!_enabled) {
return;
}
assert(widget.value == null ||
widget.items
.where((DropdownMenuItem<T> item) => item.value == widget.value)
.length ==
1);
_selectedIndex = null;
for (int itemIndex = 0; itemIndex < widget.items.length; itemIndex++) {
if (widget.items[itemIndex].value == widget.value) {
_selectedIndex = itemIndex;
return;
}
}
}
TextStyle get _textStyle =>
widget.style ?? Theme.of(context).textTheme.subhead;
void _handleTap() {
final RenderBox itemBox = context.findRenderObject();
final Rect itemRect = itemBox.localToGlobal(Offset.zero) & itemBox.size;
final TextDirection textDirection = Directionality.of(context);
final EdgeInsetsGeometry menuMargin =
ButtonTheme.of(context).alignedDropdown
? _kAlignedMenuMargin
: _kUnalignedMenuMargin;
assert(_dropdownRoute == null);
_dropdownRoute = _DropdownRoute<T>(
items: widget.items,
height: widget.height,
buttonRect: menuMargin.resolve(textDirection).inflateRect(itemRect),
padding: _kMenuItemPadding.resolve(textDirection),
selectedIndex: _selectedIndex ?? 0,
elevation: widget.elevation,
theme: Theme.of(context, shadowThemeOnly: true),
style: _textStyle,
barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
);
Navigator.push(context, _dropdownRoute)
.then<void>((_DropdownRouteResult<T> newValue) {
_dropdownRoute = null;
if (!mounted || newValue == null) return;
if (widget.onChanged != null) widget.onChanged(newValue.result);
});
}
// When isDense is true, reduce the height of this button from _kMenuItemHeight to
// _kDenseButtonHeight, but don't make it smaller than the text that it contains.
// Similarly, we don't reduce the height of the button so much that its icon
// would be clipped.
double get _denseButtonHeight {
final double fontSize =
_textStyle.fontSize ?? Theme.of(context).textTheme.subhead.fontSize;
return math.max(fontSize, math.max(widget.iconSize, _kDenseButtonHeight));
}
Color get _iconColor {
// These colors are not defined in the Material Design spec.
if (_enabled) {
if (widget.iconEnabledColor != null) {
return widget.iconEnabledColor;
}
switch (Theme.of(context).brightness) {
case Brightness.light:
return Colors.grey.shade700;
case Brightness.dark:
return Colors.white70;
}
} else {
if (widget.iconDisabledColor != null) {
return widget.iconDisabledColor;
}
switch (Theme.of(context).brightness) {
case Brightness.light:
return Colors.grey.shade400;
case Brightness.dark:
return Colors.white10;
}
}
assert(false);
return null;
}
bool get _enabled =>
widget.items != null &&
widget.items.isNotEmpty &&
widget.onChanged != null;
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
assert(debugCheckHasMaterialLocalizations(context));
// The width of the button and the menu are defined by the widest
// item and the width of the hint.
final List<Widget> items =
_enabled ? List<Widget>.from(widget.items) : <Widget>[];
int hintIndex;
if (widget.hint != null || (!_enabled && widget.disabledHint != null)) {
final Widget emplacedHint = _enabled
? widget.hint
: DropdownMenuItem<Widget>(child: widget.disabledHint ?? widget.hint);
hintIndex = items.length;
items.add(DefaultTextStyle(
style: _textStyle.copyWith(color: Theme.of(context).hintColor),
child: IgnorePointer(
child: emplacedHint,
ignoringSemantics: false,
),
));
}
final EdgeInsetsGeometry padding = ButtonTheme.of(context).alignedDropdown
? _kAlignedButtonPadding
: _kUnalignedButtonPadding;
// If value is null (then _selectedIndex is null) or if disabled then we
// display the hint or nothing at all.
final int index = _enabled ? (_selectedIndex ?? hintIndex) : hintIndex;
Widget innerItemsWidget;
if (items.isEmpty) {
innerItemsWidget = Container();
} else {
innerItemsWidget = IndexedStack(
index: index,
alignment: AlignmentDirectional.centerStart,
children: items,
);
}
const Icon defaultIcon = Icon(Icons.arrow_drop_down);
Widget result = DefaultTextStyle(
style: _textStyle,
child: Container(
padding: padding.resolve(Directionality.of(context)),
height: widget.isDense ? _denseButtonHeight : null,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
widget.isExpanded
? Expanded(child: innerItemsWidget)
: innerItemsWidget,
IconTheme(
data: IconThemeData(
color: _iconColor,
size: widget.iconSize,
),
child: widget.icon ?? defaultIcon,
),
],
),
),
);
if (!DropdownButtonHideUnderline.at(context)) {
final double bottom = widget.isDense ? 0.0 : 8.0;
result = Stack(
children: <Widget>[
result,
Positioned(
left: 0.0,
right: 0.0,
bottom: bottom,
child: widget.underline ??
Container(
height: 1.0,
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(
color: Color(0xFFBDBDBD), width: 0.0))),
),
),
],
);
}
return Semantics(
button: true,
child: GestureDetector(
onTap: _enabled ? _handleTap : null,
behavior: HitTestBehavior.opaque,
child: result,
),
);
}
}
/// A convenience widget that wraps a [DropdownButton] in a [FormField].
class DropdownButtonFormField<T> extends FormField<T> {
/// Creates a [DropdownButton] widget wrapped in an [InputDecorator] and
/// [FormField].
///
/// The [DropdownButton] [items] parameters must not be null.
DropdownButtonFormField({
Key key,
T value,
@required List<DropdownMenuItem<T>> items,
this.onChanged,
InputDecoration decoration = const InputDecoration(),
FormFieldSetter<T> onSaved,
FormFieldValidator<T> validator,
Widget hint,
}) : assert(decoration != null),
super(
key: key,
onSaved: onSaved,
initialValue: value,
validator: validator,
builder: (FormFieldState<T> field) {
final InputDecoration effectiveDecoration = decoration
.applyDefaults(Theme.of(field.context).inputDecorationTheme);
return InputDecorator(
decoration:
effectiveDecoration.copyWith(errorText: field.errorText),
isEmpty: value == null,
child: DropdownButtonHideUnderline(
child: DropdownButton<T>(
isDense: true,
value: value,
items: items,
hint: hint,
onChanged: field.didChange,
),
),
);
});
/// Called when the user selects an item.
final ValueChanged<T> onChanged;
@override
FormFieldState<T> createState() => _DropdownButtonFormFieldState<T>();
}
class _DropdownButtonFormFieldState<T> extends FormFieldState<T> {
@override
DropdownButtonFormField<T> get widget => super.widget;
@override
void didChange(T value) {
super.didChange(value);
if (widget.onChanged != null) widget.onChanged(value);
}
}
@tejashu7
Copy link

The dropdownmenu is displaying on top of my dropdown could you please help me:
dropdown

Code:

import 'package:flutter/material.dart';
import 'package:fujitsuseats/model/team.dart';
import 'package:fujitsuseats/components/custom_dropdown.dart' as custom;

class DateTimeInputScreen extends StatefulWidget {
static const String id = 'dateTimeInput_screen';
@OverRide
_DateTimeInputScreenState createState() => _DateTimeInputScreenState();
}

class _DateTimeInputScreenState extends State {
String dropdownMonthValue = 'Jan';
String dropdownWeekValue = 'One';

@OverRide
Widget build(BuildContext context) {
final Team team = ModalRoute.of(context).settings.arguments;

return Scaffold(
  appBar: AppBar(
    title: Text(team.team),
  ),
  body: Container(
    child: Center(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.center,
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Card(
            child: ListTile(
              title: Text('Date'),
              subtitle: Column(
                  crossAxisAlignment: CrossAxisAlignment.center,
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    custom.DropdownButton<String>(
                      height: 100.0,
                      isExpanded: true,
                      value: dropdownMonthValue,
                      iconSize: 24,
                      elevation: 6,
                      style: TextStyle(color: Colors.deepPurple),
                      underline: Container(
                        height: 2,
                        color: Colors.deepPurpleAccent,
                      ),
                      onChanged: (String newValue) {
                        setState(() {
                          dropdownMonthValue = newValue;
                        });
                      },
                      items: <String>[
                        'Jan',
                        'Feb',
                        'Mar',
                        'Apr',
                        'May',
                        'Jun',
                        'Jul',
                        'Aug',
                        'Sept',
                        'Oct',
                        'Nov',
                        'Dec'
                      ].map<custom.DropdownMenuItem<String>>(
                          (String value) {
                        return custom.DropdownMenuItem<String>(
                          value: value,
                          child: Text(value),
                        );
                      }).toList(),
                    ),
                  ]),
              isThreeLine: true,
            ),
          ),
        ],
      ),
    ),
  ),
);

}
}

Did you find a solution for this?

@tejashu7
Copy link

Hello, how are you? I'm using your code for my project, and when I display the "DropDown" options, the list of options appears above, how should I locate that list? Thank you
2019-09-30_17-11-08
6

did you get a solution for this?

@reymillenium
Copy link

reymillenium commented Feb 20, 2021

It is showing the DropDown options above. This is not working.

Screen Shot 2021-02-19 at 8 09 20 PM

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