Segmented control fixes (#20202)
Segment width now determined by width of widest child + children widgets now centered within segments
This commit is contained in:
parent
99d5ef903c
commit
b0046b1811
@ -1,2 +1 @@
|
|||||||
64b7a3a7aef2fea9c7529d4834bf9eb3d85602d8
|
46cf554baf4840c326bbceaa51b11534069bb557
|
||||||
|
|
@ -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 {
|
|||||||
/// 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 {
|
|||||||
}) : 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 {
|
|||||||
/// 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>>
|
|||||||
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
|
|||||||
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() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
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() {
|
|||||||
|
|
||||||
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),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
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();
|
// Widgets are centered taking into account 16px of horizontal padding
|
||||||
|
expect(tester.getCenter(find.text('Child 1')), const Offset(58.0, 100.0));
|
||||||
DefaultTextStyle textStyle = tester.widget(find.widgetWithText(DefaultTextStyle, 'Child 1'));
|
expect(tester.getCenter(find.text('Child 2')), const Offset(142.0, 100.0));
|
||||||
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('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() {
|
|||||||
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() {
|
|||||||
// 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() {
|
|||||||
|
|
||||||
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() {
|
|||||||
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() {
|
|||||||
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() {
|
|||||||
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() {
|
|||||||
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() {
|
|||||||
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);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user