From cf7e7e4529d3f4f6ee53b68cab1db6b000187ca3 Mon Sep 17 00:00:00 2001 From: Shi-Hao Hong Date: Sat, 14 Sep 2019 07:56:05 -0700 Subject: [PATCH] Implement DropdownButton.selectedItemBuilder (#40461) * Implement DropdownButton.selectedItemBuilder --- .../flutter/lib/src/material/dropdown.dart | 63 ++++++++++++++++++- .../flutter/test/material/dropdown_test.dart | 46 +++++++++++++- 2 files changed, 107 insertions(+), 2 deletions(-) diff --git a/packages/flutter/lib/src/material/dropdown.dart b/packages/flutter/lib/src/material/dropdown.dart index b8e54bd53a..2d755cb908 100644 --- a/packages/flutter/lib/src/material/dropdown.dart +++ b/packages/flutter/lib/src/material/dropdown.dart @@ -28,6 +28,8 @@ const EdgeInsets _kUnalignedButtonPadding = EdgeInsets.zero; const EdgeInsets _kAlignedMenuMargin = EdgeInsets.zero; const EdgeInsetsGeometry _kUnalignedMenuMargin = EdgeInsetsDirectional.only(start: 16.0, end: 24.0); +typedef DropdownButtonBuilder = List Function(BuildContext context); + class _DropdownMenuPainter extends CustomPainter { _DropdownMenuPainter({ this.color, @@ -604,6 +606,7 @@ class DropdownButton extends StatefulWidget { DropdownButton({ Key key, @required this.items, + this.selectedItemBuilder, this.value, this.hint, this.disabledHint, @@ -655,6 +658,50 @@ class DropdownButton extends StatefulWidget { /// {@endtemplate} final ValueChanged onChanged; + /// A builder to customize the dropdown buttons corresponding to the + /// [DropdownMenuItem]s in [items]. + /// + /// When a [DropdownMenuItem] is selected, the widget that will be displayed + /// from the list corresponds to the [DropdownMenuItem] of the same index + /// in [items]. + /// + /// {@tool snippet --template=stateful_widget_scaffold} + /// + /// This sample shows a `DropdownButton` with a button with [Text] that + /// corresponds to but is unique from [DropdownMenuItem]. + /// + /// ```dart + /// final List items = ['1','2','3']; + /// String selectedItem = '1'; + /// + /// @override + /// Widget build(BuildContext context) { + /// return Padding( + /// padding: const EdgeInsets.symmetric(horizontal: 12.0), + /// child: DropdownButton( + /// value: selectedItem, + /// onChanged: (String string) => setState(() => selectedItem = string), + /// selectedItemBuilder: (BuildContext context) { + /// return items.map((String item) { + /// return Text(item); + /// }).toList(); + /// }, + /// items: items.map((String item) { + /// return DropdownMenuItem( + /// child: Text('Log $item'), + /// value: item, + /// ); + /// }).toList(), + /// ), + /// ); + /// } + /// ``` + /// {@end-tool} + /// + /// If this callback is null, the [DropdownMenuItem] from [items] + /// that matches [value] will be displayed. + final DropdownButtonBuilder selectedItemBuilder; + /// 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, @@ -849,7 +896,21 @@ class _DropdownButtonState extends State> with WidgetsBindi // 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) : []; + List items; + if (_enabled) { + items = widget.selectedItemBuilder == null + ? List.from(widget.items) + : widget.selectedItemBuilder(context).map((Widget item) { + return Container( + height: _kMenuItemHeight, + alignment: AlignmentDirectional.centerStart, + child: item, + ); + }).toList(); + } else { + items = []; + } + int hintIndex; if (widget.hint != null || (!_enabled && widget.disabledHint != null)) { final Widget emplacedHint = _enabled diff --git a/packages/flutter/test/material/dropdown_test.dart b/packages/flutter/test/material/dropdown_test.dart index 3048709207..7e823ae4dc 100644 --- a/packages/flutter/test/material/dropdown_test.dart +++ b/packages/flutter/test/material/dropdown_test.dart @@ -1317,7 +1317,51 @@ void main() { expect(tester.widget(decoratedBox).decoration, defaultDecoration); }); - testWidgets('Dropdown form field with autovalidation test', (WidgetTester tester) async { + testWidgets('DropdownButton selectedItemBuilder builds custom buttons', (WidgetTester tester) async { + const List items = [ + 'One', + 'Two', + 'Three', + ]; + String selectedItem = items[0]; + + await tester.pumpWidget( + StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return MaterialApp( + home: Scaffold( + body: DropdownButton( + value: selectedItem, + onChanged: (String string) => setState(() => selectedItem = string), + selectedItemBuilder: (BuildContext context) { + int index = 0; + return items.map((String string) { + index += 1; + return Text('$string as an Arabic numeral: $index'); + }).toList(); + }, + items: items.map((String string) { + return DropdownMenuItem( + child: Text(string), + value: string, + ); + }).toList(), + ), + ), + ); + }, + ), + ); + + expect(find.text('One as an Arabic numeral: 1'), findsOneWidget); + await tester.tap(find.text('One as an Arabic numeral: 1')); + await tester.pumpAndSettle(); + await tester.tap(find.text('Two')); + await tester.pumpAndSettle(); + expect(find.text('Two as an Arabic numeral: 2'), findsOneWidget); + }); + + testWidgets('Dropdown form field with autovalidation test', (WidgetTester tester) async { String value = 'one'; int _validateCalled = 0;