diff --git a/packages/flutter/lib/src/cupertino/segmented_control.dart b/packages/flutter/lib/src/cupertino/segmented_control.dart index 1340a92473f..64f0b0f897e 100644 --- a/packages/flutter/lib/src/cupertino/segmented_control.dart +++ b/packages/flutter/lib/src/cupertino/segmented_control.dart @@ -18,11 +18,6 @@ const EdgeInsets _kHorizontalItemPadding = const EdgeInsets.symmetric(horizontal // Minimum height of the segmented control. 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 // is selected. 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 /// 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: /// /// * class SegmentedControl extends StatefulWidget { /// 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]. /// Further, the length of the [children] list must be greater than one. /// @@ -82,10 +84,18 @@ class SegmentedControl extends StatefulWidget { @required this.children, @required this.onValueChanged, this.groupValue, + this.unselectedColor = CupertinoColors.white, + this.selectedColor = CupertinoColors.activeBlue, + this.borderColor = CupertinoColors.activeBlue, + this.pressedColor = const Color(0x33007AFF), }) : assert(children != null), assert(children.length >= 2), assert(onValueChanged != null), 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); /// The identifying keys and corresponding widget values in the @@ -147,6 +157,41 @@ class SegmentedControl extends StatefulWidget { /// ``` final ValueChanged 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 _SegmentedControlState createState() => _SegmentedControlState(); } @@ -158,31 +203,33 @@ class _SegmentedControlState extends State> final List _selectionControllers = []; final List _childTweens = []; - static final ColorTween forwardBackgroundColorTween = new ColorTween( - begin: _kPressedBackground, - end: CupertinoColors.activeBlue, - ); - - static final ColorTween reverseBackgroundColorTween = new ColorTween( - begin: CupertinoColors.white, - end: CupertinoColors.activeBlue, - ); - - static final ColorTween textColorTween = new ColorTween( - begin: CupertinoColors.activeBlue, - end: CupertinoColors.white, - ); + ColorTween _forwardBackgroundColorTween; + ColorTween _reverseBackgroundColorTween; + ColorTween _textColorTween; @override void 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) { final AnimationController animationController = createAnimationController(); if (widget.groupValue == key) { - _childTweens.add(reverseBackgroundColorTween); + _childTweens.add(_reverseBackgroundColorTween); animationController.value = 1.0; } else { - _childTweens.add(forwardBackgroundColorTween); + _childTweens.add(_forwardBackgroundColorTween); } _selectionControllers.add(animationController); } @@ -230,20 +277,20 @@ class _SegmentedControlState extends State> Color getTextColor(int index, T currentKey) { if (_selectionControllers[index].isAnimating) - return textColorTween.evaluate(_selectionControllers[index]); + return _textColorTween.evaluate(_selectionControllers[index]); if (widget.groupValue == currentKey) - return CupertinoColors.white; - return CupertinoColors.activeBlue; + return widget.unselectedColor; + return widget.selectedColor; } Color getBackgroundColor(int index, T currentKey) { if (_selectionControllers[index].isAnimating) return _childTweens[index].evaluate(_selectionControllers[index]); if (widget.groupValue == currentKey) - return CupertinoColors.activeBlue; + return widget.selectedColor; if (_pressedKey == currentKey) - return _kPressedBackground; - return CupertinoColors.white; + return widget.pressedColor; + return widget.unselectedColor; } void updateAnimationControllers() { @@ -253,7 +300,7 @@ class _SegmentedControlState extends State> } else { for (int index = _selectionControllers.length; index < widget.children.length; index += 1) { _selectionControllers.add(createAnimationController()); - _childTweens.add(reverseBackgroundColorTween); + _childTweens.add(_reverseBackgroundColorTween); } } } @@ -270,10 +317,10 @@ class _SegmentedControlState extends State> int index = 0; for (T key in widget.children.keys) { if (widget.groupValue == key) { - _childTweens[index] = forwardBackgroundColorTween; + _childTweens[index] = _forwardBackgroundColorTween; _selectionControllers[index].forward(); } else { - _childTweens[index] = reverseBackgroundColorTween; + _childTweens[index] = _reverseBackgroundColorTween; _selectionControllers[index].reverse(); } index += 1; @@ -332,6 +379,7 @@ class _SegmentedControlState extends State> selectedIndex: selectedIndex, pressedIndex: pressedIndex, backgroundColors: _backgroundColors, + borderColor: widget.borderColor, ); return new Padding( @@ -351,6 +399,7 @@ class _SegmentedControlRenderWidget extends MultiChildRenderObjectWidget { @required this.selectedIndex, @required this.pressedIndex, @required this.backgroundColors, + @required this.borderColor, }) : super( key: key, children: children, @@ -359,6 +408,7 @@ class _SegmentedControlRenderWidget extends MultiChildRenderObjectWidget { final int selectedIndex; final int pressedIndex; final List backgroundColors; + final Color borderColor; @override RenderObject createRenderObject(BuildContext context) { @@ -367,6 +417,7 @@ class _SegmentedControlRenderWidget extends MultiChildRenderObjectWidget { selectedIndex: selectedIndex, pressedIndex: pressedIndex, backgroundColors: backgroundColors, + borderColor: borderColor, ); } @@ -376,7 +427,8 @@ class _SegmentedControlRenderWidget extends MultiChildRenderObjectWidget { ..textDirection = Directionality.of(context) ..selectedIndex = selectedIndex ..pressedIndex = pressedIndex - ..backgroundColors = backgroundColors; + ..backgroundColors = backgroundColors + ..borderColor = borderColor; } } @@ -395,11 +447,13 @@ class _RenderSegmentedControl extends RenderBox @required int pressedIndex, @required TextDirection textDirection, @required List backgroundColors, + @required Color borderColor, }) : assert(textDirection != null), _textDirection = textDirection, _selectedIndex = selectedIndex, _pressedIndex = pressedIndex, - _backgroundColors = backgroundColors { + _backgroundColors = backgroundColors, + _borderColor = borderColor { addAll(children); } @@ -443,10 +497,15 @@ class _RenderSegmentedControl extends RenderBox markNeedsPaint(); } - final Paint _outlinePaint = new Paint() - ..color = CupertinoColors.activeBlue - ..strokeWidth = 1.0 - ..style = PaintingStyle.stroke; + Color get borderColor => _borderColor; + Color _borderColor; + set borderColor(Color value) { + if (_borderColor == value) { + return; + } + _borderColor = value; + markNeedsPaint(); + } @override double computeMinIntrinsicWidth(double height) { @@ -614,7 +673,10 @@ class _RenderSegmentedControl extends RenderBox ); context.canvas.drawRRect( childParentData.surroundingRect.shift(offset), - _outlinePaint, + new Paint() + ..color = borderColor + ..strokeWidth = 1.0 + ..style = PaintingStyle.stroke, ); context.paintChild(child, childParentData.offset + offset); diff --git a/packages/flutter/test/cupertino/segmented_control_test.dart b/packages/flutter/test/cupertino/segmented_control_test.dart index 502724e0592..40c9f236874 100644 --- a/packages/flutter/test/cupertino/segmented_control_test.dart +++ b/packages/flutter/test/cupertino/segmented_control_test.dart @@ -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 { await tester.pumpWidget( boilerplate( @@ -174,6 +175,21 @@ void main() { } on AssertionError catch (e) { expect(e.toString(), contains('onValueChanged')); } + + try { + await tester.pumpWidget( + boilerplate( + child: new SegmentedControl( + 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', @@ -220,6 +236,66 @@ void main() { expect(iconTheme.data.color, CupertinoColors.white); }); + testWidgets('SegmentedControl is correct when user provides custom colors', + (WidgetTester tester) async { + final Map children = {}; + 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( + 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 { final Map children = {}; children[0] = const Text('Child 1');