From 1fdae703584956396ba8ef689ca736b3831574e5 Mon Sep 17 00:00:00 2001 From: AYM1607 Date: Sun, 31 Mar 2019 21:50:58 -0600 Subject: [PATCH] Created cusotm dropdown menu to match the mocks --- lib/src/screens/new_task_screen.dart | 17 + lib/src/widgets/big_text_input.dart | 3 + lib/src/widgets/custom_dropdown.dart | 812 +++++++++++++++++++++++++++ 3 files changed, 832 insertions(+) create mode 100644 lib/src/widgets/custom_dropdown.dart diff --git a/lib/src/screens/new_task_screen.dart b/lib/src/screens/new_task_screen.dart index 0e2c221..1ab7c71 100644 --- a/lib/src/screens/new_task_screen.dart +++ b/lib/src/screens/new_task_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import '../blocs/new_task_bloc.dart'; import '../widgets/custom_app_bar.dart'; +import '../widgets/custom_dropdown.dart'; import '../widgets/big_text_input.dart'; class NewTaskScreen extends StatefulWidget { @@ -24,6 +25,22 @@ class _NewTaskScreenState extends State { BigTextInput( height: 95, ), + SizedBox( + height: 10, + ), + CustomDropdownButton( + hint: Text('Event'), + onChanged: (item) {}, + items: ['Math', 'Lenguajes', 'Redes'].map((String name) { + return CustomDropdownMenuItem( + value: name, + child: Text( + name, + style: TextStyle(color: Colors.white), + ), + ); + }).toList(), + ), ], ), ), diff --git a/lib/src/widgets/big_text_input.dart b/lib/src/widgets/big_text_input.dart index 770c751..cf6d14d 100644 --- a/lib/src/widgets/big_text_input.dart +++ b/lib/src/widgets/big_text_input.dart @@ -3,14 +3,17 @@ import 'package:flutter/material.dart'; class BigTextInput extends StatelessWidget { final double height; final double width; + final bool elevated; BigTextInput({ this.height, this.width, + this.elevated = true, }); Widget build(BuildContext context) { return Material( + elevation: elevated ? 10 : 0, child: ConstrainedBox( constraints: BoxConstraints( minWidth: 100, diff --git a/lib/src/widgets/custom_dropdown.dart b/lib/src/widgets/custom_dropdown.dart new file mode 100644 index 0000000..2aaaa38 --- /dev/null +++ b/lib/src/widgets/custom_dropdown.dart @@ -0,0 +1,812 @@ +// 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'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; + +const Duration _kDropdownMenuDuration = Duration(milliseconds: 300); +const double _kMenuItemHeight = 48.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 resize; + + final BoxPainter _painter; + + @override + void paint(Canvas canvas, Size size) { + // Changed this tween to avoid animating the top part of the menu. + final Tween top = Tween( + begin: + 0.0, //selectedItemOffset.clamp(0.0, size.height - _kMenuItemHeight), + end: 0.0, + ); + + final Tween bottom = Tween( + begin: + (top.begin + _kMenuItemHeight).clamp(_kMenuItemHeight, size.height), + end: size.height, + ); + + // Modified the original recatngle to be flush with the button instead of + // overflowing its original size. + final Rect rect = Rect.fromLTRB(20, top.evaluate(resize) + 10, + size.width - 20, 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 extends StatefulWidget { + const _DropdownMenu({ + Key key, + this.padding, + this.route, + }) : super(key: key); + + final _DropdownRoute route; + final EdgeInsets padding; + + @override + _DropdownMenuState createState() => _DropdownMenuState(); +} + +class _DropdownMenuState extends State<_DropdownMenu> { + 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 route = widget.route; + final double unit = 0.5 / (route.items.length + 1.5); + final List children = []; + 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, + // Added padding to compensate for the added offset in the painter. + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: InkWell( + child: Container( + padding: widget.padding, + child: route.items[itemIndex], + ), + onTap: () => Navigator.pop( + context, + _DropdownRouteResult(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: ListView( + controller: widget.route.scrollController, + padding: kMaterialListPadding, + itemExtent: _kMenuItemHeight, + shrinkWrap: true, + children: children, + ), + ), + ), + ), + ), + ), + ); + } +} + +class _DropdownMenuRouteLayout 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 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 { + const _DropdownRouteResult(this.result); + + final T result; + + @override + bool operator ==(dynamic other) { + if (other is! _DropdownRouteResult) return false; + final _DropdownRouteResult typedOther = other; + return result == typedOther.result; + } + + @override + int get hashCode => result.hashCode; +} + +class _DropdownRoute extends PopupRoute<_DropdownRouteResult> { + _DropdownRoute({ + this.items, + this.padding, + this.buttonRect, + this.selectedIndex, + this.elevation = 8, + this.theme, + @required this.style, + this.barrierLabel, + }) : assert(style != null); + + final List> 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 animation, + Animation secondaryAnimation) { + assert(debugCheckHasDirectionality(context)); + final double screenHeight = MediaQuery.of(context).size.height; + final double maxMenuHeight = screenHeight - 2.0 * _kMenuItemHeight; + + final double buttonTop = buttonRect.top; + final double buttonBottom = buttonRect.bottom; + + // 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(screenHeight - _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 (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; + scrollController = ScrollController(initialScrollOffset: scrollOffset); + } + + final TextDirection textDirection = Directionality.of(context); + Widget menu = _DropdownMenu( + route: this, + 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( + buttonRect: buttonRect, + menuTop: menuTop, + menuHeight: menuHeight, + textDirection: textDirection, + ), + child: menu, + ); + }, + ), + ); + } + + void _dismiss() { + navigator?.removeRoute(this); + } +} + +/// 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 CustomDropdownMenuItem extends StatelessWidget { + /// Creates an item for a dropdown menu. + /// + /// The [child] argument is required. + const CustomDropdownMenuItem({ + 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, + ); + } +} + +/// 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} +/// +/// This sample shows a `DropdownButton` whose value is one of +/// "One", "Two", "Free", or "Four". +/// +/// ```dart preamble +/// String dropdownValue = 'One'; +/// ``` +/// +/// ```dart +/// Widget build(BuildContext context) { +/// return Scaffold( +/// body: Center( +/// child: DropdownButton( +/// value: dropdownValue, +/// onChanged: (String newValue) { +/// setState(() { +/// dropdownValue = newValue; +/// }); +/// }, +/// items: ['One', 'Two', 'Free', 'Four'] +/// .map>((String value) { +/// return DropdownMenuItem( +/// 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. +/// * +class CustomDropdownButton 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. + CustomDropdownButton({ + Key key, + @required this.items, + this.value, + this.hint, + this.disabledHint, + @required this.onChanged, + this.elevation = 8, + this.isElevated = true, + this.style, + this.iconSize = 24.0, + this.isDense = false, + this.isExpanded = false, + }) : assert(items == null || + items.isEmpty || + value == null || + items + .where( + (CustomDropdownMenuItem item) => item.value == value) + .length == + 1), + assert(elevation != null), + assert(iconSize != null), + assert(isDense != null), + assert(isExpanded != null), + super(key: key); + + /// 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> 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 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; + + final bool isElevated; + + /// 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 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 createState() => _DropdownButtonState(); +} + +class _DropdownButtonState extends State> + with WidgetsBindingObserver { + int _selectedIndex; + _DropdownRoute _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(CustomDropdownButton oldWidget) { + super.didUpdateWidget(oldWidget); + _updateSelectedIndex(); + } + + void _updateSelectedIndex() { + if (!_enabled) { + return; + } + + assert(widget.value == null || + widget.items + .where((CustomDropdownMenuItem 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( + items: widget.items, + 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((_DropdownRouteResult newValue) { + _dropdownRoute = null; + if (!mounted || newValue == null) return; + if (widget.onChanged != null) widget.onChanged(newValue.result); + }); + } + + 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 items = + _enabled ? List.from(widget.items) : []; + int hintIndex; + if (widget.hint != null || (!_enabled && widget.disabledHint != null)) { + final Widget emplacedHint = _enabled + ? widget.hint + : CustomDropdownMenuItem( + child: widget.disabledHint ?? widget.hint); + hintIndex = items.length; + items.add(DefaultTextStyle( + style: _textStyle.copyWith( + color: Theme.of(context).cursorColor, + fontFamily: 'IBM Plex Sans', + fontWeight: FontWeight.w500, + ), + 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, + ); + } + + Widget result = DefaultTextStyle( + style: _textStyle, + child: Material( + elevation: widget.isElevated ? 10 : 0, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + borderRadius: BorderRadius.circular(8.0), + ), + padding: padding.resolve(Directionality.of(context)), + height: 35.0, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: 15, + ), + Expanded(child: innerItemsWidget), + Icon( + FontAwesomeIcons.chevronCircleDown, + size: 16.0, + color: Colors.white, + ), + SizedBox( + width: 15, + ), + ], + ), + ), + ), + ); + + return Semantics( + button: true, + child: GestureDetector( + onTap: _enabled ? _handleTap : null, + behavior: HitTestBehavior.opaque, + child: result), + ); + } +}