“This article has participated in the good article call order activity, click to see: back end, big front end double track submission, 20,000 yuan prize pool for you to challenge!”

Try the DropdownButton box with Flutter before the side dish, simple and convenient; But the original effect alone is not enough to meet all kinds of personalized design; So the side dish is based on DropdownButton, adjust part of the source code, extension for ACEDropdownButton custom drop-down box components;

  1. Add backgroundColor to set the backgroundColor of the drop-down box;
  2. Added the menuRadius drop-down box border effect.
  3. Add the default checked status and iconChecked ICONS in the isChecked Settings drop-down box.
  4. The dropdown box does not hide the DropdownButton button when it is displayed. It is displayed at the top or bottom of the button by default.
  5. The drop-down box display effect is adjusted to the default top-down;

forDropdownButtonThe overall functionality is very complete, including routing management, already animation effects; Just standing on the shoulders of giants with a little expansion, learning source code really helps our own coding;

DropdownButton source

DropdownButton source code is integrated in a file, the file has many private classes, does not affect other components;

To understand the side dish, the whole drop-down box includes three core components, respectivelyDropdownButton,_DropdownMenu_DropdownRoute;

DropdownButton is a stative component of StatefulWidget most directly faced by developers. It contains many attributes. Its basic framework is a Semantics component convenient for visually impaired people, and its core component is a hierarchical mask IndexedStack. Among them, background ICONS and other styles are drawn;

Widget innerItemsWidget; if (items.isEmpty) { innerItemsWidget = Container(); } else { innerItemsWidget = IndexedStack( index: index, alignment: AlignmentDirectional.centerStart, children: widget.isDense ? items : items.map((Widget item) { return widget.itemHeight ! = null ? SizedBox(height: widget.itemHeight, child: item) : Column(mainAxisSize: MainAxisSize.min, children: <Widget>[item]); }).toList()); }Copy the code

The _handleTap() operation on the DropdownButton is done primarily with the _DropdownRoute, which is a PopupRoute route; GetMenuLimits calculates the size and position of the drop-down box, the position of each sub-item, and so on. Here you can determine the starting position of the drop-down box display and the distance judgment from both ends of the screen, specify specific constraints; DropdownButton also plays a role in linking _DropdownMenu display;

One more thing to note in _DropdownMenuRouteLayout is to set the Menu maximum height to be at least one item container space apart from the screen height by calculating the difference between the screen height and the Menu maximum height. It is used to close the drop-down box when the user clicks.

_MenuLimits getMenuLimits(Rect buttonRect, double availableHeight, Int index) {final double maxMenuHeight = availableHeight - 2.0 * _kMenuItemHeight; final double buttonTop = buttonRect.top; final double buttonBottom = math.min(buttonRect.bottom, availableHeight); final double selectedItemOffset = getItemOffset(index); final double topLimit = math.min(_kMenuItemHeight, buttonTop); final double bottomLimit = math.max(availableHeight - _kMenuItemHeight, buttonBottom); Double menuTop = (buttontop-selecteditemoffset) - (itemHeights[selectedIndex] -buttonRect.height) / 2.0; double preferredMenuHeight = kMaterialListPadding.vertical; if (items.isNotEmpty) preferredMenuHeight += itemHeights.reduce((double total, double height) => total + height); final double menuHeight = math.min(maxMenuHeight, preferredMenuHeight); double menuBottom = menuTop + menuHeight; if (menuTop < topLimit) menuTop = math.min(buttonTop, topLimit); if (menuBottom > bottomLimit) { menuBottom = math.max(buttonBottom, bottomLimit); menuTop = menuBottom - menuHeight; } final double scrollOffset = preferredMenuHeight <= maxMenuHeight ? 0: math.max(0.0, selectedItemOffset - (buttonTop-menutop)); 0: math.max(0.0, selectedItemOffset - (buttontop-menuTop)); return _MenuLimits(menuTop, menuBottom, menuHeight, scrollOffset); }Copy the code

_DropdownMenu is a StatefulWidget. The dropdown box displays a series of animations. The animation is divided into three stages, [0-0.25s] fading into the rectangular container where the selected item is located. [0.25-0.5s] Expands the selected item to both ends until it can accommodate all items, and [0.5-1.0s] fades in from top to bottom to display the item contents;

_DropdownMenu through _DropdownMenuPainter and _DropdownMenuItemContainer respectively in the drop-down box and drawing of the item, side dishes are mainly in the drop-down box style extension;

CustomPaint(
  painter: _DropdownMenuPainter(
      color: route.backgroundColor ?? Theme.of(context).canvasColor,
      menuRadius: route.menuRadius,
      elevation: route.elevation,
      selectedIndex: route.selectedIndex,
      resize: _resize,
      getSelectedItemOffset: () => route.getItemOffset(route.selectedIndex))
Copy the code

Source has too much to learn, side dish strongly recommend to read more source;

ACEDropdownButton extension

1. BackgroundColor the backgroundColor of the drop-down box

The background color of the DropdownButton can be processed by _DropdownMenu when drawing _DropdownMenuPainter. The default background color is theme.of (context).canvascolor; Of course, we can manually set the theme canvasColor to update the drop-down box background color;

Add the backgroundColor attribute to the side dish, and set the backgroundColor of the drop-down box via ACEDropdownButton -> _DropdownRoute -> _DropdownMenu.

class _DropdownMenuState<T> extends State<_DropdownMenu<T>> { ... @override Widget build(BuildContext context) { return FadeTransition( opacity: _fadeOpacity, child: CustomPaint( painter: _DropdownMenuPainter( color: route.backgroundColor ?? Theme.of(context).canvasColor, elevation: route.elevation, selectedIndex: route.selectedIndex, resize: _resize, getSelectedItemOffset: () => route.getItemOffset(route.selectedIndex)), ... }... } return ACEDropdownButton<String>(value: dropdownValue, backgroundColor: color.green. WithOpacity (0.8), onChanged: (String newValue) => setState(() => dropdownValue = newValue), items: <String>[' Beijing ', 'Tianjin ',' Hebei ', }.map<ACEDropdownMenuItem<String>>((String value) {return ACEDropdownMenuItem<String>(value: value, child: Text(value)); }).toList());Copy the code

2. MenuRadius Dropdown frame Effect

The border of the drop-down box needs to be drawn in the _DropdownMenuPainter, the same as the backgroundColor, set the menuRadius drop-down property, and go through the _DropdownRoute. Add menuRadius to _DropdownMenuPainter;

class _DropdownMenuPainter extends CustomPainter { _DropdownMenuPainter( {this.color, this.elevation, this.selectedIndex, this.resize, this.getSelectedItemOffset, this.menuRadius}) : _painter = BoxDecoration( color: color, borderRadius: menuRadius ?? BorderRadius. Circular (2.0), boxShadow: kElevationToShadow[elevation],).createBoxPainter(), super(repaint: resize); } Return ACEDropdownButton<String>(value: dropdownValue, backgroundColor: color.green. WithOpacity (0.8), menuRadius: Const BorderRadius. All (Radius. Circular (15.0)), onChanged: (String newValue) => setState(() => dropdownValue = newValue), items: <String>[' Beijing ', 'Tianjin ',' Hebei ', }.map<ACEDropdownMenuItem<String>>((String value) {return ACEDropdownMenuItem<String>(value: value, child: Text(value)); }).toList());Copy the code

3. Select the status and icon from the isChecked & iconChecked drop-down list box

The dish wants to highlight the selected item in the drop-down box display, so add an iconChecked icon in the corresponding item position. When isChecked is true, the selected icon will be displayed; otherwise, it will not be displayed normally.

The drawing of item is loaded in _DropdownMenuItemButton, you can add property Settings through _DropdownMenuItemButton, small dishes in order to unified management, still through _DropdownRoute transit;

class _DropdownMenuItemButtonState<T> extends State<_DropdownMenuItemButton<T>> { @override Widget build(BuildContext context) { ... Widget child = FadeTransition( opacity: opacity, child: InkWell( autofocus: widget.itemIndex == widget.route.selectedIndex, child: Container( padding: widget.padding, child: Row(children: <Widget>[ Expanded(child: widget.route.items[widget.itemIndex]), widget.route.isChecked == true && widget.itemIndex == widget.route.selectedIndex ? (widget.route.iconChecked ?? Icon(Icons.check, size: _kIconCheckedSize)) : Container() ])), ... }} return ACEDropdownButton<String>(value: dropdownValue, backgroundColor: color.green. WithOpacity (0.8), menuRadius: Const BorderRadius. All (radius.circular (15.0)), isChecked: true, iconChecked: Icon(icon.tag_faces), onChanged: (String newValue) => setState(() => dropdownValue = newValue), items: <String>[' Beijing ', 'Tianjin ',' Hebei ', }.map<ACEDropdownMenuItem<String>>((String value) {return ACEDropdownMenuItem<String>(value: value, child: Text(value)); }).toList());Copy the code

4. Avoid shielding

The most important reason for choosing a custom ACEDropdownButton is that the DropdownButton attached to the Flutter will be displayed by default, and the desired effect of the dish is:

  1. If the screen space under the button is sufficient to display all the drop-down items, it will be displayed in the part under the button without blocking the button;
  2. If the height of the lower part of the button is insufficient to display the drop down items, check whether the screen space of the upper part of the button is sufficient to display all the drop down items. If so, display it without blocking the button.
  3. If the upper and lower parts of the screen space of the button are not enough to display all the drop-down items, a slideable items drop-down box is displayed with the top or bottom of the screen as the boundary.

The default menuTop is obtained by calculating the top of the button with the position of the selected item and the overall height of the drop-down box. Therefore, the display position takes precedence by selecting item to cover the button position, and then extends up and down;

Simplified calculation method, only judge the remaining screen space and button height difference can accommodate the height of the drop-down box; To determine the starting position of the menuTop, display in the top half of the button or the bottom half of the button;

final double menuHeight = math.min(maxMenuHeight, preferredMenuHeight);
if (bottomLimit - buttonRect.bottom < menuHeight) {
    menuTop = buttonRect.top - menuHeight;
} else {
    menuTop = buttonRect.bottom;
}
double menuBottom = menuTop + menuHeight;
Copy the code

The Animate drop-down box shows the animation

By default, the animation starts with the selected item and extends up and down;

The side dish changed the position of the drop-down box display, because the animation would be very abrupt, so the side dish changed the starting position of the animation, ingetSelectedItemOffsetSet toroute.getItemOffset(0)The first oneitemWho can; The side dish was also tested when displaying a drop down box in the top half of the button, by the enditemTo the firstitemAnimation, modified a lot of methods, the result of the effect is very strange, not in line with the daily animation display effect, so no matter where to show the drop-down box, are from the firstitemPosition to start showing animation;

getSelectedItemOffset: () => route.getItemOffset(0)),
Copy the code


ACEDropdownButton case source code


The understanding of the source code is not deep enough, only to the effect of the need to modify part of the source code, for all test scenarios may not be comprehensive enough; If there is any mistake, please give guidance!

Source: Young Monk Atze