Unverified Commit 506cf3cb authored by Natalie Sampsell's avatar Natalie Sampsell Committed by GitHub

Added ability to use custom colors for SegmentedControl (#20005)

parent 091da9c7
...@@ -18,11 +18,6 @@ const EdgeInsets _kHorizontalItemPadding = const EdgeInsets.symmetric(horizontal ...@@ -18,11 +18,6 @@ const EdgeInsets _kHorizontalItemPadding = const EdgeInsets.symmetric(horizontal
// Minimum height of the segmented control. // Minimum height of the segmented control.
const double _kMinSegmentedControlHeight = 28.0; const double _kMinSegmentedControlHeight = 28.0;
// Light, partially-transparent blue color. Used to fill the background of
// a child option the user is temporarily interacting with through a long
// press or drag.
const Color _kPressedBackground = const Color(0x33007aff);
// The duration of the fade animation used to transition when a new widget // The duration of the fade animation used to transition when a new widget
// is selected. // is selected.
const Duration _kFadeDuration = const Duration(milliseconds: 165); const Duration _kFadeDuration = const Duration(milliseconds: 165);
...@@ -58,13 +53,20 @@ const Duration _kFadeDuration = const Duration(milliseconds: 165); ...@@ -58,13 +53,20 @@ const Duration _kFadeDuration = const Duration(milliseconds: 165);
/// [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.
/// ///
/// A segmented control may optionally be created with custom colors. The
/// [unselectedColor], [selectedColor], [borderColor], and [pressedColor]
/// arguments can be used to change the segmented control's colors from
/// [CupertinoColors.activeBlue] and [CupertinoColors.white] to a custom
/// configuration.
///
/// See also: /// See also:
/// ///
/// * <https://developer.apple.com/design/human-interface-guidelines/ios/controls/segmented-controls/> /// * <https://developer.apple.com/design/human-interface-guidelines/ios/controls/segmented-controls/>
class SegmentedControl<T> extends StatefulWidget { class SegmentedControl<T> extends StatefulWidget {
/// Creates an iOS-style segmented control bar. /// Creates an iOS-style segmented control bar.
/// ///
/// The [children] and [onValueChanged] arguments must not be null. The /// The [children], [onValueChanged], [unselectedColor], [selectedColor],
/// [borderColor], and [pressedColor] arguments must not be null. The
/// [children] argument must be an ordered [Map] such as a [LinkedHashMap]. /// [children] argument must be an ordered [Map] such as a [LinkedHashMap].
/// Further, the length of the [children] list must be greater than one. /// Further, the length of the [children] list must be greater than one.
/// ///
...@@ -82,10 +84,18 @@ class SegmentedControl<T> extends StatefulWidget { ...@@ -82,10 +84,18 @@ class SegmentedControl<T> extends StatefulWidget {
@required this.children, @required this.children,
@required this.onValueChanged, @required this.onValueChanged,
this.groupValue, this.groupValue,
this.unselectedColor = CupertinoColors.white,
this.selectedColor = CupertinoColors.activeBlue,
this.borderColor = CupertinoColors.activeBlue,
this.pressedColor = const Color(0x33007AFF),
}) : 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)),
assert(unselectedColor != null),
assert(selectedColor != null),
assert(borderColor != null),
assert(pressedColor != null),
super(key: key); super(key: key);
/// The identifying keys and corresponding widget values in the /// The identifying keys and corresponding widget values in the
...@@ -147,6 +157,41 @@ class SegmentedControl<T> extends StatefulWidget { ...@@ -147,6 +157,41 @@ class SegmentedControl<T> extends StatefulWidget {
/// ``` /// ```
final ValueChanged<T> onValueChanged; final ValueChanged<T> onValueChanged;
/// The color used to fill the backgrounds of unselected widgets and as the
/// text color of the selected widget.
///
/// This attribute must not be null.
///
/// If this attribute is unspecified, this color will default to
/// [CupertinoColors.white].
final Color unselectedColor;
/// The color used to fill the background of the selected widget and as the text
/// color of unselected widgets.
///
/// This attribute must not be null.
///
/// If this attribute is unspecified, this color will default to
/// [CupertinoColors.activeBlue].
final Color selectedColor;
/// The color used as the border around each widget.
///
/// This attribute must not be null.
///
/// If this attribute is unspecified, this color will default to
/// [CupertinoColors.activeBlue].
final Color borderColor;
/// The color used to fill the background of the widget the user is
/// temporarily interacting with through a long press or drag.
///
/// This attribute must not be null.
///
/// If this attribute is unspecified, this color will default to
/// 'Color(0x33007AFF)', a light, partially-transparent blue color.
final Color pressedColor;
@override @override
_SegmentedControlState<T> createState() => _SegmentedControlState<T>(); _SegmentedControlState<T> createState() => _SegmentedControlState<T>();
} }
...@@ -158,31 +203,33 @@ class _SegmentedControlState<T> extends State<SegmentedControl<T>> ...@@ -158,31 +203,33 @@ class _SegmentedControlState<T> extends State<SegmentedControl<T>>
final List<AnimationController> _selectionControllers = <AnimationController>[]; final List<AnimationController> _selectionControllers = <AnimationController>[];
final List<ColorTween> _childTweens = <ColorTween>[]; final List<ColorTween> _childTweens = <ColorTween>[];
static final ColorTween forwardBackgroundColorTween = new ColorTween( ColorTween _forwardBackgroundColorTween;
begin: _kPressedBackground, ColorTween _reverseBackgroundColorTween;
end: CupertinoColors.activeBlue, ColorTween _textColorTween;
);
static final ColorTween reverseBackgroundColorTween = new ColorTween(
begin: CupertinoColors.white,
end: CupertinoColors.activeBlue,
);
static final ColorTween textColorTween = new ColorTween(
begin: CupertinoColors.activeBlue,
end: CupertinoColors.white,
);
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_forwardBackgroundColorTween = new ColorTween(
begin: widget.pressedColor,
end: widget.selectedColor,
);
_reverseBackgroundColorTween = new ColorTween(
begin: widget.unselectedColor,
end: widget.selectedColor,
);
_textColorTween = new ColorTween(
begin: widget.selectedColor,
end: widget.unselectedColor,
);
for (T key in widget.children.keys) { for (T key in widget.children.keys) {
final AnimationController animationController = createAnimationController(); final AnimationController animationController = createAnimationController();
if (widget.groupValue == key) { if (widget.groupValue == key) {
_childTweens.add(reverseBackgroundColorTween); _childTweens.add(_reverseBackgroundColorTween);
animationController.value = 1.0; animationController.value = 1.0;
} else { } else {
_childTweens.add(forwardBackgroundColorTween); _childTweens.add(_forwardBackgroundColorTween);
} }
_selectionControllers.add(animationController); _selectionControllers.add(animationController);
} }
...@@ -230,20 +277,20 @@ class _SegmentedControlState<T> extends State<SegmentedControl<T>> ...@@ -230,20 +277,20 @@ class _SegmentedControlState<T> extends State<SegmentedControl<T>>
Color getTextColor(int index, T currentKey) { Color getTextColor(int index, T currentKey) {
if (_selectionControllers[index].isAnimating) if (_selectionControllers[index].isAnimating)
return textColorTween.evaluate(_selectionControllers[index]); return _textColorTween.evaluate(_selectionControllers[index]);
if (widget.groupValue == currentKey) if (widget.groupValue == currentKey)
return CupertinoColors.white; return widget.unselectedColor;
return CupertinoColors.activeBlue; return widget.selectedColor;
} }
Color getBackgroundColor(int index, T currentKey) { Color getBackgroundColor(int index, T currentKey) {
if (_selectionControllers[index].isAnimating) if (_selectionControllers[index].isAnimating)
return _childTweens[index].evaluate(_selectionControllers[index]); return _childTweens[index].evaluate(_selectionControllers[index]);
if (widget.groupValue == currentKey) if (widget.groupValue == currentKey)
return CupertinoColors.activeBlue; return widget.selectedColor;
if (_pressedKey == currentKey) if (_pressedKey == currentKey)
return _kPressedBackground; return widget.pressedColor;
return CupertinoColors.white; return widget.unselectedColor;
} }
void updateAnimationControllers() { void updateAnimationControllers() {
...@@ -253,7 +300,7 @@ class _SegmentedControlState<T> extends State<SegmentedControl<T>> ...@@ -253,7 +300,7 @@ class _SegmentedControlState<T> extends State<SegmentedControl<T>>
} else { } else {
for (int index = _selectionControllers.length; index < widget.children.length; index += 1) { for (int index = _selectionControllers.length; index < widget.children.length; index += 1) {
_selectionControllers.add(createAnimationController()); _selectionControllers.add(createAnimationController());
_childTweens.add(reverseBackgroundColorTween); _childTweens.add(_reverseBackgroundColorTween);
} }
} }
} }
...@@ -270,10 +317,10 @@ class _SegmentedControlState<T> extends State<SegmentedControl<T>> ...@@ -270,10 +317,10 @@ class _SegmentedControlState<T> extends State<SegmentedControl<T>>
int index = 0; int index = 0;
for (T key in widget.children.keys) { for (T key in widget.children.keys) {
if (widget.groupValue == key) { if (widget.groupValue == key) {
_childTweens[index] = forwardBackgroundColorTween; _childTweens[index] = _forwardBackgroundColorTween;
_selectionControllers[index].forward(); _selectionControllers[index].forward();
} else { } else {
_childTweens[index] = reverseBackgroundColorTween; _childTweens[index] = _reverseBackgroundColorTween;
_selectionControllers[index].reverse(); _selectionControllers[index].reverse();
} }
index += 1; index += 1;
...@@ -332,6 +379,7 @@ class _SegmentedControlState<T> extends State<SegmentedControl<T>> ...@@ -332,6 +379,7 @@ class _SegmentedControlState<T> extends State<SegmentedControl<T>>
selectedIndex: selectedIndex, selectedIndex: selectedIndex,
pressedIndex: pressedIndex, pressedIndex: pressedIndex,
backgroundColors: _backgroundColors, backgroundColors: _backgroundColors,
borderColor: widget.borderColor,
); );
return new Padding( return new Padding(
...@@ -351,6 +399,7 @@ class _SegmentedControlRenderWidget<T> extends MultiChildRenderObjectWidget { ...@@ -351,6 +399,7 @@ class _SegmentedControlRenderWidget<T> extends MultiChildRenderObjectWidget {
@required this.selectedIndex, @required this.selectedIndex,
@required this.pressedIndex, @required this.pressedIndex,
@required this.backgroundColors, @required this.backgroundColors,
@required this.borderColor,
}) : super( }) : super(
key: key, key: key,
children: children, children: children,
...@@ -359,6 +408,7 @@ class _SegmentedControlRenderWidget<T> extends MultiChildRenderObjectWidget { ...@@ -359,6 +408,7 @@ class _SegmentedControlRenderWidget<T> extends MultiChildRenderObjectWidget {
final int selectedIndex; final int selectedIndex;
final int pressedIndex; final int pressedIndex;
final List<Color> backgroundColors; final List<Color> backgroundColors;
final Color borderColor;
@override @override
RenderObject createRenderObject(BuildContext context) { RenderObject createRenderObject(BuildContext context) {
...@@ -367,6 +417,7 @@ class _SegmentedControlRenderWidget<T> extends MultiChildRenderObjectWidget { ...@@ -367,6 +417,7 @@ class _SegmentedControlRenderWidget<T> extends MultiChildRenderObjectWidget {
selectedIndex: selectedIndex, selectedIndex: selectedIndex,
pressedIndex: pressedIndex, pressedIndex: pressedIndex,
backgroundColors: backgroundColors, backgroundColors: backgroundColors,
borderColor: borderColor,
); );
} }
...@@ -376,7 +427,8 @@ class _SegmentedControlRenderWidget<T> extends MultiChildRenderObjectWidget { ...@@ -376,7 +427,8 @@ class _SegmentedControlRenderWidget<T> extends MultiChildRenderObjectWidget {
..textDirection = Directionality.of(context) ..textDirection = Directionality.of(context)
..selectedIndex = selectedIndex ..selectedIndex = selectedIndex
..pressedIndex = pressedIndex ..pressedIndex = pressedIndex
..backgroundColors = backgroundColors; ..backgroundColors = backgroundColors
..borderColor = borderColor;
} }
} }
...@@ -395,11 +447,13 @@ class _RenderSegmentedControl<T> extends RenderBox ...@@ -395,11 +447,13 @@ class _RenderSegmentedControl<T> extends RenderBox
@required int pressedIndex, @required int pressedIndex,
@required TextDirection textDirection, @required TextDirection textDirection,
@required List<Color> backgroundColors, @required List<Color> backgroundColors,
@required Color borderColor,
}) : assert(textDirection != null), }) : assert(textDirection != null),
_textDirection = textDirection, _textDirection = textDirection,
_selectedIndex = selectedIndex, _selectedIndex = selectedIndex,
_pressedIndex = pressedIndex, _pressedIndex = pressedIndex,
_backgroundColors = backgroundColors { _backgroundColors = backgroundColors,
_borderColor = borderColor {
addAll(children); addAll(children);
} }
...@@ -443,10 +497,15 @@ class _RenderSegmentedControl<T> extends RenderBox ...@@ -443,10 +497,15 @@ class _RenderSegmentedControl<T> extends RenderBox
markNeedsPaint(); markNeedsPaint();
} }
final Paint _outlinePaint = new Paint() Color get borderColor => _borderColor;
..color = CupertinoColors.activeBlue Color _borderColor;
..strokeWidth = 1.0 set borderColor(Color value) {
..style = PaintingStyle.stroke; if (_borderColor == value) {
return;
}
_borderColor = value;
markNeedsPaint();
}
@override @override
double computeMinIntrinsicWidth(double height) { double computeMinIntrinsicWidth(double height) {
...@@ -614,7 +673,10 @@ class _RenderSegmentedControl<T> extends RenderBox ...@@ -614,7 +673,10 @@ class _RenderSegmentedControl<T> extends RenderBox
); );
context.canvas.drawRRect( context.canvas.drawRRect(
childParentData.surroundingRect.shift(offset), childParentData.surroundingRect.shift(offset),
_outlinePaint, new Paint()
..color = borderColor
..strokeWidth = 1.0
..style = PaintingStyle.stroke,
); );
context.paintChild(child, childParentData.offset + offset); context.paintChild(child, childParentData.offset + offset);
......
...@@ -142,7 +142,8 @@ void main() { ...@@ -142,7 +142,8 @@ void main() {
} }
}); });
testWidgets('Children and onValueChanged can not be null', (WidgetTester tester) async { testWidgets('Children, onValueChanged, and color arguments can not be null',
(WidgetTester tester) async {
try { try {
await tester.pumpWidget( await tester.pumpWidget(
boilerplate( boilerplate(
...@@ -174,6 +175,21 @@ void main() { ...@@ -174,6 +175,21 @@ void main() {
} on AssertionError catch (e) { } on AssertionError catch (e) {
expect(e.toString(), contains('onValueChanged')); expect(e.toString(), contains('onValueChanged'));
} }
try {
await tester.pumpWidget(
boilerplate(
child: new SegmentedControl<int>(
children: children,
onValueChanged: (int newValue) {},
unselectedColor: null,
),
),
);
fail('Should not be possible to create segmented control with null unselectedColor');
} on AssertionError catch (e) {
expect(e.toString(), contains('unselectedColor'));
}
}); });
testWidgets('Widgets have correct default text/icon styles, change correctly on selection', testWidgets('Widgets have correct default text/icon styles, change correctly on selection',
...@@ -220,6 +236,66 @@ void main() { ...@@ -220,6 +236,66 @@ void main() {
expect(iconTheme.data.color, CupertinoColors.white); expect(iconTheme.data.color, CupertinoColors.white);
}); });
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));
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.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);
});
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>{};
children[0] = const Text('Child 1'); children[0] = const Text('Child 1');
......
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