Segmented control fixes (#20202)
Segment width now determined by width of widest child + children widgets now centered within segments
diff --git a/bin/internal/goldens.version b/bin/internal/goldens.version
index e717d09..59df7b3 100644
--- a/bin/internal/goldens.version
+++ b/bin/internal/goldens.version
@@ -1,2 +1 @@
-64b7a3a7aef2fea9c7529d4834bf9eb3d85602d8
-
+46cf554baf4840c326bbceaa51b11534069bb557
\ No newline at end of file
diff --git a/packages/flutter/lib/src/cupertino/segmented_control.dart b/packages/flutter/lib/src/cupertino/segmented_control.dart
index b6c7866..99071a6 100644
--- a/packages/flutter/lib/src/cupertino/segmented_control.dart
+++ b/packages/flutter/lib/src/cupertino/segmented_control.dart
@@ -46,10 +46,11 @@
/// The [children] will be displayed in the order of the keys in the [Map].
/// The height of the segmented control is determined by the height of the
/// tallest widget provided as a value in the [Map] of [children].
-/// The width of the segmented control is determined by the horizontal
-/// constraints on its parent. The available horizontal space is divided by
-/// the number of provided [children] to determine the width of each widget.
-/// The selection area for each of the widgets in the [Map] of
+/// The width of each child in the segmented control will be equal to the width
+/// of widest child, unless the combined width of the children is wider than
+/// the available horizontal space. In this case, the available horizontal space
+/// is divided by the number of provided [children] to determine the width of
+/// each widget. The selection area for each of the widgets in the [Map] of
/// [children] will then be expanded to fill the calculated space, so each
/// widget will appear to have the same dimensions.
///
@@ -75,10 +76,10 @@
/// in the [onValueChanged] callback when a new value from the [children] map
/// is selected.
///
- /// The [groupValue] must be one of the keys in the [children] map.
/// The [groupValue] is the currently selected value for the segmented control.
/// If no [groupValue] is provided, or the [groupValue] is null, no widget will
- /// appear as selected.
+ /// appear as selected. The [groupValue] must be either null or one of the keys
+ /// in the [children] map.
SegmentedControl({
Key key,
@required this.children,
@@ -91,7 +92,8 @@
}) : assert(children != null),
assert(children.length >= 2),
assert(onValueChanged != null),
- assert(groupValue == null || children.keys.any((T child) => child == groupValue)),
+ assert(groupValue == null || children.keys.any((T child) => child == groupValue),
+ 'The groupValue must be either null or one of the keys in the children map.'),
assert(unselectedColor != null),
assert(selectedColor != null),
assert(borderColor != null),
@@ -189,7 +191,7 @@
/// This attribute must not be null.
///
/// If this attribute is unspecified, this color will default to
- /// 'Color(0x33007AFF)', a light, partially-transparent blue color.
+ /// `Color(0x33007AFF)`, a light, partially-transparent blue color.
final Color pressedColor;
@override
@@ -346,7 +348,10 @@
color: getTextColor(index, currentKey),
);
- Widget child = widget.children[currentKey];
+ Widget child = new Center(
+ child: widget.children[currentKey],
+ );
+
child = new GestureDetector(
onTapDown: (TapDownDetails event) {
_onTapDown(currentKey);
@@ -599,15 +604,11 @@
void performLayout() {
double maxHeight = _kMinSegmentedControlHeight;
- double childWidth;
- if (constraints.maxWidth.isFinite) {
- childWidth = constraints.maxWidth / childCount;
- } else {
- childWidth = constraints.minWidth / childCount;
- for (RenderBox child in getChildrenAsList()) {
- childWidth = math.max(childWidth, child.getMaxIntrinsicWidth(double.infinity));
- }
+ double childWidth = constraints.minWidth / childCount;
+ for (RenderBox child in getChildrenAsList()) {
+ childWidth = math.max(childWidth, child.getMaxIntrinsicWidth(double.infinity));
}
+ childWidth = math.min(childWidth, constraints.maxWidth / childCount);
RenderBox child = firstChild;
while (child != null) {
diff --git a/packages/flutter/test/cupertino/segmented_control_test.dart b/packages/flutter/test/cupertino/segmented_control_test.dart
index 40c9f23..f4257e9 100644
--- a/packages/flutter/test/cupertino/segmented_control_test.dart
+++ b/packages/flutter/test/cupertino/segmented_control_test.dart
@@ -218,8 +218,6 @@
),
);
- await tester.pumpAndSettle();
-
DefaultTextStyle textStyle = tester.widget(find.widgetWithText(DefaultTextStyle, 'Child 1'));
IconTheme iconTheme = tester.widget(find.widgetWithIcon(IconTheme, const IconData(1)));
@@ -238,63 +236,88 @@
testWidgets('SegmentedControl is correct when user provides custom colors',
(WidgetTester tester) async {
- final Map<int, Widget> children = <int, Widget>{};
- children[0] = const Text('Child 1');
- children[1] = const Icon(IconData(1));
+ final Map<int, Widget> children = <int, Widget>{};
+ children[0] = const Text('Child 1');
+ children[1] = const Icon(IconData(1));
- int sharedValue = 0;
+ int sharedValue = 0;
- await tester.pumpWidget(
- new StatefulBuilder(
- builder: (BuildContext context, StateSetter setState) {
- return boilerplate(
- child: new SegmentedControl<int>(
- children: children,
- onValueChanged: (int newValue) {
- setState(() {
- sharedValue = newValue;
- });
- },
- groupValue: sharedValue,
- unselectedColor: CupertinoColors.lightBackgroundGray,
- selectedColor: CupertinoColors.activeGreen,
- borderColor: CupertinoColors.black,
- pressedColor: const Color(0x638CFC7B),
- ),
- );
- },
+ await tester.pumpWidget(
+ new StatefulBuilder(
+ builder: (BuildContext context, StateSetter setState) {
+ return boilerplate(
+ child: new SegmentedControl<int>(
+ children: children,
+ onValueChanged: (int newValue) {
+ setState(() {
+ sharedValue = newValue;
+ });
+ },
+ groupValue: sharedValue,
+ unselectedColor: CupertinoColors.lightBackgroundGray,
+ selectedColor: CupertinoColors.activeGreen,
+ borderColor: CupertinoColors.black,
+ pressedColor: const Color(0x638CFC7B),
+ ),
+ );
+ },
+ ),
+ );
+
+ DefaultTextStyle textStyle = tester.widget(find.widgetWithText(DefaultTextStyle, 'Child 1'));
+ IconTheme iconTheme = tester.widget(find.widgetWithIcon(IconTheme, const IconData(1)));
+
+ expect(getRenderSegmentedControl(tester).borderColor, CupertinoColors.black);
+ expect(textStyle.style.color, CupertinoColors.lightBackgroundGray);
+ expect(iconTheme.data.color, CupertinoColors.activeGreen);
+ expect(getBackgroundColor(tester, 0), CupertinoColors.activeGreen);
+ expect(getBackgroundColor(tester, 1), CupertinoColors.lightBackgroundGray);
+
+ await tester.tap(find.widgetWithIcon(IconTheme, const IconData(1)));
+ await tester.pumpAndSettle();
+
+ textStyle = tester.widget(find.widgetWithText(DefaultTextStyle, 'Child 1'));
+ iconTheme = tester.widget(find.widgetWithIcon(IconTheme, const IconData(1)));
+
+ expect(textStyle.style.color, CupertinoColors.activeGreen);
+ expect(iconTheme.data.color, CupertinoColors.lightBackgroundGray);
+ expect(getBackgroundColor(tester, 0), CupertinoColors.lightBackgroundGray);
+ expect(getBackgroundColor(tester, 1), CupertinoColors.activeGreen);
+
+ final Offset center = tester.getCenter(find.text('Child 1'));
+ await tester.startGesture(center);
+ await tester.pumpAndSettle();
+
+ expect(getBackgroundColor(tester, 0), const Color(0x638CFC7B));
+ expect(getBackgroundColor(tester, 1), CupertinoColors.activeGreen);
+ });
+
+ testWidgets('Widgets are centered within segments', (WidgetTester tester) async {
+ final Map<int, Widget> children = <int, Widget>{};
+ children[0] = const Text('Child 1');
+ children[1] = const Text('Child 2');
+
+ await tester.pumpWidget(
+ new Directionality(
+ textDirection: TextDirection.ltr,
+ child: new Align(
+ alignment: Alignment.topLeft,
+ child: new SizedBox(
+ width: 200.0,
+ height: 200.0,
+ child: new SegmentedControl<int>(
+ children: children,
+ onValueChanged: (int newValue) {},
+ ),
),
- );
+ ),
+ ),
+ );
- await tester.pumpAndSettle();
-
- DefaultTextStyle textStyle = tester.widget(find.widgetWithText(DefaultTextStyle, 'Child 1'));
- IconTheme iconTheme = tester.widget(find.widgetWithIcon(IconTheme, const IconData(1)));
-
- expect(getRenderSegmentedControl(tester).borderColor, CupertinoColors.black);
- expect(textStyle.style.color, CupertinoColors.lightBackgroundGray);
- expect(iconTheme.data.color, CupertinoColors.activeGreen);
- expect(getBackgroundColor(tester, 0), CupertinoColors.activeGreen);
- expect(getBackgroundColor(tester, 1), CupertinoColors.lightBackgroundGray);
-
- await tester.tap(find.widgetWithIcon(IconTheme, const IconData(1)));
- await tester.pumpAndSettle();
-
- textStyle = tester.widget(find.widgetWithText(DefaultTextStyle, 'Child 1'));
- iconTheme = tester.widget(find.widgetWithIcon(IconTheme, const IconData(1)));
-
- expect(textStyle.style.color, CupertinoColors.activeGreen);
- expect(iconTheme.data.color, CupertinoColors.lightBackgroundGray);
- expect(getBackgroundColor(tester, 0), CupertinoColors.lightBackgroundGray);
- expect(getBackgroundColor(tester, 1), CupertinoColors.activeGreen);
-
- final Offset center = tester.getCenter(find.text('Child 1'));
- await tester.startGesture(center);
- await tester.pumpAndSettle();
-
- expect(getBackgroundColor(tester, 0), const Color(0x638CFC7B));
- expect(getBackgroundColor(tester, 1), CupertinoColors.activeGreen);
- });
+ // Widgets are centered taking into account 16px of horizontal padding
+ expect(tester.getCenter(find.text('Child 1')), const Offset(58.0, 100.0));
+ expect(tester.getCenter(find.text('Child 2')), const Offset(142.0, 100.0));
+ });
testWidgets('Tap calls onValueChanged', (WidgetTester tester) async {
final Map<int, Widget> children = <int, Widget>{};
@@ -510,16 +533,21 @@
final RenderBox buttonBox = tester.renderObject(
find.byKey(const ValueKey<String>('Segmented Control')));
- // Default height of Placeholder is 400.0px, which is greater than heights
- // of other child widgets.
expect(buttonBox.size.height, 400.0);
});
- testWidgets('Width of each child widget is the same', (WidgetTester tester) async {
+ testWidgets('Width of each segmented control segment is determined by widest widget',
+ (WidgetTester tester) async {
final Map<int, Widget> children = <int, Widget>{};
- children[0] = new Container();
- children[1] = const Placeholder();
- children[2] = new Container();
+ children[0] = new Container(
+ constraints: const BoxConstraints.tightFor(width: 50.0),
+ );
+ children[1] = new Container(
+ constraints: const BoxConstraints.tightFor(width: 100.0),
+ );
+ children[2] = new Container(
+ constraints: const BoxConstraints.tightFor(width: 200.0),
+ );
await tester.pumpWidget(
new StatefulBuilder(
@@ -542,6 +570,8 @@
// to each child equally.
final double childWidth = (segmentedControl.size.width - 32.0) / 3;
+ expect(childWidth, 200.0);
+
expect(childWidth,
getRenderSegmentedControl(tester).getChildrenAsList()[0].parentData.surroundingRect.width);
expect(childWidth,
@@ -748,8 +778,8 @@
testWidgets('Non-centered taps work on smaller widgets', (WidgetTester tester) async {
final Map<int, Widget> children = <int, Widget>{};
- children[0] = const Text('A');
- children[1] = const Text('B');
+ children[0] = const Text('Child 1');
+ children[1] = const Text('Child 2');
int sharedValue = 1;
@@ -775,10 +805,15 @@
expect(sharedValue, 1);
final double childWidth = getRenderSegmentedControl(tester).firstChild.size.width;
- final Offset centerOfSegmentedControl = tester.getCenter(find.text('A'));
+ final Offset centerOfSegmentedControl = tester.getCenter(find.text('Child 1'));
// Tap just inside segment bounds
- await tester.tapAt(new Offset(childWidth - 10.0, centerOfSegmentedControl.dy));
+ await tester.tapAt(
+ new Offset(
+ centerOfSegmentedControl.dx + (childWidth / 2) - 10.0,
+ centerOfSegmentedControl.dy,
+ ),
+ );
expect(sharedValue, 0);
});
@@ -1257,11 +1292,14 @@
child: new StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return boilerplate(
- child: new SegmentedControl<int>(
- key: const ValueKey<String>('Segmented Control'),
- children: children,
- onValueChanged: (int newValue) {},
- groupValue: currentValue,
+ child: new SizedBox(
+ width: 800.0,
+ child: new SegmentedControl<int>(
+ key: const ValueKey<String>('Segmented Control'),
+ children: children,
+ onValueChanged: (int newValue) {},
+ groupValue: currentValue,
+ ),
),
);
},
@@ -1273,7 +1311,7 @@
find.byType(RepaintBoundary),
matchesGoldenFile('segmented_control_test.0.0.png'),
);
- }, skip: !Platform.isLinux);
+ }, skip: !Platform.isMacOS);
testWidgets('Golden Test Pressed State', (WidgetTester tester) async {
final Map<int, Widget> children = <int, Widget>{};
@@ -1288,11 +1326,14 @@
child: new StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return boilerplate(
- child: new SegmentedControl<int>(
- key: const ValueKey<String>('Segmented Control'),
- children: children,
- onValueChanged: (int newValue) {},
- groupValue: currentValue,
+ child: new SizedBox(
+ width: 800.0,
+ child: new SegmentedControl<int>(
+ key: const ValueKey<String>('Segmented Control'),
+ children: children,
+ onValueChanged: (int newValue) {},
+ groupValue: currentValue,
+ ),
),
);
},
@@ -1308,5 +1349,5 @@
find.byType(RepaintBoundary),
matchesGoldenFile('segmented_control_test.1.0.png'),
);
- }, skip: !Platform.isLinux);
+ }, skip: !Platform.isMacOS);
}