|
|
|
|
|
|
using Unity.UIWidgets.async2; |
|
|
|
using Unity.UIWidgets.foundation; |
|
|
|
using Unity.UIWidgets.gestures; |
|
|
|
using Unity.UIWidgets.material; |
|
|
|
using Unity.UIWidgets.services; |
|
|
|
using Material = UnityEngine.Material; |
|
|
|
using Rect = Unity.UIWidgets.ui.Rect; |
|
|
|
using TextStyle = Unity.UIWidgets.painting.TextStyle; |
|
|
|
|
|
|
|
|
|
|
public const float _kMenuItemHeight = 48.0f; |
|
|
|
public const float _kMenuItemHeight = kMinInteractiveDimension; |
|
|
|
public const float _kDenseButtonHeight = 24.0f; |
|
|
|
public static readonly EdgeInsets _kMenuItemPadding = EdgeInsets.symmetric(horizontal: 16.0f); |
|
|
|
public static readonly EdgeInsets _kAlignedButtonPadding = EdgeInsets.only(left: 16.0f, right: 4.0f); |
|
|
|
|
|
|
|
|
|
|
public delegate List<Widget> DropdownButtonBuilder(BuildContext context); |
|
|
|
} |
|
|
|
|
|
|
|
class _DropdownMenuPainter : AbstractCustomPainter { |
|
|
|
|
|
|
int? selectedIndex = null, |
|
|
|
Animation<float> resize = null |
|
|
|
Animation<float> resize = null, |
|
|
|
ValueGetter<float> getSelectedItemOffset = null |
|
|
|
) : base(repaint: resize) { |
|
|
|
D.assert(elevation != null); |
|
|
|
_painter = new BoxDecoration( |
|
|
|
|
|
|
this.elevation = elevation; |
|
|
|
this.selectedIndex = selectedIndex; |
|
|
|
this.resize = resize; |
|
|
|
this.getSelectedItemOffset = getSelectedItemOffset; |
|
|
|
} |
|
|
|
|
|
|
|
public readonly Color color; |
|
|
|
|
|
|
|
|
|
|
public readonly ValueGetter<float> getSelectedItemOffset; |
|
|
|
float selectedItemOffset = selectedIndex ?? 0 * material_._kMenuItemHeight + |
|
|
|
material_.kMaterialListPadding.top; |
|
|
|
float selectedItemOffset = getSelectedItemOffset(); |
|
|
|
|
|
|
|
FloatTween top = new FloatTween( |
|
|
|
begin: selectedItemOffset.clamp(0.0f, size.height - material_._kMenuItemHeight), |
|
|
|
end: 0.0f |
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
class _DropdownMenuItemButton<T> : StatefulWidget where T : class { |
|
|
|
internal _DropdownMenuItemButton( |
|
|
|
Key key = null, |
|
|
|
_DropdownRoute<T> 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<T> route; |
|
|
|
public EdgeInsets padding; |
|
|
|
public Rect buttonRect; |
|
|
|
public BoxConstraints constraints; |
|
|
|
public int? itemIndex; |
|
|
|
|
|
|
|
public override State createState() => new _DropdownMenuItemButtonState<T>(); |
|
|
|
} |
|
|
|
|
|
|
|
class _DropdownMenuItemButtonState<T> : State<_DropdownMenuItemButton<T>> where T : class { |
|
|
|
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<T> dropdownMenuItem = widget.route.items[widget.itemIndex ?? 0].item; |
|
|
|
|
|
|
|
if (dropdownMenuItem.onTap != null) { |
|
|
|
dropdownMenuItem.onTap(); |
|
|
|
} |
|
|
|
|
|
|
|
Navigator.pop( |
|
|
|
context, |
|
|
|
new _DropdownRouteResult<T>(dropdownMenuItem.value) |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
static readonly Dictionary<LogicalKeySet, Intent> _webShortcuts = new Dictionary<LogicalKeySet, Intent> { |
|
|
|
{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 material.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; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
_DropdownRoute<T> route = null |
|
|
|
_DropdownRoute<T> route = null, |
|
|
|
Rect buttonRect = null, |
|
|
|
BoxConstraints constraints = null, |
|
|
|
Color dropdownColor = null |
|
|
|
this.buttonRect = buttonRect; |
|
|
|
this.constraints = constraints; |
|
|
|
this.dropdownColor = dropdownColor; |
|
|
|
public readonly Rect buttonRect; |
|
|
|
public readonly BoxConstraints constraints; |
|
|
|
public readonly Color dropdownColor; |
|
|
|
|
|
|
|
|
|
|
|
public override State createState() { |
|
|
|
return new _DropdownMenuState<T>(); |
|
|
|
|
|
|
D.assert(material_.debugCheckHasMaterialLocalizations(context)); |
|
|
|
MaterialLocalizations localizations = MaterialLocalizations.of(context); |
|
|
|
_DropdownRoute<T> route = widget.route; |
|
|
|
float unit = 0.5f / (route.items.Count + 1.5f); |
|
|
|
CurvedAnimation opacity; |
|
|
|
if (itemIndex == route.selectedIndex) { |
|
|
|
opacity = new CurvedAnimation(parent: route.animation, curve: new Threshold(0.0f)); |
|
|
|
} |
|
|
|
else { |
|
|
|
float start = (0.5f + (itemIndex + 1) * unit).clamp(0.0f, 1.0f); |
|
|
|
float end = (start + 1.5f * unit).clamp(0.0f, 1.0f); |
|
|
|
opacity = new CurvedAnimation(parent: route.animation, curve: new Interval(start, end)); |
|
|
|
} |
|
|
|
|
|
|
|
var index = itemIndex; |
|
|
|
children.Add(new FadeTransition( |
|
|
|
opacity: opacity, |
|
|
|
child: new InkWell( |
|
|
|
child: new Container( |
|
|
|
padding: widget.padding, |
|
|
|
child: route.items[itemIndex] |
|
|
|
), |
|
|
|
onTap: () => Navigator.pop( |
|
|
|
context, |
|
|
|
new _DropdownRouteResult<T>(route.items[index].value) |
|
|
|
) |
|
|
|
) |
|
|
|
children.Add(new _DropdownMenuItemButton<T>( |
|
|
|
route: widget.route, |
|
|
|
padding: widget.padding, |
|
|
|
buttonRect: widget.buttonRect, |
|
|
|
constraints: widget.constraints, |
|
|
|
itemIndex: itemIndex |
|
|
|
)); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
painter: new _DropdownMenuPainter( |
|
|
|
color: Theme.of(context).canvasColor, |
|
|
|
color: widget.dropdownColor ?? Theme.of(context).canvasColor, |
|
|
|
resize: _resize |
|
|
|
resize: _resize, |
|
|
|
getSelectedItemOffset: () => route.getItemOffset(route.selectedIndex ?? 0) |
|
|
|
), |
|
|
|
child: new Material( |
|
|
|
type: MaterialType.transparency, |
|
|
|
|
|
|
child: new ListView( |
|
|
|
controller: widget.route.scrollController, |
|
|
|
padding: material_.kMaterialListPadding, |
|
|
|
itemExtent: material_._kMenuItemHeight, |
|
|
|
shrinkWrap: true, |
|
|
|
children: children |
|
|
|
) |
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
class _DropdownMenuRouteLayout<T> : SingleChildLayoutDelegate { |
|
|
|
class _DropdownMenuRouteLayout<T> : SingleChildLayoutDelegate where T : class { |
|
|
|
float menuTop, |
|
|
|
float menuHeight |
|
|
|
_DropdownRoute<T> route = null, |
|
|
|
TextDirection? textDirection = null |
|
|
|
this.menuTop = menuTop; |
|
|
|
this.menuHeight = menuHeight; |
|
|
|
this.route = route; |
|
|
|
this.textDirection = textDirection; |
|
|
|
public readonly float menuTop; |
|
|
|
public readonly float menuHeight; |
|
|
|
public readonly _DropdownRoute<T> route; |
|
|
|
public readonly TextDirection? textDirection; |
|
|
|
|
|
|
|
public override BoxConstraints getConstraintsForChild(BoxConstraints constraints) { |
|
|
|
float maxHeight = Mathf.Max(0.0f, constraints.maxHeight - 2 * material_._kMenuItemHeight); |
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
public override Offset getPositionForChild(Size size, Size childSize) { |
|
|
|
_MenuLimits menuLimits = route.getMenuLimits(buttonRect, size.height, route.selectedIndex ?? 0); |
|
|
|
D.assert(menuTop >= 0.0f); |
|
|
|
D.assert(menuTop + menuHeight <= size.height); |
|
|
|
D.assert(menuLimits.top >= 0.0f); |
|
|
|
D.assert(menuLimits.top + menuLimits.height <= size.height); |
|
|
|
float left = buttonRect.right.clamp(0.0f, size.width) - childSize.width; |
|
|
|
return new Offset(left, menuTop); |
|
|
|
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); |
|
|
|
return buttonRect != oldDelegate.buttonRect |
|
|
|
|| menuTop != oldDelegate.menuTop |
|
|
|
|| menuHeight != oldDelegate.menuHeight; |
|
|
|
return buttonRect != oldDelegate.buttonRect || textDirection != oldDelegate.textDirection; |
|
|
|
class _DropdownRouteResult<T> where T: class { |
|
|
|
class _DropdownRouteResult<T> where T : class { |
|
|
|
public _DropdownRouteResult(T result) { |
|
|
|
this.result = result; |
|
|
|
} |
|
|
|
|
|
|
public bool Equals(_DropdownRouteResult<T> other) { |
|
|
|
return result == other.result; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return Equals((_DropdownRouteResult<T>) obj); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
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; |
|
|
|
} |
|
|
|
|
|
|
|
List<DropdownMenuItem<T>> items = null, |
|
|
|
List<_MenuItem<T>> items = null, |
|
|
|
EdgeInsets padding = null, |
|
|
|
Rect buttonRect = null, |
|
|
|
int? selectedIndex = null, |
|
|
|
|
|
|
string barrierLabel = null |
|
|
|
string barrierLabel = null, |
|
|
|
float? itemHeight = null, |
|
|
|
Color dropdownColor = null |
|
|
|
) { |
|
|
|
D.assert(style != null); |
|
|
|
this.items = items; |
|
|
|
|
|
|
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<DropdownMenuItem<T>> items; |
|
|
|
public readonly List<_MenuItem<T>> items; |
|
|
|
public readonly EdgeInsets padding; |
|
|
|
public readonly Rect buttonRect; |
|
|
|
public readonly int? selectedIndex; |
|
|
|
|
|
|
public readonly float? itemHeight; |
|
|
|
public readonly Color dropdownColor; |
|
|
|
public readonly List<float> itemHeights; |
|
|
|
public ScrollController scrollController; |
|
|
|
|
|
|
|
public override TimeSpan transitionDuration { |
|
|
|
|
|
|
selectedIndex: selectedIndex, |
|
|
|
elevation: elevation, |
|
|
|
theme: theme, |
|
|
|
style: style |
|
|
|
style: style, |
|
|
|
dropdownColor: dropdownColor |
|
|
|
); |
|
|
|
} |
|
|
|
); |
|
|
|
|
|
|
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<T> : StatelessWidget where T : class { |
|
|
|
|
|
|
BoxConstraints constraints = null, |
|
|
|
List<DropdownMenuItem<T>> items = null, |
|
|
|
List<_MenuItem<T>> items = null, |
|
|
|
TextStyle style = null |
|
|
|
TextStyle style = null, |
|
|
|
Color dropdownColor = null |
|
|
|
) : base(key: key) { |
|
|
|
this.route = route; |
|
|
|
this.constraints = constraints; |
|
|
|
|
|
|
this.elevation = elevation; |
|
|
|
this.theme = theme; |
|
|
|
this.style = style; |
|
|
|
this.dropdownColor = dropdownColor; |
|
|
|
public readonly List<DropdownMenuItem<T>> items; |
|
|
|
public readonly List<_MenuItem<T>> items; |
|
|
|
public readonly EdgeInsets padding; |
|
|
|
public readonly Rect buttonRect; |
|
|
|
public readonly int? selectedIndex; |
|
|
|
|
|
|
|
|
|
|
public readonly Color dropdownColor; |
|
|
|
|
|
|
|
float availableHeight = constraints.maxHeight; |
|
|
|
float maxMenuHeight = availableHeight - 2.0f * material_._kMenuItemHeight; |
|
|
|
|
|
|
|
float buttonTop = buttonRect.top; |
|
|
|
float buttonBottom = Mathf.Min(buttonRect.bottom, availableHeight); |
|
|
|
|
|
|
|
float topLimit = Mathf.Min(material_._kMenuItemHeight, buttonTop); |
|
|
|
float bottomLimit = Mathf.Max(availableHeight - material_._kMenuItemHeight, buttonBottom); |
|
|
|
|
|
|
|
float? selectedItemOffset = selectedIndex * material_._kMenuItemHeight + |
|
|
|
material_.kMaterialListPadding.top; |
|
|
|
|
|
|
|
float? menuTop = (buttonTop - selectedItemOffset) - |
|
|
|
(material_._kMenuItemHeight - buttonRect.height) / 2.0f; |
|
|
|
float preferredMenuHeight = (items.Count * material_._kMenuItemHeight) + |
|
|
|
material_.kMaterialListPadding.vertical; |
|
|
|
|
|
|
|
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 |
|
|
|
? Mathf.Max(0.0f, selectedItemOffset ?? 0.0f - (buttonTop - (menuTop ?? 0.0f))) |
|
|
|
: 0.0f; |
|
|
|
route.scrollController = new ScrollController(initialScrollOffset: scrollOffset); |
|
|
|
_MenuLimits menuLimits = route.getMenuLimits(buttonRect, constraints.maxHeight, selectedIndex ?? 0); |
|
|
|
route.scrollController = new ScrollController(initialScrollOffset: menuLimits.scrollOffset ?? 0); |
|
|
|
TextDirection textDirection = Directionality.of(context); |
|
|
|
padding: padding |
|
|
|
padding: padding.resolve(textDirection), |
|
|
|
buttonRect: buttonRect, |
|
|
|
constraints: constraints, |
|
|
|
dropdownColor: dropdownColor |
|
|
|
); |
|
|
|
|
|
|
|
if (theme != null) { |
|
|
|
|
|
|
return new CustomSingleChildLayout( |
|
|
|
layoutDelegate: new _DropdownMenuRouteLayout<T>( |
|
|
|
buttonRect: buttonRect, |
|
|
|
menuTop: menuTop ?? 0.0f, |
|
|
|
menuHeight: menuHeight |
|
|
|
route: route, |
|
|
|
textDirection: textDirection |
|
|
|
), |
|
|
|
child: menu |
|
|
|
); |
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
public class DropdownMenuItem<T> : StatelessWidget where T : class { |
|
|
|
public DropdownMenuItem( |
|
|
|
class _MenuItem<T> : SingleChildRenderObjectWidget where T : class { |
|
|
|
internal _MenuItem( |
|
|
|
Key key = null, |
|
|
|
ValueChanged<Size> onLayout = null, |
|
|
|
DropdownMenuItem<T> item = null |
|
|
|
) : base(key: key, child: item) { |
|
|
|
D.assert(onLayout != null); |
|
|
|
this.onLayout = onLayout; |
|
|
|
this.item = item; |
|
|
|
} |
|
|
|
|
|
|
|
public readonly ValueChanged<Size> onLayout; |
|
|
|
public readonly DropdownMenuItem<T> 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<Size> onLayout = null, RenderBox child = null) : base(child) { |
|
|
|
D.assert(onLayout != null); |
|
|
|
this.onLayout = onLayout; |
|
|
|
} |
|
|
|
|
|
|
|
public ValueChanged<Size> onLayout; |
|
|
|
|
|
|
|
protected override void performLayout() { |
|
|
|
base.performLayout(); |
|
|
|
onLayout(size); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
public class _DropdownMenuItemContainer : StatelessWidget { |
|
|
|
internal _DropdownMenuItemContainer( |
|
|
|
T value = null, |
|
|
|
this.value = value; |
|
|
|
|
|
|
|
public readonly T value; |
|
|
|
height: material_._kMenuItemHeight, |
|
|
|
alignment: Alignment.centerLeft, |
|
|
|
constraints: new BoxConstraints(minHeight: material_._kMenuItemHeight), |
|
|
|
alignment: AlignmentDirectional.centerStart, |
|
|
|
public class DropdownMenuItem<T> : _DropdownMenuItemContainer where T : class { |
|
|
|
public DropdownMenuItem( |
|
|
|
Key key = null, |
|
|
|
VoidCallback onTap = null, |
|
|
|
T value = null, |
|
|
|
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, |
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
public static bool at(BuildContext context) { |
|
|
|
return context.inheritFromWidgetOfExactType(typeof(DropdownButtonHideUnderline)) != null; |
|
|
|
return context.dependOnInheritedWidgetOfExactType<DropdownButtonHideUnderline>() != null; |
|
|
|
} |
|
|
|
|
|
|
|
public override bool updateShouldNotify(InheritedWidget oldWidget) { |
|
|
|
|
|
|
public DropdownButton( |
|
|
|
Key key = null, |
|
|
|
List<DropdownMenuItem<T>> items = null, |
|
|
|
material_.DropdownButtonBuilder selectedItemBuilder = null, |
|
|
|
VoidCallback onTap = null, |
|
|
|
int elevation = 8, |
|
|
|
TextStyle style = null, |
|
|
|
Widget underline = null, |
|
|
|
|
|
|
float iconSize = 24.0f, |
|
|
|
bool isDense = false, |
|
|
|
bool isExpanded = false |
|
|
|
bool isExpanded = false, |
|
|
|
float? itemHeight = material_.kMinInteractiveDimension, |
|
|
|
Color focusColor = null, |
|
|
|
FocusNode focusNode = null, |
|
|
|
bool autofocus = false, |
|
|
|
Color dropdownColor = null |
|
|
|
D.assert(items == null || value == null || |
|
|
|
items.Where<DropdownMenuItem<T>>((DropdownMenuItem<T> item) => item.value.Equals(value)).ToList() |
|
|
|
.Count == 1); |
|
|
|
D.assert(items == null || items.isEmpty() || value == null || |
|
|
|
items.Where((DropdownMenuItem<T> item) => { return item.value == 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.selectedItemBuilder = selectedItemBuilder; |
|
|
|
this.onTap = onTap; |
|
|
|
this.elevation = elevation; |
|
|
|
this.style = style; |
|
|
|
this.underline = underline; |
|
|
|
|
|
|
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<DropdownMenuItem<T>> items; |
|
|
|
|
|
|
|
|
|
|
public readonly ValueChanged<T> onChanged; |
|
|
|
|
|
|
|
public readonly VoidCallback onTap; |
|
|
|
|
|
|
|
public readonly material_.DropdownButtonBuilder selectedItemBuilder; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public readonly Color iconEnabledColor; |
|
|
|
|
|
|
|
public readonly float iconSize; |
|
|
|
|
|
|
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<T>(); |
|
|
|
} |
|
|
|
|
|
|
int? _selectedIndex; |
|
|
|
_DropdownRoute<T> _dropdownRoute; |
|
|
|
Orientation? _lastOrientation; |
|
|
|
FocusNode _internalNode; |
|
|
|
public void didChangeTextScaleFactor() { |
|
|
|
public FocusNode focusNode { |
|
|
|
get { return widget.focusNode ?? _internalNode; } |
|
|
|
public void didChangeLocales(List<Locale> locale) { |
|
|
|
} |
|
|
|
bool _hasPrimaryFocus = false; |
|
|
|
Dictionary<LocalKey, ActionFactory> _actionMap; |
|
|
|
FocusHighlightMode _focusHighlightMode; |
|
|
|
public Future<bool> didPopRoute() { |
|
|
|
return Future.value(false).to<bool>(); |
|
|
|
} |
|
|
|
|
|
|
|
public Future<bool> didPushRoute(string route) { |
|
|
|
return Future.value(false).to<bool>(); |
|
|
|
} |
|
|
|
|
|
|
|
public void didChangeAccessibilityFeatures() { |
|
|
|
//FIX ME!!!!
|
|
|
|
throw new NotImplementedException(); |
|
|
|
} |
|
|
|
|
|
|
|
public void didChangePlatformBrightness() { |
|
|
|
FocusNode _createFocusNode() { |
|
|
|
return new FocusNode(debugLabel: $"{widget.GetType()}"); |
|
|
|
WidgetsBinding.instance.addObserver(this); |
|
|
|
if (widget.focusNode == null) { |
|
|
|
_internalNode = _internalNode?? _createFocusNode(); |
|
|
|
} |
|
|
|
|
|
|
|
_actionMap = new Dictionary<LocalKey, ActionFactory>() { |
|
|
|
{ActivateAction.key, _createAction} |
|
|
|
}; |
|
|
|
focusNode.addListener(_handleFocusChanged); |
|
|
|
FocusManager focusManager = WidgetsBinding.instance.focusManager; |
|
|
|
_focusHighlightMode = focusManager.highlightMode; |
|
|
|
focusManager.addHighlightModeListener(_handleFocusHighlightModeChange); |
|
|
|
WidgetsBinding.instance.focusManager.removeHighlightModeListener(_handleFocusHighlightModeChange); |
|
|
|
focusNode.removeListener(_handleFocusChanged); |
|
|
|
_internalNode?.dispose(); |
|
|
|
base.dispose(); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_lastOrientation = null; |
|
|
|
} |
|
|
|
|
|
|
|
void _handleFocusChanged() { |
|
|
|
if (_hasPrimaryFocus != focusNode.hasPrimaryFocus) { |
|
|
|
setState(() => { _hasPrimaryFocus = focusNode.hasPrimaryFocus; }); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
void _handleFocusHighlightModeChange(FocusHighlightMode mode) { |
|
|
|
if (!mounted) { |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
setState(() => { _focusHighlightMode = mode; }); |
|
|
|
if (oldWidget is DropdownButton<T> dropdownButton && widget.focusNode != dropdownButton.focusNode) { |
|
|
|
dropdownButton.focusNode?.removeListener(_handleFocusChanged); |
|
|
|
if (widget.focusNode == null) { |
|
|
|
_internalNode =_internalNode?? _createFocusNode(); |
|
|
|
} |
|
|
|
|
|
|
|
_hasPrimaryFocus = focusNode.hasPrimaryFocus; |
|
|
|
focusNode.addListener(_handleFocusChanged); |
|
|
|
} |
|
|
|
|
|
|
|
public void didChangeTextScaleFactor() { |
|
|
|
} |
|
|
|
|
|
|
|
public void didChangeLocales(List<Locale> locale) { |
|
|
|
} |
|
|
|
|
|
|
|
public Future<bool> didPopRoute() { |
|
|
|
return Future.value(false).to<bool>(); |
|
|
|
} |
|
|
|
|
|
|
|
public Future<bool> didPushRoute(string route) { |
|
|
|
return Future.value(false).to<bool>(); |
|
|
|
} |
|
|
|
|
|
|
|
public void didChangeAccessibilityFeatures() { |
|
|
|
//FIX ME!!!!
|
|
|
|
throw new NotImplementedException(); |
|
|
|
} |
|
|
|
|
|
|
|
public void didChangePlatformBrightness() { |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
void _updateSelectedIndex() { |
|
|
|
if (!_enabled) { |
|
|
|
return; |
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
TextStyle _textStyle { |
|
|
|
get { return widget.style ?? Theme.of(context).textTheme.subhead; } |
|
|
|
get { return widget.style ?? Theme.of(context).textTheme.subtitle1; } |
|
|
|
} |
|
|
|
|
|
|
|
void _handleTap() { |
|
|
|
|
|
|
? material_._kAlignedMenuMargin |
|
|
|
: material_._kUnalignedMenuMargin; |
|
|
|
|
|
|
|
List<_MenuItem<T>> menuItems = new List<_MenuItem<T>>(widget.items.Count); |
|
|
|
for (int index = 0; index < widget.items.Count; index += 1) { |
|
|
|
menuItems[index] = new _MenuItem<T>( |
|
|
|
item: widget.items[index], |
|
|
|
onLayout: (Size size) => { |
|
|
|
if (_dropdownRoute == null) |
|
|
|
return; |
|
|
|
|
|
|
|
_dropdownRoute.itemHeights[index] = size.height; |
|
|
|
} |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
items: widget.items, |
|
|
|
items: menuItems, |
|
|
|
buttonRect: menuMargin.inflateRect(itemRect), |
|
|
|
padding: material_._kMenuItemPadding, |
|
|
|
selectedIndex: _selectedIndex ?? 0, |
|
|
|
|
|
|
barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel |
|
|
|
barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel, |
|
|
|
itemHeight: widget.itemHeight, |
|
|
|
dropdownColor: widget.dropdownColor |
|
|
|
_dropdownRoute = null; |
|
|
|
_removeDropdownRoute(); |
|
|
|
if (!mounted || newValue == null) { |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
} |
|
|
|
}); |
|
|
|
if (widget.onTap != null) { |
|
|
|
widget.onTap(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
UiWidgetAction _createAction() { |
|
|
|
return new CallbackAction( |
|
|
|
ActivateAction.key, |
|
|
|
onInvoke: (FocusNode node, Intent intent) => { _handleTap(); } |
|
|
|
); |
|
|
|
return Mathf.Max(_textStyle.fontSize ?? 0.0f, |
|
|
|
Mathf.Max(widget.iconSize, material_._kDenseButtonHeight)); |
|
|
|
float fontSize = _textStyle.fontSize ?? Theme.of(context).textTheme.subtitle1.fontSize ?? 0; |
|
|
|
|
|
|
|
return Mathf.Max(fontSize, Mathf.Max(widget.iconSize, material_._kDenseButtonHeight)); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
return Colors.white10; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
D.assert(false); |
|
|
|
return null; |
|
|
|
} |
|
|
|
|
|
|
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; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
Orientation? newOrientation = _getOrientation(context); |
|
|
|
_lastOrientation = _lastOrientation ?? newOrientation; |
|
|
|
if (newOrientation != _lastOrientation) { |
|
|
|
_removeDropdownRoute(); |
|
|
|
_lastOrientation = newOrientation; |
|
|
|
} |
|
|
|
List<Widget> items = _enabled ? new List<Widget>(widget.items) : new List<Widget>(); |
|
|
|
List<Widget> items = null; |
|
|
|
if (_enabled) { |
|
|
|
items = widget.selectedItemBuilder == null |
|
|
|
? new List<Widget>(widget.items) |
|
|
|
: widget.selectedItemBuilder(context); |
|
|
|
} |
|
|
|
else { |
|
|
|
items = widget.selectedItemBuilder == null |
|
|
|
? new List<Widget>() |
|
|
|
: widget.selectedItemBuilder(context); |
|
|
|
} |
|
|
|
|
|
|
|
Widget emplacedHint = |
|
|
|
_enabled |
|
|
|
? widget.hint |
|
|
|
: new DropdownMenuItem<Widget>(child: widget.disabledHint ?? widget.hint); |
|
|
|
Widget displayedHint = _enabled ? widget.hint : widget.disabledHint ?? widget.hint; |
|
|
|
if (widget.selectedItemBuilder == null) { |
|
|
|
displayedHint = new _DropdownMenuItemContainer(child: displayedHint); |
|
|
|
} |
|
|
|
|
|
|
|
child: emplacedHint |
|
|
|
child: displayedHint |
|
|
|
) |
|
|
|
)); |
|
|
|
} |
|
|
|
|
|
|
: material_._kUnalignedButtonPadding; |
|
|
|
|
|
|
|
IndexedStack innerItemsWidget = new IndexedStack( |
|
|
|
index: _enabled ? (_selectedIndex ?? hintIndex) : hintIndex, |
|
|
|
alignment: Alignment.centerLeft, |
|
|
|
children: items |
|
|
|
); |
|
|
|
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 |
|
|
|
: items.Select((Widget item) => { |
|
|
|
return widget.itemHeight != null |
|
|
|
? new SizedBox(height: widget.itemHeight, child: item) |
|
|
|
: (Widget) new Column(mainAxisSize: MainAxisSize.min, |
|
|
|
children: new List<Widget>() {item}); |
|
|
|
}).ToList() |
|
|
|
); |
|
|
|
} |
|
|
|
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( |
|
|
|
|
|
|
); |
|
|
|
|
|
|
|
if (!DropdownButtonHideUnderline.at(context)) { |
|
|
|
float bottom = widget.isDense ? 0.0f : 8.0f; |
|
|
|
float bottom = widget.isDense || widget.itemHeight == null ? 0.0f : 8.0f; |
|
|
|
result = new Stack( |
|
|
|
children: new List<Widget> { |
|
|
|
result, |
|
|
|
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
return new GestureDetector( |
|
|
|
onTap: _enabled ? (GestureTapCallback) _handleTap : null, |
|
|
|
behavior: HitTestBehavior.opaque, |
|
|
|
child: result |
|
|
|
return new Focus( |
|
|
|
canRequestFocus: _enabled, |
|
|
|
focusNode: focusNode, |
|
|
|
autofocus: widget.autofocus, |
|
|
|
child: new GestureDetector( |
|
|
|
onTap: _enabled ? (GestureTapCallback) _handleTap : null, |
|
|
|
behavior: HitTestBehavior.opaque, |
|
|
|
child: result |
|
|
|
) |
|
|
|
); |
|
|
|
} |
|
|
|
} |
|
|
|