mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Allow null DropdownButton values (#6971)
This commit is contained in:
parent
23f269d854
commit
516ac574c0
@ -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(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user