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 {
@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
);
......@@ -411,14 +412,14 @@ 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,
......@@ -426,13 +427,16 @@ class DropdownButton<T> extends StatefulWidget {
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.
......@@ -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;
......@@ -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,
......@@ -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,
......
......@@ -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<RenderBox> 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;
......
......@@ -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<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;
......
......@@ -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(
......@@ -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,
......@@ -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'));
});
}
......@@ -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(
......
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