Commit 516ac574 authored by Hans Muller's avatar Hans Muller Committed by GitHub

Allow null DropdownButton values (#6971)

parent 23f269d8
...@@ -52,8 +52,9 @@ class _DropdownMenuPainter extends CustomPainter { ...@@ -52,8 +52,9 @@ class _DropdownMenuPainter extends CustomPainter {
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
final double selectedItemOffset = selectedIndex * _kMenuItemHeight + _kMenuVerticalPadding.top;
final Tween<double> top = new Tween<double>( final Tween<double> top = new Tween<double>(
begin: (selectedIndex * _kMenuItemHeight + _kMenuVerticalPadding.top).clamp(0.0, size.height - _kMenuItemHeight), begin: selectedItemOffset.clamp(0.0, size.height - _kMenuItemHeight),
end: 0.0 end: 0.0
); );
...@@ -411,14 +412,14 @@ class DropdownButtonHideUnderline extends InheritedWidget { ...@@ -411,14 +412,14 @@ class DropdownButtonHideUnderline extends InheritedWidget {
class DropdownButton<T> extends StatefulWidget { class DropdownButton<T> extends StatefulWidget {
/// Creates a dropdown button. /// 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 /// The [elevation] and [iconSize] arguments must not be null (they both have
/// defaults, so do not need to be specified). /// defaults, so do not need to be specified).
DropdownButton({ DropdownButton({
Key key, Key key,
@required this.items, @required this.items,
@required this.value, this.value,
@required this.onChanged, @required this.onChanged,
this.elevation: 8, this.elevation: 8,
this.style, this.style,
...@@ -426,13 +427,16 @@ class DropdownButton<T> extends StatefulWidget { ...@@ -426,13 +427,16 @@ class DropdownButton<T> extends StatefulWidget {
this.isDense: false, this.isDense: false,
}) : super(key: key) { }) : super(key: key) {
assert(items != null); assert(items != null);
assert(items.where((DropdownMenuItem<T> item) => item.value == value).length == 1); assert(value == null ||
items.where((DropdownMenuItem<T> item) => item.value == value).length == 1);
} }
/// The list of possible items to select among. /// The list of possible items to select among.
final List<DropdownMenuItem<T>> items; final List<DropdownMenuItem<T>> 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; final T value;
/// Called when the user selects an item. /// Called when the user selects an item.
...@@ -470,22 +474,23 @@ class DropdownButton<T> extends StatefulWidget { ...@@ -470,22 +474,23 @@ class DropdownButton<T> extends StatefulWidget {
} }
class _DropdownButtonState<T> extends State<DropdownButton<T>> { class _DropdownButtonState<T> extends State<DropdownButton<T>> {
int _selectedIndex;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_updateSelectedIndex(); _updateSelectedIndex();
assert(_selectedIndex != null);
} }
@override @override
void didUpdateConfig(DropdownButton<T> oldConfig) { void didUpdateConfig(DropdownButton<T> oldConfig) {
if (config.items[_selectedIndex].value != config.value) _updateSelectedIndex();
_updateSelectedIndex();
} }
int _selectedIndex;
void _updateSelectedIndex() { void _updateSelectedIndex() {
assert(config.value == null ||
config.items.where((DropdownMenuItem<T> item) => item.value == config.value).length == 1);
_selectedIndex = null;
for (int itemIndex = 0; itemIndex < config.items.length; itemIndex++) { for (int itemIndex = 0; itemIndex < config.items.length; itemIndex++) {
if (config.items[itemIndex].value == config.value) { if (config.items[itemIndex].value == config.value) {
_selectedIndex = itemIndex; _selectedIndex = itemIndex;
...@@ -502,7 +507,7 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> { ...@@ -502,7 +507,7 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> {
Navigator.push(context, new _DropdownRoute<T>( Navigator.push(context, new _DropdownRoute<T>(
items: config.items, items: config.items,
buttonRect: _kMenuHorizontalPadding.inflateRect(itemRect), buttonRect: _kMenuHorizontalPadding.inflateRect(itemRect),
selectedIndex: _selectedIndex, selectedIndex: _selectedIndex ?? 0,
elevation: config.elevation, elevation: config.elevation,
theme: Theme.of(context, shadowThemeOnly: true), theme: Theme.of(context, shadowThemeOnly: true),
style: _textStyle, style: _textStyle,
...@@ -533,10 +538,12 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> { ...@@ -533,10 +538,12 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
// The button's size is defined by its largest menu item. If value is
// null then an item does not appear.
new IndexedStack( new IndexedStack(
index: _selectedIndex, index: _selectedIndex,
alignment: FractionalOffset.centerLeft, alignment: FractionalOffset.centerLeft,
children: config.items children: config.items,
), ),
new Icon(Icons.arrow_drop_down, new Icon(Icons.arrow_drop_down,
size: config.iconSize, size: config.iconSize,
......
...@@ -454,7 +454,7 @@ class RenderStack extends RenderBox ...@@ -454,7 +454,7 @@ class RenderStack extends RenderBox
class RenderIndexedStack extends RenderStack { class RenderIndexedStack extends RenderStack {
/// Creates a stack render object that paints a single child. /// 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({ RenderIndexedStack({
List<RenderBox> children, List<RenderBox> children,
FractionalOffset alignment: FractionalOffset.topLeft, FractionalOffset alignment: FractionalOffset.topLeft,
...@@ -462,15 +462,12 @@ class RenderIndexedStack extends RenderStack { ...@@ -462,15 +462,12 @@ class RenderIndexedStack extends RenderStack {
}) : _index = index, super( }) : _index = index, super(
children: children, children: children,
alignment: alignment 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 get index => _index;
int _index; int _index;
set index (int value) { set index (int value) {
assert(value != null);
if (_index != value) { if (_index != value) {
_index = value; _index = value;
markNeedsLayout(); markNeedsLayout();
...@@ -478,6 +475,7 @@ class RenderIndexedStack extends RenderStack { ...@@ -478,6 +475,7 @@ class RenderIndexedStack extends RenderStack {
} }
RenderBox _childAtIndex() { RenderBox _childAtIndex() {
assert(index != null);
RenderBox child = firstChild; RenderBox child = firstChild;
int i = 0; int i = 0;
while (child != null && i < index) { while (child != null && i < index) {
...@@ -492,7 +490,7 @@ class RenderIndexedStack extends RenderStack { ...@@ -492,7 +490,7 @@ class RenderIndexedStack extends RenderStack {
@override @override
bool hitTestChildren(HitTestResult result, { Point position }) { bool hitTestChildren(HitTestResult result, { Point position }) {
if (firstChild == null) if (firstChild == null || index == null)
return false; return false;
assert(position != null); assert(position != null);
RenderBox child = _childAtIndex(); RenderBox child = _childAtIndex();
...@@ -504,7 +502,7 @@ class RenderIndexedStack extends RenderStack { ...@@ -504,7 +502,7 @@ class RenderIndexedStack extends RenderStack {
@override @override
void paintStack(PaintingContext context, Offset offset) { void paintStack(PaintingContext context, Offset offset) {
if (firstChild == null) if (firstChild == null || index == null)
return; return;
RenderBox child = _childAtIndex(); RenderBox child = _childAtIndex();
final StackParentData childParentData = child.parentData; final StackParentData childParentData = child.parentData;
......
...@@ -1534,7 +1534,10 @@ class Stack extends MultiChildRenderObjectWidget { ...@@ -1534,7 +1534,10 @@ class Stack extends MultiChildRenderObjectWidget {
/// A [Stack] that shows a single child from a list of children. /// 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]. /// For more details, see [Stack].
class IndexedStack extends Stack { class IndexedStack extends Stack {
...@@ -1546,9 +1549,7 @@ class IndexedStack extends Stack { ...@@ -1546,9 +1549,7 @@ class IndexedStack extends Stack {
FractionalOffset alignment: FractionalOffset.topLeft, FractionalOffset alignment: FractionalOffset.topLeft,
this.index: 0, this.index: 0,
List<Widget> children: const <Widget>[], List<Widget> children: const <Widget>[],
}) : super(key: key, alignment: alignment, children: children) { }) : super(key: key, alignment: alignment, children: children);
assert(index != null);
}
/// The index of the child to show. /// The index of the child to show.
final int index; final int index;
......
...@@ -7,8 +7,9 @@ import 'dart:math' as math; ...@@ -7,8 +7,9 @@ import 'dart:math' as math;
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
final List<String> menuItems = <String>['one', 'two', 'three', 'four'];
Widget buildFrame({ Key buttonKey, String value: 'two', ValueChanged<String> onChanged, bool isDense: false }) { Widget buildFrame({ Key buttonKey, String value: 'two', ValueChanged<String> onChanged, bool isDense: false }) {
final List<String> items = <String>['one', 'two', 'three', 'four'];
return new MaterialApp( return new MaterialApp(
home: new Material( home: new Material(
child: new Center( child: new Center(
...@@ -17,7 +18,7 @@ Widget buildFrame({ Key buttonKey, String value: 'two', ValueChanged<String> on ...@@ -17,7 +18,7 @@ Widget buildFrame({ Key buttonKey, String value: 'two', ValueChanged<String> on
value: value, value: value,
onChanged: onChanged, onChanged: onChanged,
isDense: isDense, isDense: isDense,
items: items.map((String item) { items: menuItems.map((String item) {
return new DropdownMenuItem<String>( return new DropdownMenuItem<String>(
key: new ValueKey<String>(item), key: new ValueKey<String>(item),
value: item, value: item,
...@@ -265,4 +266,55 @@ void main() { ...@@ -265,4 +266,55 @@ void main() {
// should have the same size and location. // should have the same size and location.
checkSelectedItemTextGeometry(tester, 'two'); 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'));
});
} }
...@@ -262,6 +262,33 @@ void main() { ...@@ -262,6 +262,33 @@ void main() {
expect(renderBox.size.height, equals(12.0)); 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: <Widget>[
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 { testWidgets('Stack clip test', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
new Center( new Center(
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment