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
// 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:
///
/// * <https://developer.apple.com/design/human-interface-guidelines/ios/controls/segmented-controls/>
class SegmentedControl<T> 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<T> 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<T> extends StatefulWidget {
/// ```
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
_SegmentedControlState<T> createState() => _SegmentedControlState<T>();
}
......@@ -158,31 +203,33 @@ class _SegmentedControlState<T> extends State<SegmentedControl<T>>
final List<AnimationController> _selectionControllers = <AnimationController>[];
final List<ColorTween> _childTweens = <ColorTween>[];
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<T> extends State<SegmentedControl<T>>
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<T> extends State<SegmentedControl<T>>
} 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<T> extends State<SegmentedControl<T>>
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<T> extends State<SegmentedControl<T>>
selectedIndex: selectedIndex,
pressedIndex: pressedIndex,
backgroundColors: _backgroundColors,
borderColor: widget.borderColor,
);
return new Padding(
......@@ -351,6 +399,7 @@ class _SegmentedControlRenderWidget<T> 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<T> extends MultiChildRenderObjectWidget {
final int selectedIndex;
final int pressedIndex;
final List<Color> backgroundColors;
final Color borderColor;
@override
RenderObject createRenderObject(BuildContext context) {
......@@ -367,6 +417,7 @@ class _SegmentedControlRenderWidget<T> extends MultiChildRenderObjectWidget {
selectedIndex: selectedIndex,
pressedIndex: pressedIndex,
backgroundColors: backgroundColors,
borderColor: borderColor,
);
}
......@@ -376,7 +427,8 @@ class _SegmentedControlRenderWidget<T> extends MultiChildRenderObjectWidget {
..textDirection = Directionality.of(context)
..selectedIndex = selectedIndex
..pressedIndex = pressedIndex
..backgroundColors = backgroundColors;
..backgroundColors = backgroundColors
..borderColor = borderColor;
}
}
......@@ -395,11 +447,13 @@ class _RenderSegmentedControl<T> extends RenderBox
@required int pressedIndex,
@required TextDirection textDirection,
@required List<Color> 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<T> 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<T> 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);
......
......@@ -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<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',
......@@ -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<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 {
final Map<int, Widget> children = <int, Widget>{};
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