Skip to content

Commit

Permalink
Allow null DropdownButton values (flutter#6971)
Browse files Browse the repository at this point in the history
  • Loading branch information
Hans Muller authored Nov 22, 2016
1 parent 23f269d commit 516ac57
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 26 deletions.
31 changes: 19 additions & 12 deletions packages/flutter/lib/src/material/drop_down.dart
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,9 @@ class _DropdownMenuPainter extends CustomPainter {

@override
void paint(Canvas canvas, Size size) {
final double selectedItemOffset = selectedIndex * _kMenuItemHeight + _kMenuVerticalPadding.top;
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
);

Expand Down Expand Up @@ -411,28 +412,31 @@ class DropdownButtonHideUnderline extends InheritedWidget {
class DropdownButton<T> 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,
this.iconSize: 24.0,
this.isDense: false,
}) : super(key: key) {
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.
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;

/// Called when the user selects an item.
Expand Down Expand Up @@ -470,22 +474,23 @@ class DropdownButton<T> extends StatefulWidget {
}

class _DropdownButtonState<T> extends State<DropdownButton<T>> {
int _selectedIndex;

@override
void initState() {
super.initState();
_updateSelectedIndex();
assert(_selectedIndex != null);
}

@override
void didUpdateConfig(DropdownButton<T> oldConfig) {
if (config.items[_selectedIndex].value != config.value)
_updateSelectedIndex();
_updateSelectedIndex();
}

int _selectedIndex;

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++) {
if (config.items[itemIndex].value == config.value) {
_selectedIndex = itemIndex;
Expand All @@ -502,7 +507,7 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> {
Navigator.push(context, new _DropdownRoute<T>(
items: config.items,
buttonRect: _kMenuHorizontalPadding.inflateRect(itemRect),
selectedIndex: _selectedIndex,
selectedIndex: _selectedIndex ?? 0,
elevation: config.elevation,
theme: Theme.of(context, shadowThemeOnly: true),
style: _textStyle,
Expand Down Expand Up @@ -533,10 +538,12 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.min,
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(
index: _selectedIndex,
alignment: FractionalOffset.centerLeft,
children: config.items
children: config.items,
),
new Icon(Icons.arrow_drop_down,
size: config.iconSize,
Expand Down
14 changes: 6 additions & 8 deletions packages/flutter/lib/src/rendering/stack.dart
Original file line number Diff line number Diff line change
Expand Up @@ -454,30 +454,28 @@ 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<RenderBox> children,
FractionalOffset alignment: FractionalOffset.topLeft,
int index: 0
}) : _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();
}
}

RenderBox _childAtIndex() {
assert(index != null);
RenderBox child = firstChild;
int i = 0;
while (child != null && i < index) {
Expand All @@ -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();
Expand All @@ -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;
Expand Down
9 changes: 5 additions & 4 deletions packages/flutter/lib/src/widgets/basic.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -1546,9 +1549,7 @@ class IndexedStack extends Stack {
FractionalOffset alignment: FractionalOffset.topLeft,
this.index: 0,
List<Widget> children: const <Widget>[],
}) : 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;
Expand Down
56 changes: 54 additions & 2 deletions packages/flutter/test/material/drop_down_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import 'dart:math' as math;
import 'package:flutter_test/flutter_test.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 }) {
final List<String> items = <String>['one', 'two', 'three', 'four'];
return new MaterialApp(
home: new Material(
child: new Center(
Expand All @@ -17,7 +18,7 @@ Widget buildFrame({ Key buttonKey, String value: 'two', ValueChanged<String> on
value: value,
onChanged: onChanged,
isDense: isDense,
items: items.map((String item) {
items: menuItems.map((String item) {
return new DropdownMenuItem<String>(
key: new ValueKey<String>(item),
value: item,
Expand Down Expand Up @@ -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'));
});

}
27 changes: 27 additions & 0 deletions packages/flutter/test/widgets/stack_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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: <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 {
await tester.pumpWidget(
new Center(
Expand Down

0 comments on commit 516ac57

Please sign in to comment.