From 516ac574c014dcb27da54c5eba2c75845a4e4a9e Mon Sep 17 00:00:00 2001 From: Hans Muller Date: Mon, 21 Nov 2016 16:36:19 -0800 Subject: [PATCH] Allow null DropdownButton values (#6971) --- .../flutter/lib/src/material/drop_down.dart | 31 ++++++---- packages/flutter/lib/src/rendering/stack.dart | 14 ++--- packages/flutter/lib/src/widgets/basic.dart | 9 +-- .../flutter/test/material/drop_down_test.dart | 56 ++++++++++++++++++- packages/flutter/test/widgets/stack_test.dart | 27 +++++++++ 5 files changed, 111 insertions(+), 26 deletions(-) diff --git a/packages/flutter/lib/src/material/drop_down.dart b/packages/flutter/lib/src/material/drop_down.dart index 360d0fbd51772..6d8cde1578291 100644 --- a/packages/flutter/lib/src/material/drop_down.dart +++ b/packages/flutter/lib/src/material/drop_down.dart @@ -52,8 +52,9 @@ class _DropdownMenuPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { + final double selectedItemOffset = selectedIndex * _kMenuItemHeight + _kMenuVerticalPadding.top; final Tween top = new Tween( - begin: (selectedIndex * _kMenuItemHeight + _kMenuVerticalPadding.top).clamp(0.0, size.height - _kMenuItemHeight), + begin: selectedItemOffset.clamp(0.0, size.height - _kMenuItemHeight), end: 0.0 ); @@ -411,14 +412,14 @@ class DropdownButtonHideUnderline extends InheritedWidget { class DropdownButton extends StatefulWidget { /// Creates a dropdown button. /// - /// The [items] must have distinct values and [value] must be among them. + /// The [items] must have distinct values and if [value] isn't null it must be among them. /// /// The [elevation] and [iconSize] arguments must not be null (they both have /// defaults, so do not need to be specified). DropdownButton({ Key key, @required this.items, - @required this.value, + this.value, @required this.onChanged, this.elevation: 8, this.style, @@ -426,13 +427,16 @@ class DropdownButton extends StatefulWidget { this.isDense: false, }) : super(key: key) { assert(items != null); - assert(items.where((DropdownMenuItem item) => item.value == value).length == 1); + assert(value == null || + items.where((DropdownMenuItem item) => item.value == value).length == 1); } /// The list of possible items to select among. final List> items; - /// The currently selected item. + /// The currently selected item, or null if no item has been selected. If + /// value is null then the menu is popped up as if the first item was + /// selected. final T value; /// Called when the user selects an item. @@ -470,22 +474,23 @@ class DropdownButton extends StatefulWidget { } class _DropdownButtonState extends State> { + int _selectedIndex; + @override void initState() { super.initState(); _updateSelectedIndex(); - assert(_selectedIndex != null); } @override void didUpdateConfig(DropdownButton oldConfig) { - if (config.items[_selectedIndex].value != config.value) - _updateSelectedIndex(); + _updateSelectedIndex(); } - int _selectedIndex; - void _updateSelectedIndex() { + assert(config.value == null || + config.items.where((DropdownMenuItem item) => item.value == config.value).length == 1); + _selectedIndex = null; for (int itemIndex = 0; itemIndex < config.items.length; itemIndex++) { if (config.items[itemIndex].value == config.value) { _selectedIndex = itemIndex; @@ -502,7 +507,7 @@ class _DropdownButtonState extends State> { Navigator.push(context, new _DropdownRoute( items: config.items, buttonRect: _kMenuHorizontalPadding.inflateRect(itemRect), - selectedIndex: _selectedIndex, + selectedIndex: _selectedIndex ?? 0, elevation: config.elevation, theme: Theme.of(context, shadowThemeOnly: true), style: _textStyle, @@ -533,10 +538,12 @@ class _DropdownButtonState extends State> { mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisSize: MainAxisSize.min, children: [ + // The button's size is defined by its largest menu item. If value is + // null then an item does not appear. new IndexedStack( index: _selectedIndex, alignment: FractionalOffset.centerLeft, - children: config.items + children: config.items, ), new Icon(Icons.arrow_drop_down, size: config.iconSize, diff --git a/packages/flutter/lib/src/rendering/stack.dart b/packages/flutter/lib/src/rendering/stack.dart index 145f939991c2d..ed6c1357f3551 100644 --- a/packages/flutter/lib/src/rendering/stack.dart +++ b/packages/flutter/lib/src/rendering/stack.dart @@ -454,7 +454,7 @@ class RenderStack extends RenderBox class RenderIndexedStack extends RenderStack { /// Creates a stack render object that paints a single child. /// - /// The [index] argument must not be null. + /// If the [index] parameter is null, nothing is displayed. RenderIndexedStack({ List children, FractionalOffset alignment: FractionalOffset.topLeft, @@ -462,15 +462,12 @@ class RenderIndexedStack extends RenderStack { }) : _index = index, super( children: children, alignment: alignment - ) { - assert(index != null); - } + ); - /// The index of the child to show. + /// The index of the child to show, null if nothing is to be displayed. int get index => _index; int _index; set index (int value) { - assert(value != null); if (_index != value) { _index = value; markNeedsLayout(); @@ -478,6 +475,7 @@ class RenderIndexedStack extends RenderStack { } RenderBox _childAtIndex() { + assert(index != null); RenderBox child = firstChild; int i = 0; while (child != null && i < index) { @@ -492,7 +490,7 @@ class RenderIndexedStack extends RenderStack { @override bool hitTestChildren(HitTestResult result, { Point position }) { - if (firstChild == null) + if (firstChild == null || index == null) return false; assert(position != null); RenderBox child = _childAtIndex(); @@ -504,7 +502,7 @@ class RenderIndexedStack extends RenderStack { @override void paintStack(PaintingContext context, Offset offset) { - if (firstChild == null) + if (firstChild == null || index == null) return; RenderBox child = _childAtIndex(); final StackParentData childParentData = child.parentData; diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart index 7e59205ee042f..65fa55642dbda 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -1534,7 +1534,10 @@ class Stack extends MultiChildRenderObjectWidget { /// A [Stack] that shows a single child from a list of children. /// -/// The displayed child is the one with the given [index]. +/// The displayed child is the one with the given [index]. The stack is +/// always as big as the largest child. +/// +/// If value is null, then nothing is displayed. /// /// For more details, see [Stack]. class IndexedStack extends Stack { @@ -1546,9 +1549,7 @@ class IndexedStack extends Stack { FractionalOffset alignment: FractionalOffset.topLeft, this.index: 0, List children: const [], - }) : super(key: key, alignment: alignment, children: children) { - assert(index != null); - } + }) : super(key: key, alignment: alignment, children: children); /// The index of the child to show. final int index; diff --git a/packages/flutter/test/material/drop_down_test.dart b/packages/flutter/test/material/drop_down_test.dart index db0168679467e..21246cc76204a 100644 --- a/packages/flutter/test/material/drop_down_test.dart +++ b/packages/flutter/test/material/drop_down_test.dart @@ -7,8 +7,9 @@ import 'dart:math' as math; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/material.dart'; +final List menuItems = ['one', 'two', 'three', 'four']; + Widget buildFrame({ Key buttonKey, String value: 'two', ValueChanged onChanged, bool isDense: false }) { - final List items = ['one', 'two', 'three', 'four']; return new MaterialApp( home: new Material( child: new Center( @@ -17,7 +18,7 @@ Widget buildFrame({ Key buttonKey, String value: 'two', ValueChanged on value: value, onChanged: onChanged, isDense: isDense, - items: items.map((String item) { + items: menuItems.map((String item) { return new DropdownMenuItem( key: new ValueKey(item), value: item, @@ -265,4 +266,55 @@ void main() { // should have the same size and location. checkSelectedItemTextGeometry(tester, 'two'); }); + + testWidgets('Size of DropdownButton with null value', (WidgetTester tester) async { + Key buttonKey = new UniqueKey(); + String value; + + Widget build() => buildFrame(buttonKey: buttonKey, value: value); + + await tester.pumpWidget(build()); + RenderBox buttonBoxNullValue = tester.renderObject(find.byKey(buttonKey)); + assert(buttonBoxNullValue.attached); + + + value = 'three'; + await tester.pumpWidget(build()); + RenderBox buttonBox = tester.renderObject(find.byKey(buttonKey)); + assert(buttonBox.attached); + + // A DropDown button with a null value should be the same size as a + // one with a non-null value. + expect(buttonBox.localToGlobal(Point.origin), equals(buttonBoxNullValue.localToGlobal(Point.origin))); + expect(buttonBox.size, equals(buttonBoxNullValue.size)); + }); + + testWidgets('Layout of a DropdownButton with null value', (WidgetTester tester) async { + Key buttonKey = new UniqueKey(); + String value; + + void onChanged(String newValue) { + value = newValue; + } + + Widget build() => buildFrame(buttonKey: buttonKey, value: value, onChanged: onChanged); + + await tester.pumpWidget(build()); + RenderBox buttonBox = tester.renderObject(find.byKey(buttonKey)); + assert(buttonBox.attached); + + // Show the menu. + await tester.tap(find.byKey(buttonKey)); + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); // finish the menu animation + + // Tap on item 'one', which must appear over the button. + await tester.tap(find.byKey(buttonKey)); + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); // finish the menu animation + + await tester.pumpWidget(build()); + expect(value, equals('one')); + }); + } diff --git a/packages/flutter/test/widgets/stack_test.dart b/packages/flutter/test/widgets/stack_test.dart index 6f4861c9f6454..9a9c244e699f7 100644 --- a/packages/flutter/test/widgets/stack_test.dart +++ b/packages/flutter/test/widgets/stack_test.dart @@ -262,6 +262,33 @@ void main() { expect(renderBox.size.height, equals(12.0)); }); + testWidgets('IndexedStack with null index', (WidgetTester tester) async { + bool tapped; + + await tester.pumpWidget( + new Center( + child: new IndexedStack( + index: null, + children: [ + new GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { print("HELLO"); tapped = true; }, + child: new SizedBox( + width: 200.0, + height: 200.0, + ), + ), + ], + ), + ), + ); + + await tester.tap(find.byType(IndexedStack)); + RenderBox box = tester.renderObject(find.byType(IndexedStack)); + expect(box.size, equals(const Size(200.0, 200.0))); + expect(tapped, isNull); + }); + testWidgets('Stack clip test', (WidgetTester tester) async { await tester.pumpWidget( new Center(