Added ability to use custom colors for SegmentedControl (#20005)
This commit is contained in:
parent
091da9c7d3
commit
506cf3cb88
@ -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');
|
||||
|
Loading…
x
Reference in New Issue
Block a user