using System; using System.Collections.Generic; using System.Linq; using uiwidgets; using Unity.UIWidgets.animation; using Unity.UIWidgets.async; using Unity.UIWidgets.external; using Unity.UIWidgets.foundation; using Unity.UIWidgets.gestures; using Unity.UIWidgets.material; using Unity.UIWidgets.painting; using Unity.UIWidgets.rendering; using Unity.UIWidgets.service; using Unity.UIWidgets.services; using Unity.UIWidgets.ui; using Unity.UIWidgets.widgets; using UnityEngine; using Canvas = Unity.UIWidgets.ui.Canvas; using Color = Unity.UIWidgets.ui.Color; using Material = UnityEngine.Material; using Rect = Unity.UIWidgets.ui.Rect; using TextStyle = Unity.UIWidgets.painting.TextStyle; namespace Unity.UIWidgets.material { public partial class material_ { public static readonly TimeSpan _kDropdownMenuDuration = new TimeSpan(0, 0, 0, 0, 300); public const float _kMenuItemHeight = kMinInteractiveDimension; public const float _kDenseButtonHeight = 24.0f; public static readonly EdgeInsets _kMenuItemPadding = EdgeInsets.symmetric(horizontal: 16.0f); public static readonly EdgeInsetsGeometry _kAlignedButtonPadding = EdgeInsets.only(left: 16.0f, right: 4.0f); public static readonly EdgeInsets _kUnalignedButtonPadding = EdgeInsets.zero; public static readonly EdgeInsets _kAlignedMenuMargin = EdgeInsets.zero; public static readonly EdgeInsetsGeometry _kUnalignedMenuMargin = EdgeInsets.only(left: 16.0f, right: 24.0f); public delegate List DropdownButtonBuilder(BuildContext context); } class _DropdownMenuPainter : AbstractCustomPainter { public _DropdownMenuPainter( Color color = null, int? elevation = null, int? selectedIndex = null, Animation resize = null, ValueGetter getSelectedItemOffset = null ) : base(repaint: resize) { D.assert(elevation != null); _painter = new BoxDecoration( color: color, borderRadius: BorderRadius.circular(2.0f), boxShadow: material_.kElevationToShadow[elevation ?? 0] ).createBoxPainter(); this.color = color; this.elevation = elevation; this.selectedIndex = selectedIndex; this.resize = resize; this.getSelectedItemOffset = getSelectedItemOffset; } public readonly Color color; public readonly int? elevation; public readonly int? selectedIndex; public readonly Animation resize; public readonly ValueGetter getSelectedItemOffset; public readonly BoxPainter _painter; public override void paint(Canvas canvas, Size size) { float selectedItemOffset = getSelectedItemOffset(); FloatTween top = new FloatTween( begin: selectedItemOffset.clamp(0.0f, size.height - material_._kMenuItemHeight), end: 0.0f ); FloatTween bottom = new FloatTween( begin: (top.begin + material_._kMenuItemHeight).clamp(material_._kMenuItemHeight, size.height), end: size.height ); Rect rect = Rect.fromLTRB(0.0f, top.evaluate(resize), size.width, bottom.evaluate(resize)); _painter.paint(canvas, rect.topLeft, new ImageConfiguration(size: rect.size)); } public override bool shouldRepaint(CustomPainter painter) { _DropdownMenuPainter oldPainter = painter as _DropdownMenuPainter; return oldPainter.color != color || oldPainter.elevation != elevation || oldPainter.selectedIndex != selectedIndex || oldPainter.resize != resize; } } class _DropdownScrollBehavior : ScrollBehavior { public _DropdownScrollBehavior() { } public override Widget buildViewportChrome(BuildContext context, Widget child, AxisDirection axisDirection) { return child; } public override ScrollPhysics getScrollPhysics(BuildContext context) { return new ClampingScrollPhysics(); } } class _DropdownMenuItemButton : StatefulWidget { internal _DropdownMenuItemButton( Key key = null, _DropdownRoute route = null, EdgeInsets padding = null, Rect buttonRect = null, BoxConstraints constraints = null, int? itemIndex = null ) : base(key: key) { this.route = route; this.padding = padding; this.buttonRect = buttonRect; this.constraints = constraints; this.itemIndex = itemIndex; } public _DropdownRoute route; public EdgeInsets padding; public Rect buttonRect; public BoxConstraints constraints; public int? itemIndex; public override State createState() => new _DropdownMenuItemButtonState(); } class _DropdownMenuItemButtonState : State<_DropdownMenuItemButton> { void _handleFocusChange(bool focused) { bool inTraditionalMode = false; switch (FocusManager.instance.highlightMode) { case FocusHighlightMode.touch: inTraditionalMode = false; break; case FocusHighlightMode.traditional: inTraditionalMode = true; break; } if (focused && inTraditionalMode) { _MenuLimits menuLimits = widget.route.getMenuLimits( widget.buttonRect, widget.constraints.maxHeight, widget.itemIndex ?? 0); widget.route.scrollController.animateTo( menuLimits.scrollOffset ?? 0, curve: Curves.easeInOut, duration: new TimeSpan(0, 0, 0, 0, 100) ); } } void _handleOnTap() { DropdownMenuItem dropdownMenuItem = widget.route.items[widget.itemIndex ?? 0].item; if (dropdownMenuItem.onTap != null) { dropdownMenuItem.onTap(); } Navigator.pop( context, new _DropdownRouteResult(dropdownMenuItem.value) ); } static readonly Dictionary _webShortcuts = new Dictionary { {new LogicalKeySet(LogicalKeyboardKey.enter), new Intent(ActivateAction.key)} }; public override Widget build(BuildContext context) { CurvedAnimation opacity; float unit = 0.5f / (widget.route.items.Count + 1.5f); if (widget.itemIndex == widget.route.selectedIndex) { opacity = new CurvedAnimation(parent: widget.route.animation, curve: new Threshold(0.0f)); } else { float start = ((0.5f + (widget.itemIndex + 1) * unit) ?? 0).clamp(0.0f, 1.0f); float end = (start + 1.5f * unit).clamp(0.0f, 1.0f); opacity = new CurvedAnimation(parent: widget.route.animation, curve: new Interval(start, end)); } Widget child = new FadeTransition( opacity: opacity, child: new InkWell( autofocus: widget.itemIndex == widget.route.selectedIndex, child: new Container( padding: widget.padding, child: widget.route.items[widget.itemIndex ?? 0] ), onTap: _handleOnTap, onFocusChange: _handleFocusChange ) ); return child; } } class _DropdownMenu : StatefulWidget { public _DropdownMenu( Key key = null, EdgeInsets padding = null, _DropdownRoute route = null, Rect buttonRect = null, BoxConstraints constraints = null, Color dropdownColor = null ) : base(key: key) { this.route = route; this.padding = padding; this.buttonRect = buttonRect; this.constraints = constraints; this.dropdownColor = dropdownColor; } public readonly _DropdownRoute route; public readonly EdgeInsets padding; public readonly Rect buttonRect; public readonly BoxConstraints constraints; public readonly Color dropdownColor; public override State createState() { return new _DropdownMenuState(); } } class _DropdownMenuState : State<_DropdownMenu> { CurvedAnimation _fadeOpacity; CurvedAnimation _resize; public _DropdownMenuState() { } public override void initState() { base.initState(); _fadeOpacity = new CurvedAnimation( parent: widget.route.animation, curve: new Interval(0.0f, 0.25f), reverseCurve: new Interval(0.75f, 1.0f) ); _resize = new CurvedAnimation( parent: widget.route.animation, curve: new Interval(0.25f, 0.5f), reverseCurve: new Threshold(0.0f) ); } public override Widget build(BuildContext context) { D.assert(material_.debugCheckHasMaterialLocalizations(context)); MaterialLocalizations localizations = MaterialLocalizations.of(context); _DropdownRoute route = widget.route; List children = new List(); for (int itemIndex = 0; itemIndex < route.items.Count; ++itemIndex) { children.Add(new _DropdownMenuItemButton( route: widget.route, padding: widget.padding, buttonRect: widget.buttonRect, constraints: widget.constraints, itemIndex: itemIndex )); } return new FadeTransition( opacity: _fadeOpacity, child: new CustomPaint( painter: new _DropdownMenuPainter( color: widget.dropdownColor ?? Theme.of(context).canvasColor, elevation: route.elevation, selectedIndex: route.selectedIndex, resize: _resize, getSelectedItemOffset: () => route.getItemOffset(route.selectedIndex ?? 0) ), child: new Material( type: MaterialType.transparency, textStyle: route.style, child: new ScrollConfiguration( behavior: new _DropdownScrollBehavior(), child: new Scrollbar( child: new ListView( controller: widget.route.scrollController, padding: material_.kMaterialListPadding, shrinkWrap: true, children: children ) ) ) ) ) ); } } class _DropdownMenuRouteLayout : SingleChildLayoutDelegate { public _DropdownMenuRouteLayout( Rect buttonRect, _DropdownRoute route = null, TextDirection? textDirection = null ) { this.buttonRect = buttonRect; this.route = route; this.textDirection = textDirection; } public readonly Rect buttonRect; public readonly _DropdownRoute route; public readonly TextDirection? textDirection; public override BoxConstraints getConstraintsForChild(BoxConstraints constraints) { float maxHeight = Mathf.Max(0.0f, constraints.maxHeight - 2 * material_._kMenuItemHeight); float width = Mathf.Min(constraints.maxWidth, buttonRect.width); return new BoxConstraints( minWidth: width, maxWidth: width, minHeight: 0.0f, maxHeight: maxHeight ); } public override Offset getPositionForChild(Size size, Size childSize) { _MenuLimits menuLimits = route.getMenuLimits(buttonRect, size.height, route.selectedIndex ?? 0); D.assert(() => { Rect container = Offset.zero & size; if (container.intersect(buttonRect) == buttonRect) { D.assert(menuLimits.top >= 0.0f); D.assert(menuLimits.top + menuLimits.height <= size.height); } return true; }); D.assert(textDirection != null); float left = 0; switch (textDirection) { case TextDirection.rtl: left = (buttonRect.right.clamp(0.0f, size.width)) - childSize.width; break; case TextDirection.ltr: left = buttonRect.left.clamp(0.0f, size.width - childSize.width); break; } return new Offset(left, menuLimits.top ?? 0); } public override bool shouldRelayout(SingleChildLayoutDelegate _oldDelegate) { _DropdownMenuRouteLayout oldDelegate = _oldDelegate as _DropdownMenuRouteLayout; return buttonRect != oldDelegate.buttonRect || textDirection != oldDelegate.textDirection; } } class _DropdownRouteResult { public _DropdownRouteResult(T result) { this.result = result; } public readonly T result; public static bool operator ==(_DropdownRouteResult left, _DropdownRouteResult right) { return left.Equals(right); } public static bool operator !=(_DropdownRouteResult left, _DropdownRouteResult right) { return !left.Equals(right); } public bool Equals(_DropdownRouteResult other) { return result.Equals(other.result); } public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) { return false; } if (ReferenceEquals(this, obj)) { return true; } if (obj.GetType() != GetType()) { return false; } return Equals((_DropdownRouteResult) obj); } public override int GetHashCode() { return result.GetHashCode(); } } class _MenuLimits { internal _MenuLimits(float? top, float? bottom, float? height, float? scrollOffset) { this.top = top; this.bottom = bottom; this.height = height; this.scrollOffset = scrollOffset; } public readonly float? top; public readonly float? bottom; public readonly float? height; public readonly float? scrollOffset; } class _DropdownRoute : PopupRoute<_DropdownRouteResult> { public _DropdownRoute( List<_MenuItem> items = null, EdgeInsetsGeometry padding = null, Rect buttonRect = null, int? selectedIndex = null, int elevation = 8, ThemeData theme = null, TextStyle style = null, string barrierLabel = null, float? itemHeight = null, Color dropdownColor = null ) { D.assert(style != null); this.items = items; this.padding = padding; this.buttonRect = buttonRect; this.selectedIndex = selectedIndex; this.elevation = elevation; this.theme = theme; this.style = style; this.barrierLabel = barrierLabel; this.itemHeight = itemHeight; this.dropdownColor = dropdownColor; itemHeights = Enumerable.Repeat(itemHeight ?? material_.kMinInteractiveDimension, items.Count).ToList(); } public readonly List<_MenuItem> items; public readonly EdgeInsetsGeometry padding; public readonly Rect buttonRect; public readonly int? selectedIndex; public readonly int elevation; public readonly ThemeData theme; public readonly TextStyle style; public readonly float? itemHeight; public readonly Color dropdownColor; public readonly List itemHeights; public ScrollController scrollController; public override TimeSpan transitionDuration { get { return material_._kDropdownMenuDuration; } } public override bool barrierDismissible { get { return true; } } public override Color barrierColor { get { return null; } } public string barrierLabel; public override Widget buildPage(BuildContext context, Animation animation, Animation secondaryAnimation) { return new LayoutBuilder( builder: (ctx, constraints) => { return new _DropdownRoutePage( route: this, constraints: constraints, items: items, padding: padding, buttonRect: buttonRect, selectedIndex: selectedIndex, elevation: elevation, theme: theme, style: style, dropdownColor: dropdownColor ); } ); } internal void _dismiss() { navigator?.removeRoute(this); } public float getItemOffset(int index) { float offset = material_.kMaterialListPadding.top; if (items.isNotEmpty() && index > 0) { D.assert(items.Count == itemHeights?.Count); offset += itemHeights .Aggregate(0, (float total, float height) => total + height); } return offset; } public _MenuLimits getMenuLimits(Rect buttonRect, float availableHeight, int index) { float maxMenuHeight = availableHeight - 2.0f * material_._kMenuItemHeight; float buttonTop = buttonRect.top; float buttonBottom = Mathf.Min(buttonRect.bottom, availableHeight); float selectedItemOffset = getItemOffset(index); float topLimit = Mathf.Min(material_._kMenuItemHeight, buttonTop); float bottomLimit = Mathf.Max(availableHeight - material_._kMenuItemHeight, buttonBottom); float menuTop = (buttonTop - selectedItemOffset) - (itemHeights[selectedIndex ?? 0] - buttonRect.height) / 2.0f; float preferredMenuHeight = material_.kMaterialListPadding.vertical; if (items.isNotEmpty()) preferredMenuHeight += itemHeights.Aggregate((float total, float height) => total + height); float menuHeight = Mathf.Min(maxMenuHeight, preferredMenuHeight); float menuBottom = menuTop + menuHeight; if (menuTop < topLimit) menuTop = Mathf.Min(buttonTop, topLimit); if (menuBottom > bottomLimit) { menuBottom = Mathf.Max(buttonBottom, bottomLimit); menuTop = menuBottom - menuHeight; } float scrollOffset = preferredMenuHeight <= maxMenuHeight ? 0 : Mathf.Max(0.0f, selectedItemOffset - (buttonTop - menuTop)); return new _MenuLimits(menuTop, menuBottom, menuHeight, scrollOffset); } } class _DropdownRoutePage : StatelessWidget { public _DropdownRoutePage( Key key = null, _DropdownRoute route = null, BoxConstraints constraints = null, List<_MenuItem> items = null, EdgeInsetsGeometry padding = null, Rect buttonRect = null, int? selectedIndex = null, int elevation = 0, ThemeData theme = null, TextStyle style = null, Color dropdownColor = null ) : base(key: key) { this.route = route; this.constraints = constraints; this.items = items; this.padding = padding; this.buttonRect = buttonRect; this.selectedIndex = selectedIndex; this.elevation = elevation; this.theme = theme; this.style = style; this.dropdownColor = dropdownColor; } public readonly _DropdownRoute route; public readonly BoxConstraints constraints; public readonly List<_MenuItem> items; public readonly EdgeInsetsGeometry padding; public readonly Rect buttonRect; public readonly int? selectedIndex; public readonly int elevation; public readonly ThemeData theme; public readonly TextStyle style; public readonly Color dropdownColor; public override Widget build(BuildContext context) { D.assert(WidgetsD.debugCheckHasDirectionality(context)); if (route.scrollController == null) { _MenuLimits menuLimits = route.getMenuLimits(buttonRect, constraints.maxHeight, selectedIndex ?? 0); route.scrollController = new ScrollController(initialScrollOffset: menuLimits.scrollOffset ?? 0); } TextDirection textDirection = Directionality.of(context); Widget menu = new _DropdownMenu( route: route, padding: padding.resolve(textDirection), buttonRect: buttonRect, constraints: constraints, dropdownColor: dropdownColor ); if (theme != null) { menu = new Theme(data: theme, child: menu); } return MediaQuery.removePadding( context: context, removeTop: true, removeBottom: true, removeLeft: true, removeRight: true, child: new Builder( builder: (BuildContext _context) => { return new CustomSingleChildLayout( layoutDelegate: new _DropdownMenuRouteLayout( buttonRect: buttonRect, route: route, textDirection: textDirection ), child: menu ); } ) ); } } class _MenuItem : SingleChildRenderObjectWidget { internal _MenuItem( Key key = null, ValueChanged onLayout = null, DropdownMenuItem item = null ) : base(key: key, child: item) { D.assert(onLayout != null); this.onLayout = onLayout; this.item = item; } public readonly ValueChanged onLayout; public readonly DropdownMenuItem item; public override RenderObject createRenderObject(BuildContext context) { return new _RenderMenuItem(onLayout); } public override void updateRenderObject(BuildContext context, RenderObject renderObject) { if (renderObject is _RenderMenuItem renderMenuItem) { renderMenuItem.onLayout = onLayout; } } } class _RenderMenuItem : RenderProxyBox { internal _RenderMenuItem(ValueChanged onLayout = null, RenderBox child = null) : base(child) { D.assert(onLayout != null); this.onLayout = onLayout; } public ValueChanged onLayout; protected override void performLayout() { base.performLayout(); onLayout(size); } } public class _DropdownMenuItemContainer : StatelessWidget { internal _DropdownMenuItemContainer( Key key = null, Widget child = null ) : base(key: key) { D.assert(child != null); this.child = child; } public readonly Widget child; public override Widget build(BuildContext context) { return new Container( constraints: new BoxConstraints(minHeight: material_._kMenuItemHeight), alignment: AlignmentDirectional.centerStart, child: child ); } } public class DropdownMenuItem : _DropdownMenuItemContainer { public DropdownMenuItem( Key key = null, VoidCallback onTap = null, T value = default, Widget child = null ) : base(key: key, child: child) { D.assert(child != null); this.onTap = onTap; this.value = value; } public readonly VoidCallback onTap; public readonly T value; } public class DropdownButtonHideUnderline : InheritedWidget { public DropdownButtonHideUnderline( Key key = null, Widget child = null ) : base(key: key, child: child) { D.assert(child != null); } public static bool at(BuildContext context) { return context.dependOnInheritedWidgetOfExactType() != null; } public override bool updateShouldNotify(InheritedWidget oldWidget) { return false; } } public class DropdownButton : StatefulWidget{ public DropdownButton( Key key = null, List> items = null, material_.DropdownButtonBuilder selectedItemBuilder = null, T value = default, Widget hint = null, Widget disabledHint = null, ValueChanged onChanged = null, VoidCallback onTap = null, int elevation = 8, TextStyle style = null, Widget underline = null, Widget icon = null, Color iconDisabledColor = null, Color iconEnabledColor = null, float iconSize = 24.0f, bool isDense = false, bool isExpanded = false, float? itemHeight = material_.kMinInteractiveDimension, Color focusColor = null, FocusNode focusNode = null, bool autofocus = false, Color dropdownColor = null ) : base(key: key) { D.assert(items == null || items.isEmpty() || value == null || LinqUtils>.WhereList(items,((DropdownMenuItem item) => { return item.value.Equals(value); })).Count() == 1, () => "There should be exactly one item with [DropdownButton]'s value: " + $"{value}. \n" + "Either zero or 2 or more [DropdownMenuItem]s were detected " + "with the same value" ); D.assert(itemHeight == null || itemHeight >= material_.kMinInteractiveDimension); this.items = items; this.selectedItemBuilder = selectedItemBuilder; this.value = value; this.hint = hint; this.disabledHint = disabledHint; this.onChanged = onChanged; this.onTap = onTap; this.elevation = elevation; this.style = style; this.underline = underline; this.icon = icon; this.iconDisabledColor = iconDisabledColor; this.iconEnabledColor = iconEnabledColor; this.iconSize = iconSize; this.isDense = isDense; this.isExpanded = isExpanded; this.itemHeight = itemHeight; this.focusColor = focusColor; this.focusNode = focusNode; this.autofocus = autofocus; this.dropdownColor = dropdownColor; } public readonly List> items; public readonly T value; public readonly Widget hint; public readonly Widget disabledHint; public readonly ValueChanged onChanged; public readonly VoidCallback onTap; public readonly material_.DropdownButtonBuilder selectedItemBuilder; public readonly int elevation; public readonly TextStyle style; public readonly Widget underline; public readonly Widget icon; public readonly Color iconDisabledColor; public readonly Color iconEnabledColor; public readonly float iconSize; public readonly bool isDense; public readonly bool isExpanded; public readonly float? itemHeight; public readonly Color focusColor; public readonly FocusNode focusNode; public readonly bool autofocus; public readonly Color dropdownColor; public override State createState() { return new _DropdownButtonState(); } } class _DropdownButtonState : State>, WidgetsBindingObserver{ int? _selectedIndex; _DropdownRoute _dropdownRoute; Orientation? _lastOrientation; FocusNode _internalNode; public FocusNode focusNode { get { return widget.focusNode ?? _internalNode; } } bool _hasPrimaryFocus = false; Dictionary _actionMap; FocusHighlightMode _focusHighlightMode; FocusNode _createFocusNode() { return new FocusNode(debugLabel: $"{widget.GetType()}"); } public override void initState() { base.initState(); _updateSelectedIndex(); if (widget.focusNode == null) { _internalNode = _internalNode ?? _createFocusNode(); } _actionMap = new Dictionary() { {ActivateAction.key, _createAction} }; focusNode.addListener(_handleFocusChanged); FocusManager focusManager = WidgetsBinding.instance.focusManager; _focusHighlightMode = focusManager.highlightMode; focusManager.addHighlightModeListener(_handleFocusHighlightModeChange); } public override void dispose() { WidgetsBinding.instance.removeObserver(this); _removeDropdownRoute(); WidgetsBinding.instance.focusManager.removeHighlightModeListener(_handleFocusHighlightModeChange); focusNode.removeListener(_handleFocusChanged); _internalNode?.dispose(); base.dispose(); } public void didChangeMetrics() { _removeDropdownRoute(); } void _removeDropdownRoute() { _dropdownRoute?._dismiss(); _dropdownRoute = null; _lastOrientation = null; } void _handleFocusChanged() { if (_hasPrimaryFocus != focusNode.hasPrimaryFocus) { setState(() => { _hasPrimaryFocus = focusNode.hasPrimaryFocus; }); } } void _handleFocusHighlightModeChange(FocusHighlightMode mode) { if (!mounted) { return; } setState(() => { _focusHighlightMode = mode; }); } public override void didUpdateWidget(StatefulWidget oldWidget) { base.didUpdateWidget(oldWidget); if (oldWidget is DropdownButton dropdownButton && widget.focusNode != dropdownButton.focusNode) { dropdownButton.focusNode?.removeListener(_handleFocusChanged); if (widget.focusNode == null) { _internalNode = _internalNode ?? _createFocusNode(); } _hasPrimaryFocus = focusNode.hasPrimaryFocus; focusNode.addListener(_handleFocusChanged); } _updateSelectedIndex(); } public void didChangeTextScaleFactor() { } public void didChangeLocales(List locale) { } public Future didPopRoute() { return Future.value(false).to(); } public Future didPushRoute(string route) { return Future.value(false).to(); } public void didChangeAccessibilityFeatures() { //FIX ME!!!! throw new NotImplementedException(); } public void didChangePlatformBrightness() { } void _updateSelectedIndex() { if (!_enabled) { return; } D.assert(widget.value == null || LinqUtils>.WhereList(widget.items,((DropdownMenuItem item) => item.value.Equals(widget.value)) ).Count == 1); _selectedIndex = null; for (int itemIndex = 0; itemIndex < widget.items.Count; itemIndex++) { if (widget.items[itemIndex].value.Equals(widget.value)) { _selectedIndex = itemIndex; return; } } } TextStyle _textStyle { get { return widget.style ?? Theme.of(context).textTheme.subtitle1; } } void _handleTap() { RenderBox itemBox = (RenderBox) context.findRenderObject(); Rect itemRect = itemBox.localToGlobal(Offset.zero) & itemBox.size; TextDirection textDirection = Directionality.of(context); EdgeInsetsGeometry menuMargin = ButtonTheme.of(context).alignedDropdown ? material_._kAlignedMenuMargin : material_._kUnalignedMenuMargin; List<_MenuItem> menuItems = new List<_MenuItem>(new _MenuItem[widget.items.Count]); for (int index = 0; index < widget.items.Count; index += 1) { var pindex = index; menuItems[pindex] = new _MenuItem( item: widget.items[pindex], onLayout: (Size size) => { if (_dropdownRoute == null) return; _dropdownRoute.itemHeights[pindex] = size.height; } ); } D.assert(_dropdownRoute == null); _dropdownRoute = new _DropdownRoute( items: menuItems, buttonRect: menuMargin.resolve(textDirection).inflateRect(itemRect), padding: material_._kMenuItemPadding.resolve(textDirection), selectedIndex: _selectedIndex ?? 0, elevation: widget.elevation, theme: Theme.of(context, shadowThemeOnly: true), style: _textStyle, barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel, itemHeight: widget.itemHeight, dropdownColor: widget.dropdownColor ); Navigator.push(context, _dropdownRoute).then(newValue => { _DropdownRouteResult value = newValue as _DropdownRouteResult; _removeDropdownRoute(); if (!mounted || newValue == null) { return; } if (widget.onChanged != null) { widget.onChanged(value.result); } }); if (widget.onTap != null) { widget.onTap(); } } UiWidgetAction _createAction() { return new CallbackAction( ActivateAction.key, onInvoke: (FocusNode node, Intent intent) => { _handleTap(); } ); } float? _denseButtonHeight { get { float fontSize = _textStyle.fontSize ?? Theme.of(context).textTheme.subtitle1.fontSize ?? 0; return Mathf.Max(fontSize, Mathf.Max(widget.iconSize, material_._kDenseButtonHeight)); } } Color _iconColor { get { 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; } } D.assert(false); return null; } } bool _enabled { get { return widget.items != null && widget.items.isNotEmpty() && widget.onChanged != null; } } Orientation? _getOrientation(BuildContext context) { Orientation? result = MediaQuery.of(context, nullOk: true)?.orientation; if (result == null) { // If there's no MediaQuery, then use the window aspect to determine // orientation. Size size = Window.instance.physicalSize; result = size.width > size.height ? Orientation.landscape : Orientation.portrait; } return result; } bool? _showHighlight { get { switch (_focusHighlightMode) { case FocusHighlightMode.touch: return false; case FocusHighlightMode.traditional: return _hasPrimaryFocus; } return null; } } public override Widget build(BuildContext context) { D.assert(material_.debugCheckHasMaterial(context)); D.assert(material_.debugCheckHasMaterialLocalizations(context)); Orientation? newOrientation = _getOrientation(context); _lastOrientation = _lastOrientation ?? newOrientation; if (newOrientation != _lastOrientation) { _removeDropdownRoute(); _lastOrientation = newOrientation; } List items = null; if (_enabled) { items = widget.selectedItemBuilder == null ? new List(widget.items) : widget.selectedItemBuilder(context); } else { items = widget.selectedItemBuilder == null ? new List() : widget.selectedItemBuilder(context); } int hintIndex = 0; if (widget.hint != null || (!_enabled && widget.disabledHint != null)) { Widget displayedHint = _enabled ? widget.hint : widget.disabledHint ?? widget.hint; if (widget.selectedItemBuilder == null) { displayedHint = new _DropdownMenuItemContainer(child: displayedHint); } hintIndex = items.Count; items.Add(new DefaultTextStyle( style: _textStyle.copyWith(color: Theme.of(context).hintColor), child: new IgnorePointer( child: displayedHint ) )); } EdgeInsetsGeometry padding = ButtonTheme.of(context).alignedDropdown ? material_._kAlignedButtonPadding : material_._kUnalignedButtonPadding; int index = _enabled ? (_selectedIndex ?? hintIndex) : hintIndex; Widget innerItemsWidget = null; if (items.isEmpty()) { innerItemsWidget = new Container(); } else { innerItemsWidget = new IndexedStack( index: index, alignment: AlignmentDirectional.centerStart, children: widget.isDense ? items : LinqUtils.SelectList(items,(Widget item) => { return widget.itemHeight != null ? new SizedBox(height: widget.itemHeight, child: item) : (Widget) new Column(mainAxisSize: MainAxisSize.min, children: new List() {item}); })); } Icon defaultIcon = new Icon(Icons.arrow_drop_down); Widget result = new DefaultTextStyle( style: _textStyle, child: new Container( decoration: _showHighlight ?? false ? new BoxDecoration( color: widget.focusColor ?? Theme.of(context).focusColor, borderRadius: BorderRadius.all(Radius.circular(4.0f)) ) : null, padding: padding, height: widget.isDense ? _denseButtonHeight : null, child: new Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisSize: MainAxisSize.min, children: new List { widget.isExpanded ? new Expanded(child: innerItemsWidget) : (Widget) innerItemsWidget, new IconTheme( data: new IconThemeData( color: _iconColor, size: widget.iconSize ), child: widget.icon ?? defaultIcon ) } ) ) ); if (!DropdownButtonHideUnderline.at(context)) { float bottom = widget.isDense || widget.itemHeight == null ? 0.0f : 8.0f; result = new Stack( children: new List { result, new Positioned( left: 0.0f, right: 0.0f, bottom: bottom, child: widget.underline ?? new Container( height: 1.0f, decoration: new BoxDecoration( border: new Border( bottom: new BorderSide(color: new Color(0xFFBDBDBD), width: 0.0f)) ) ) ) } ); } return new Focus( canRequestFocus: _enabled, focusNode: focusNode, autofocus: widget.autofocus, child: new GestureDetector( onTap: _enabled ? (GestureTapCallback) _handleTap : null, behavior: HitTestBehavior.opaque, child: result ) ); } } public class DropdownButtonFormField : FormField where T : class { public DropdownButtonFormField( Key key = null, T value = null, List> items = null, ValueChanged onChanged = null, InputDecoration decoration = null, FormFieldSetter onSaved = null, FormFieldValidator validator = null, Widget hint = null ) : base( key: key, onSaved: onSaved, initialValue: value, validator: validator, builder: (FormFieldState field) => { InputDecoration effectiveDecoration = (decoration ?? new InputDecoration()) .applyDefaults(Theme.of(field.context).inputDecorationTheme); return new InputDecorator( decoration: effectiveDecoration.copyWith(errorText: field.errorText), isEmpty: value == null, child: new DropdownButtonHideUnderline( child: new DropdownButton( isDense: true, value: value, items: items, hint: hint, onChanged: field.didChange ) ) ); } ) { this.onChanged = onChanged; } public readonly ValueChanged onChanged; public override State createState() { return new _DropdownButtonFormFieldState(); } } class _DropdownButtonFormFieldState : FormFieldState where T : class { public new DropdownButtonFormField widget { get { return base.widget as DropdownButtonFormField; } } public override void didChange(T value) { base.didChange(value); if (widget.onChanged != null) { widget.onChanged(value); } } } }