Unverified Commit b0046b18 authored by Natalie Sampsell's avatar Natalie Sampsell Committed by GitHub

Segmented control fixes (#20202)

Segment width now determined by width of widest child + children widgets now centered within segments
parent 99d5ef90
64b7a3a7aef2fea9c7529d4834bf9eb3d85602d8 46cf554baf4840c326bbceaa51b11534069bb557
\ No newline at end of file
...@@ -46,10 +46,11 @@ const Duration _kFadeDuration = Duration(milliseconds: 165); ...@@ -46,10 +46,11 @@ const Duration _kFadeDuration = Duration(milliseconds: 165);
/// The [children] will be displayed in the order of the keys in the [Map]. /// 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 /// The height of the segmented control is determined by the height of the
/// tallest widget provided as a value in the [Map] of [children]. /// tallest widget provided as a value in the [Map] of [children].
/// The width of the segmented control is determined by the horizontal /// The width of each child in the segmented control will be equal to the width
/// constraints on its parent. The available horizontal space is divided by /// of widest child, unless the combined width of the children is wider than
/// the number of provided [children] to determine the width of each widget. /// the available horizontal space. In this case, the available horizontal space
/// The selection area for each of the widgets in the [Map] of /// 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 /// [children] will then be expanded to fill the calculated space, so each
/// widget will appear to have the same dimensions. /// widget will appear to have the same dimensions.
/// ///
...@@ -75,10 +76,10 @@ class SegmentedControl<T> extends StatefulWidget { ...@@ -75,10 +76,10 @@ class SegmentedControl<T> extends StatefulWidget {
/// in the [onValueChanged] callback when a new value from the [children] map /// in the [onValueChanged] callback when a new value from the [children] map
/// is selected. /// 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. /// The [groupValue] is the currently selected value for the segmented control.
/// If no [groupValue] is provided, or the [groupValue] is null, no widget will /// 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({ SegmentedControl({
Key key, Key key,
@required this.children, @required this.children,
...@@ -91,7 +92,8 @@ class SegmentedControl<T> extends StatefulWidget { ...@@ -91,7 +92,8 @@ class SegmentedControl<T> extends StatefulWidget {
}) : assert(children != null), }) : assert(children != null),
assert(children.length >= 2), assert(children.length >= 2),
assert(onValueChanged != null), 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(unselectedColor != null),
assert(selectedColor != null), assert(selectedColor != null),
assert(borderColor != null), assert(borderColor != null),
...@@ -189,7 +191,7 @@ class SegmentedControl<T> extends StatefulWidget { ...@@ -189,7 +191,7 @@ class SegmentedControl<T> extends StatefulWidget {
/// This attribute must not be null. /// This attribute must not be null.
/// ///
/// If this attribute is unspecified, this color will default to /// 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; final Color pressedColor;
@override @override
...@@ -346,7 +348,10 @@ class _SegmentedControlState<T> extends State<SegmentedControl<T>> ...@@ -346,7 +348,10 @@ class _SegmentedControlState<T> extends State<SegmentedControl<T>>
color: getTextColor(index, currentKey), color: getTextColor(index, currentKey),
); );
Widget child = widget.children[currentKey]; Widget child = new Center(
child: widget.children[currentKey],
);
child = new GestureDetector( child = new GestureDetector(
onTapDown: (TapDownDetails event) { onTapDown: (TapDownDetails event) {
_onTapDown(currentKey); _onTapDown(currentKey);
...@@ -599,15 +604,11 @@ class _RenderSegmentedControl<T> extends RenderBox ...@@ -599,15 +604,11 @@ class _RenderSegmentedControl<T> extends RenderBox
void performLayout() { void performLayout() {
double maxHeight = _kMinSegmentedControlHeight; double maxHeight = _kMinSegmentedControlHeight;
double childWidth; double childWidth = constraints.minWidth / childCount;
if (constraints.maxWidth.isFinite) { for (RenderBox child in getChildrenAsList()) {
childWidth = constraints.maxWidth / childCount; childWidth = math.max(childWidth, child.getMaxIntrinsicWidth(double.infinity));
} else {
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; RenderBox child = firstChild;
while (child != null) { while (child != null) {
......
...@@ -218,8 +218,6 @@ void main() { ...@@ -218,8 +218,6 @@ void main() {
), ),
); );
await tester.pumpAndSettle();
DefaultTextStyle textStyle = tester.widget(find.widgetWithText(DefaultTextStyle, 'Child 1')); DefaultTextStyle textStyle = tester.widget(find.widgetWithText(DefaultTextStyle, 'Child 1'));
IconTheme iconTheme = tester.widget(find.widgetWithIcon(IconTheme, const IconData(1))); IconTheme iconTheme = tester.widget(find.widgetWithIcon(IconTheme, const IconData(1)));
...@@ -238,63 +236,88 @@ void main() { ...@@ -238,63 +236,88 @@ void main() {
testWidgets('SegmentedControl is correct when user provides custom colors', testWidgets('SegmentedControl is correct when user provides custom colors',
(WidgetTester tester) async { (WidgetTester tester) async {
final Map<int, Widget> children = <int, Widget>{}; final Map<int, Widget> children = <int, Widget>{};
children[0] = const Text('Child 1'); children[0] = const Text('Child 1');
children[1] = const Icon(IconData(1)); children[1] = const Icon(IconData(1));
int sharedValue = 0; int sharedValue = 0;
await tester.pumpWidget( await tester.pumpWidget(
new StatefulBuilder( new StatefulBuilder(
builder: (BuildContext context, StateSetter setState) { builder: (BuildContext context, StateSetter setState) {
return boilerplate( return boilerplate(
child: new SegmentedControl<int>( child: new SegmentedControl<int>(
children: children, children: children,
onValueChanged: (int newValue) { onValueChanged: (int newValue) {
setState(() { setState(() {
sharedValue = newValue; sharedValue = newValue;
}); });
}, },
groupValue: sharedValue, groupValue: sharedValue,
unselectedColor: CupertinoColors.lightBackgroundGray, unselectedColor: CupertinoColors.lightBackgroundGray,
selectedColor: CupertinoColors.activeGreen, selectedColor: CupertinoColors.activeGreen,
borderColor: CupertinoColors.black, borderColor: CupertinoColors.black,
pressedColor: const Color(0x638CFC7B), pressedColor: const Color(0x638CFC7B),
), ),
); );
}, },
), ),
); );
await tester.pumpAndSettle(); DefaultTextStyle textStyle = tester.widget(find.widgetWithText(DefaultTextStyle, 'Child 1'));
IconTheme iconTheme = tester.widget(find.widgetWithIcon(IconTheme, const IconData(1)));
DefaultTextStyle textStyle = tester.widget(find.widgetWithText(DefaultTextStyle, 'Child 1')); expect(getRenderSegmentedControl(tester).borderColor, CupertinoColors.black);
IconTheme iconTheme = tester.widget(find.widgetWithIcon(IconTheme, const IconData(1))); expect(textStyle.style.color, CupertinoColors.lightBackgroundGray);
expect(iconTheme.data.color, CupertinoColors.activeGreen);
expect(getBackgroundColor(tester, 0), CupertinoColors.activeGreen);
expect(getBackgroundColor(tester, 1), CupertinoColors.lightBackgroundGray);
expect(getRenderSegmentedControl(tester).borderColor, CupertinoColors.black); await tester.tap(find.widgetWithIcon(IconTheme, const IconData(1)));
expect(textStyle.style.color, CupertinoColors.lightBackgroundGray); await tester.pumpAndSettle();
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))); textStyle = tester.widget(find.widgetWithText(DefaultTextStyle, 'Child 1'));
await tester.pumpAndSettle(); iconTheme = tester.widget(find.widgetWithIcon(IconTheme, const IconData(1)));
textStyle = tester.widget(find.widgetWithText(DefaultTextStyle, 'Child 1')); expect(textStyle.style.color, CupertinoColors.activeGreen);
iconTheme = tester.widget(find.widgetWithIcon(IconTheme, const IconData(1))); expect(iconTheme.data.color, CupertinoColors.lightBackgroundGray);
expect(getBackgroundColor(tester, 0), CupertinoColors.lightBackgroundGray);
expect(getBackgroundColor(tester, 1), CupertinoColors.activeGreen);
expect(textStyle.style.color, CupertinoColors.activeGreen); final Offset center = tester.getCenter(find.text('Child 1'));
expect(iconTheme.data.color, CupertinoColors.lightBackgroundGray); await tester.startGesture(center);
expect(getBackgroundColor(tester, 0), CupertinoColors.lightBackgroundGray); await tester.pumpAndSettle();
expect(getBackgroundColor(tester, 1), CupertinoColors.activeGreen);
final Offset center = tester.getCenter(find.text('Child 1')); expect(getBackgroundColor(tester, 0), const Color(0x638CFC7B));
await tester.startGesture(center); expect(getBackgroundColor(tester, 1), CupertinoColors.activeGreen);
await tester.pumpAndSettle(); });
expect(getBackgroundColor(tester, 0), const Color(0x638CFC7B)); testWidgets('Widgets are centered within segments', (WidgetTester tester) async {
expect(getBackgroundColor(tester, 1), CupertinoColors.activeGreen); 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) {},
),
),
),
),
);
// 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 { testWidgets('Tap calls onValueChanged', (WidgetTester tester) async {
final Map<int, Widget> children = <int, Widget>{}; final Map<int, Widget> children = <int, Widget>{};
...@@ -510,16 +533,21 @@ void main() { ...@@ -510,16 +533,21 @@ void main() {
final RenderBox buttonBox = tester.renderObject( final RenderBox buttonBox = tester.renderObject(
find.byKey(const ValueKey<String>('Segmented Control'))); 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); 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>{}; final Map<int, Widget> children = <int, Widget>{};
children[0] = new Container(); children[0] = new Container(
children[1] = const Placeholder(); constraints: const BoxConstraints.tightFor(width: 50.0),
children[2] = new Container(); );
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( await tester.pumpWidget(
new StatefulBuilder( new StatefulBuilder(
...@@ -542,6 +570,8 @@ void main() { ...@@ -542,6 +570,8 @@ void main() {
// to each child equally. // to each child equally.
final double childWidth = (segmentedControl.size.width - 32.0) / 3; final double childWidth = (segmentedControl.size.width - 32.0) / 3;
expect(childWidth, 200.0);
expect(childWidth, expect(childWidth,
getRenderSegmentedControl(tester).getChildrenAsList()[0].parentData.surroundingRect.width); getRenderSegmentedControl(tester).getChildrenAsList()[0].parentData.surroundingRect.width);
expect(childWidth, expect(childWidth,
...@@ -748,8 +778,8 @@ void main() { ...@@ -748,8 +778,8 @@ void main() {
testWidgets('Non-centered taps work on smaller widgets', (WidgetTester tester) async { testWidgets('Non-centered taps work on smaller widgets', (WidgetTester tester) async {
final Map<int, Widget> children = <int, Widget>{}; final Map<int, Widget> children = <int, Widget>{};
children[0] = const Text('A'); children[0] = const Text('Child 1');
children[1] = const Text('B'); children[1] = const Text('Child 2');
int sharedValue = 1; int sharedValue = 1;
...@@ -775,10 +805,15 @@ void main() { ...@@ -775,10 +805,15 @@ void main() {
expect(sharedValue, 1); expect(sharedValue, 1);
final double childWidth = getRenderSegmentedControl(tester).firstChild.size.width; 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 // 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); expect(sharedValue, 0);
}); });
...@@ -1257,11 +1292,14 @@ void main() { ...@@ -1257,11 +1292,14 @@ void main() {
child: new StatefulBuilder( child: new StatefulBuilder(
builder: (BuildContext context, StateSetter setState) { builder: (BuildContext context, StateSetter setState) {
return boilerplate( return boilerplate(
child: new SegmentedControl<int>( child: new SizedBox(
key: const ValueKey<String>('Segmented Control'), width: 800.0,
children: children, child: new SegmentedControl<int>(
onValueChanged: (int newValue) {}, key: const ValueKey<String>('Segmented Control'),
groupValue: currentValue, children: children,
onValueChanged: (int newValue) {},
groupValue: currentValue,
),
), ),
); );
}, },
...@@ -1273,7 +1311,7 @@ void main() { ...@@ -1273,7 +1311,7 @@ void main() {
find.byType(RepaintBoundary), find.byType(RepaintBoundary),
matchesGoldenFile('segmented_control_test.0.0.png'), matchesGoldenFile('segmented_control_test.0.0.png'),
); );
}, skip: !Platform.isLinux); }, skip: !Platform.isMacOS);
testWidgets('Golden Test Pressed State', (WidgetTester tester) async { testWidgets('Golden Test Pressed State', (WidgetTester tester) async {
final Map<int, Widget> children = <int, Widget>{}; final Map<int, Widget> children = <int, Widget>{};
...@@ -1288,11 +1326,14 @@ void main() { ...@@ -1288,11 +1326,14 @@ void main() {
child: new StatefulBuilder( child: new StatefulBuilder(
builder: (BuildContext context, StateSetter setState) { builder: (BuildContext context, StateSetter setState) {
return boilerplate( return boilerplate(
child: new SegmentedControl<int>( child: new SizedBox(
key: const ValueKey<String>('Segmented Control'), width: 800.0,
children: children, child: new SegmentedControl<int>(
onValueChanged: (int newValue) {}, key: const ValueKey<String>('Segmented Control'),
groupValue: currentValue, children: children,
onValueChanged: (int newValue) {},
groupValue: currentValue,
),
), ),
); );
}, },
...@@ -1308,5 +1349,5 @@ void main() { ...@@ -1308,5 +1349,5 @@ void main() {
find.byType(RepaintBoundary), find.byType(RepaintBoundary),
matchesGoldenFile('segmented_control_test.1.0.png'), matchesGoldenFile('segmented_control_test.1.0.png'),
); );
}, skip: !Platform.isLinux); }, skip: !Platform.isMacOS);
} }
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