Unverified Commit 236acb52 authored by Natalie Sampsell's avatar Natalie Sampsell Committed by GitHub

Segmented control animation (#18811)

Added animation to segmented control widget.
parent 03a1f4ac
......@@ -23,6 +23,10 @@ const double _kMinSegmentedControlHeight = 28.0;
// 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);
/// An iOS-style segmented control.
///
/// Displays the widgets provided in the [Map] of [children] in a
......@@ -147,19 +151,68 @@ class SegmentedControl<T> extends StatefulWidget {
_SegmentedControlState<T> createState() => _SegmentedControlState<T>();
}
class _SegmentedControlState<T> extends State<SegmentedControl<T>> {
class _SegmentedControlState<T> extends State<SegmentedControl<T>>
with TickerProviderStateMixin<SegmentedControl<T>> {
T _pressedKey;
void _onTapDown(T currentKey) {
setState(() {
_pressedKey = currentKey;
});
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,
);
@override
void initState() {
super.initState();
for (T key in widget.children.keys) {
final AnimationController animationController = createAnimationController();
if (widget.groupValue == key) {
_childTweens.add(reverseBackgroundColorTween);
animationController.value = 1.0;
} else {
_childTweens.add(forwardBackgroundColorTween);
}
_selectionControllers.add(animationController);
}
}
void _onTapUp(TapUpDetails event) {
setState(() {
_pressedKey = null;
});
AnimationController createAnimationController() {
return new AnimationController(
duration: _kFadeDuration,
vsync: this,
)..addListener(() {
setState(() {
// State of background/text colors has changed
});
});
}
@override
void dispose() {
for (AnimationController animationController in _selectionControllers) {
animationController.dispose();
}
super.dispose();
}
void _onTapDown(T currentKey) {
if (_pressedKey == null && currentKey != widget.groupValue) {
setState(() {
_pressedKey = currentKey;
});
}
}
void _onTapCancel() {
......@@ -169,14 +222,69 @@ class _SegmentedControlState<T> extends State<SegmentedControl<T>> {
}
void _onTap(T currentKey) {
if (currentKey != widget.groupValue) {
if (currentKey != widget.groupValue && currentKey == _pressedKey) {
widget.onValueChanged(currentKey);
_pressedKey = null;
}
}
Color getTextColor(int index, T currentKey) {
if (_selectionControllers[index].isAnimating)
return textColorTween.evaluate(_selectionControllers[index]);
if (widget.groupValue == currentKey)
return CupertinoColors.white;
return CupertinoColors.activeBlue;
}
Color getBackgroundColor(int index, T currentKey) {
if (_selectionControllers[index].isAnimating)
return _childTweens[index].evaluate(_selectionControllers[index]);
if (widget.groupValue == currentKey)
return CupertinoColors.activeBlue;
if (_pressedKey == currentKey)
return _kPressedBackground;
return CupertinoColors.white;
}
void updateAnimationControllers() {
if (_selectionControllers.length > widget.children.length) {
_selectionControllers.length = widget.children.length;
_childTweens.length = widget.children.length;
} else {
for (int index = _selectionControllers.length; index < widget.children.length; index += 1) {
_selectionControllers.add(createAnimationController());
_childTweens.add(reverseBackgroundColorTween);
}
}
}
@override
void didUpdateWidget(SegmentedControl<T> oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.children.length != widget.children.length) {
updateAnimationControllers();
}
if (oldWidget.groupValue != widget.groupValue) {
int index = 0;
for (T key in widget.children.keys) {
if (widget.groupValue == key) {
_childTweens[index] = forwardBackgroundColorTween;
_selectionControllers[index].forward();
} else {
_childTweens[index] = reverseBackgroundColorTween;
_selectionControllers[index].reverse();
}
index += 1;
}
}
}
@override
Widget build(BuildContext context) {
final List<Widget> gestureChildren = <Widget>[];
final List<Widget> _gestureChildren = <Widget>[];
final List<Color> _backgroundColors = <Color>[];
int index = 0;
int selectedIndex;
int pressedIndex;
......@@ -185,12 +293,10 @@ class _SegmentedControlState<T> extends State<SegmentedControl<T>> {
pressedIndex = (_pressedKey == currentKey) ? index : pressedIndex;
final TextStyle textStyle = DefaultTextStyle.of(context).style.copyWith(
color: (widget.groupValue == currentKey) ?
CupertinoColors.white : CupertinoColors.activeBlue,
color: getTextColor(index, currentKey),
);
final IconThemeData iconTheme = new IconThemeData(
color: (widget.groupValue == currentKey) ?
CupertinoColors.white : CupertinoColors.activeBlue,
color: getTextColor(index, currentKey),
);
Widget child = widget.children[currentKey];
......@@ -198,7 +304,6 @@ class _SegmentedControlState<T> extends State<SegmentedControl<T>> {
onTapDown: (TapDownDetails event) {
_onTapDown(currentKey);
},
onTapUp: _onTapUp,
onTapCancel: _onTapCancel,
onTap: () {
_onTap(currentKey);
......@@ -215,14 +320,17 @@ class _SegmentedControlState<T> extends State<SegmentedControl<T>> {
),
),
);
gestureChildren.add(child);
_backgroundColors.add(getBackgroundColor(index, currentKey));
_gestureChildren.add(child);
index += 1;
}
final Widget box = new _SegmentedControlRenderWidget<T>(
children: gestureChildren,
children: _gestureChildren,
selectedIndex: selectedIndex,
pressedIndex: pressedIndex,
backgroundColors: _backgroundColors,
);
return new Padding(
......@@ -241,6 +349,7 @@ class _SegmentedControlRenderWidget<T> extends MultiChildRenderObjectWidget {
List<Widget> children = const <Widget>[],
@required this.selectedIndex,
@required this.pressedIndex,
@required this.backgroundColors,
}) : super(
key: key,
children: children,
......@@ -248,6 +357,7 @@ class _SegmentedControlRenderWidget<T> extends MultiChildRenderObjectWidget {
final int selectedIndex;
final int pressedIndex;
final List<Color> backgroundColors;
@override
RenderObject createRenderObject(BuildContext context) {
......@@ -255,6 +365,7 @@ class _SegmentedControlRenderWidget<T> extends MultiChildRenderObjectWidget {
textDirection: Directionality.of(context),
selectedIndex: selectedIndex,
pressedIndex: pressedIndex,
backgroundColors: backgroundColors,
);
}
......@@ -263,7 +374,8 @@ class _SegmentedControlRenderWidget<T> extends MultiChildRenderObjectWidget {
renderObject
..textDirection = Directionality.of(context)
..selectedIndex = selectedIndex
..pressedIndex = pressedIndex;
..pressedIndex = pressedIndex
..backgroundColors = backgroundColors;
}
}
......@@ -281,10 +393,12 @@ class _RenderSegmentedControl<T> extends RenderBox
@required int selectedIndex,
@required int pressedIndex,
@required TextDirection textDirection,
@required List<Color> backgroundColors,
}) : assert(textDirection != null),
_textDirection = textDirection,
_selectedIndex = selectedIndex,
_pressedIndex = pressedIndex {
_pressedIndex = pressedIndex,
_backgroundColors = backgroundColors {
addAll(children);
}
......@@ -318,6 +432,16 @@ class _RenderSegmentedControl<T> extends RenderBox
markNeedsLayout();
}
List<Color> get backgroundColors => _backgroundColors;
List<Color> _backgroundColors;
set backgroundColors(List<Color> value) {
if (_backgroundColors == value) {
return;
}
_backgroundColors = value;
markNeedsPaint();
}
final Paint _outlinePaint = new Paint()
..color = CupertinoColors.activeBlue
..strokeWidth = 1.0
......@@ -481,17 +605,10 @@ class _RenderSegmentedControl<T> extends RenderBox
final _SegmentedControlContainerBoxParentData childParentData = child.parentData;
Color color = CupertinoColors.white;
if (selectedIndex != null && selectedIndex == childIndex) {
color = CupertinoColors.activeBlue;
} else if (pressedIndex != null && pressedIndex == childIndex) {
color = _kPressedBackground;
}
context.canvas.drawRRect(
childParentData.surroundingRect.shift(offset),
new Paint()
..color = color
..color = backgroundColors[childIndex]
..style = PaintingStyle.fill,
);
context.canvas.drawRRect(
......@@ -505,6 +622,14 @@ class _RenderSegmentedControl<T> extends RenderBox
@override
bool hitTestChildren(HitTestResult result, {@required Offset position}) {
assert(position != null);
return defaultHitTestChildren(result, position: position);
RenderBox child = lastChild;
while (child != null) {
final _SegmentedControlContainerBoxParentData childParentData = child.parentData;
if (childParentData.surroundingRect.contains(position)) {
return child.hitTest(result, position: (Offset.zero & child.size).center);
}
child = childParentData.previousSibling;
}
return false;
}
}
......@@ -8,7 +8,6 @@ import 'package:flutter/widgets.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_test/flutter_test.dart';
import '../rendering/mock_canvas.dart';
import '../widgets/semantics_tester.dart';
dynamic getRenderSegmentedControl(WidgetTester tester) {
......@@ -23,7 +22,6 @@ StatefulBuilder setupSimpleSegmentedControl() {
final Map<int, Widget> children = <int, Widget>{};
children[0] = const Text('Child 1');
children[1] = const Text('Child 2');
int sharedValue = 0;
return new StatefulBuilder(
......@@ -50,6 +48,10 @@ Widget boilerplate({Widget child}) {
);
}
Color getBackgroundColor(WidgetTester tester, int childIndex) {
return getRenderSegmentedControl(tester).backgroundColors[childIndex];
}
void main() {
testWidgets('Tap changes toggle state', (WidgetTester tester) async {
final Map<int, Widget> children = <int, Widget>{};
......@@ -96,8 +98,7 @@ void main() {
),
),
);
fail(
'Should not be possible to create a segmented control with no children');
fail('Should not be possible to create a segmented control with no children');
} on AssertionError catch (e) {
expect(e.toString(), contains('children.length'));
}
......@@ -112,15 +113,14 @@ void main() {
),
),
);
fail(
'Should not be possible to create a segmented control with just one child');
fail('Should not be possible to create a segmented control with just one child');
} on AssertionError catch (e) {
expect(e.toString(), contains('children.length'));
}
});
testWidgets('Value attribute must be the key of one of the children widgets',
(WidgetTester tester) async {
(WidgetTester tester) async {
final Map<int, Widget> children = <int, Widget>{};
children[0] = const Text('Child 1');
children[1] = const Text('Child 2');
......@@ -142,8 +142,7 @@ void main() {
}
});
testWidgets('Children and onValueChanged can not be null',
(WidgetTester tester) async {
testWidgets('Children and onValueChanged can not be null', (WidgetTester tester) async {
try {
await tester.pumpWidget(
boilerplate(
......@@ -153,8 +152,7 @@ void main() {
),
),
);
fail(
'Should not be possible to create segmented control with null children');
fail('Should not be possible to create segmented control with null children');
} on AssertionError catch (e) {
expect(e.toString(), contains('children'));
}
......@@ -172,16 +170,14 @@ void main() {
),
),
);
fail(
'Should not be possible to create segmented control with null onValueChanged');
fail('Should not be possible to create segmented control with null onValueChanged');
} on AssertionError catch (e) {
expect(e.toString(), contains('onValueChanged'));
}
});
testWidgets(
'Widgets have correct default text/icon styles, change correctly on selection',
(WidgetTester tester) async {
testWidgets('Widgets have correct default text/icon styles, change correctly on selection',
(WidgetTester tester) async {
final Map<int, Widget> children = <int, Widget>{};
children[0] = const Text('Child 1');
children[1] = const Icon(IconData(1));
......@@ -206,20 +202,19 @@ void main() {
),
);
DefaultTextStyle textStyle =
tester.widget(find.widgetWithText(DefaultTextStyle, 'Child 1'));
IconTheme iconTheme =
tester.widget(find.widgetWithIcon(IconTheme, const IconData(1)));
await tester.pumpAndSettle();
DefaultTextStyle textStyle = tester.widget(find.widgetWithText(DefaultTextStyle, 'Child 1'));
IconTheme iconTheme = tester.widget(find.widgetWithIcon(IconTheme, const IconData(1)));
expect(textStyle.style.color, CupertinoColors.white);
expect(iconTheme.data.color, CupertinoColors.activeBlue);
await tester.tap(find.widgetWithIcon(IconTheme, const IconData(1)));
await tester.pump();
await tester.pumpAndSettle();
textStyle = tester.widget(find.widgetWithText(DefaultTextStyle, 'Child 1'));
iconTheme =
tester.widget(find.widgetWithIcon(IconTheme, const IconData(1)));
iconTheme = tester.widget(find.widgetWithIcon(IconTheme, const IconData(1)));
expect(textStyle.style.color, CupertinoColors.activeBlue);
expect(iconTheme.data.color, CupertinoColors.white);
......@@ -254,9 +249,8 @@ void main() {
expect(value, isTrue);
});
testWidgets(
'State does not change if onValueChanged does not call setState()',
(WidgetTester tester) async {
testWidgets('State does not change if onValueChanged does not call setState()',
(WidgetTester tester) async {
final Map<int, Widget> children = <int, Widget>{};
children[0] = const Text('Child 1');
children[1] = const Text('Child 2');
......@@ -277,49 +271,14 @@ void main() {
),
);
final dynamic childList =
getRenderSegmentedControl(tester).getChildrenAsList();
expect(
getRenderSegmentedControl(tester),
paints
..rrect(
rrect: childList.elementAt(0).parentData.surroundingRect,
color: CupertinoColors.activeBlue,
),
);
expect(
getRenderSegmentedControl(tester),
paints
..rrect()
..rrect()
..rrect(
rrect: childList.elementAt(1).parentData.surroundingRect,
color: CupertinoColors.white,
),
);
expect(getBackgroundColor(tester, 0), CupertinoColors.activeBlue);
expect(getBackgroundColor(tester, 1), CupertinoColors.white);
await tester.tap(find.text('Child 2'));
await tester.pump();
expect(
getRenderSegmentedControl(tester),
paints
..rrect(
rrect: childList.elementAt(0).parentData.surroundingRect,
color: CupertinoColors.activeBlue,
),
);
expect(
getRenderSegmentedControl(tester),
paints
..rrect()
..rrect()
..rrect(
rrect: childList.elementAt(1).parentData.surroundingRect,
color: CupertinoColors.white,
),
);
expect(getBackgroundColor(tester, 0), CupertinoColors.activeBlue);
expect(getBackgroundColor(tester, 1), CupertinoColors.white);
});
testWidgets(
......@@ -327,47 +286,17 @@ void main() {
'and should not change when tapped again', (WidgetTester tester) async {
await tester.pumpWidget(setupSimpleSegmentedControl());
final dynamic childList =
getRenderSegmentedControl(tester).getChildrenAsList();
expect(
getRenderSegmentedControl(tester),
paints
..rrect()
..rrect()
..rrect(
rrect: childList.elementAt(1).parentData.surroundingRect,
color: CupertinoColors.white,
),
);
expect(getBackgroundColor(tester, 1), CupertinoColors.white);
await tester.tap(find.text('Child 2'));
await tester.pump();
await tester.pumpAndSettle(const Duration(milliseconds: 200));
expect(
getRenderSegmentedControl(tester),
paints
..rrect()
..rrect()
..rrect(
rrect: childList.elementAt(1).parentData.surroundingRect,
color: CupertinoColors.activeBlue,
),
);
expect(getBackgroundColor(tester, 1), CupertinoColors.activeBlue);
await tester.tap(find.text('Child 2'));
await tester.pump();
expect(
getRenderSegmentedControl(tester),
paints
..rrect()
..rrect()
..rrect(
rrect: childList.elementAt(1).parentData.surroundingRect,
color: CupertinoColors.activeBlue,
),
);
expect(getBackgroundColor(tester, 1), CupertinoColors.activeBlue);
});
testWidgets(
......@@ -404,36 +333,17 @@ void main() {
);
testWidgets('Passed in value is child initially selected',
(WidgetTester tester) async {
(WidgetTester tester) async {
await tester.pumpWidget(setupSimpleSegmentedControl());
expect(getRenderSegmentedControl(tester).selectedIndex, 0);
final dynamic childList =
getRenderSegmentedControl(tester).getChildrenAsList();
expect(
getRenderSegmentedControl(tester),
paints
..rrect(
rrect: childList.elementAt(0).parentData.surroundingRect,
color: CupertinoColors.activeBlue,
),
);
expect(
getRenderSegmentedControl(tester),
paints
..rrect()
..rrect()
..rrect(
rrect: childList.elementAt(1).parentData.surroundingRect,
color: CupertinoColors.white,
),
);
expect(getBackgroundColor(tester, 0), CupertinoColors.activeBlue);
expect(getBackgroundColor(tester, 1), CupertinoColors.white);
});
testWidgets('Null input for value results in no child initially selected',
(WidgetTester tester) async {
(WidgetTester tester) async {
final Map<int, Widget> children = <int, Widget>{};
children[0] = const Text('Child 1');
children[1] = const Text('Child 2');
......@@ -460,132 +370,42 @@ void main() {
expect(getRenderSegmentedControl(tester).selectedIndex, null);
final dynamic childList =
getRenderSegmentedControl(tester).getChildrenAsList();
expect(
getRenderSegmentedControl(tester),
paints
..rrect(
rrect: childList.elementAt(0).parentData.surroundingRect,
color: CupertinoColors.white,
),
);
expect(
getRenderSegmentedControl(tester),
paints
..rrect()
..rrect()
..rrect(
rrect: childList.elementAt(1).parentData.surroundingRect,
color: CupertinoColors.white,
),
);
expect(getBackgroundColor(tester, 0), CupertinoColors.white);
expect(getBackgroundColor(tester, 1), CupertinoColors.white);
});
testWidgets('Long press changes background color of not-selected child',
(WidgetTester tester) async {
(WidgetTester tester) async {
await tester.pumpWidget(setupSimpleSegmentedControl());
final dynamic childList =
getRenderSegmentedControl(tester).getChildrenAsList();
expect(
getRenderSegmentedControl(tester),
paints
..rrect(
rrect: childList.elementAt(0).parentData.surroundingRect,
color: CupertinoColors.activeBlue,
),
);
expect(
getRenderSegmentedControl(tester),
paints
..rrect()
..rrect()
..rrect(
rrect: childList.elementAt(1).parentData.surroundingRect,
color: CupertinoColors.white,
),
);
expect(getBackgroundColor(tester, 0), CupertinoColors.activeBlue);
expect(getBackgroundColor(tester, 1), CupertinoColors.white);
final Offset center = tester.getCenter(find.text('Child 2'));
await tester.startGesture(center);
await tester.pumpAndSettle();
expect(
getRenderSegmentedControl(tester),
paints
..rrect(
rrect: childList.elementAt(0).parentData.surroundingRect,
color: CupertinoColors.activeBlue,
),
);
expect(
getRenderSegmentedControl(tester),
paints
..rrect()
..rrect()
..rrect(
rrect: childList.elementAt(1).parentData.surroundingRect,
color: const Color(0x33007aff),
),
);
expect(getBackgroundColor(tester, 0), CupertinoColors.activeBlue);
expect(getBackgroundColor(tester, 1), const Color(0x33007aff));
});
testWidgets(
'Long press does not change background color of currently-selected child',
(WidgetTester tester) async {
testWidgets('Long press does not change background color of currently-selected child',
(WidgetTester tester) async {
await tester.pumpWidget(setupSimpleSegmentedControl());
final dynamic childList =
getRenderSegmentedControl(tester).getChildrenAsList();
expect(
getRenderSegmentedControl(tester),
paints
..rrect(
rrect: childList.elementAt(0).parentData.surroundingRect,
color: CupertinoColors.activeBlue,
),
);
expect(
getRenderSegmentedControl(tester),
paints
..rrect()
..rrect()
..rrect(
rrect: childList.elementAt(1).parentData.surroundingRect,
color: CupertinoColors.white,
),
);
expect(getBackgroundColor(tester, 0), CupertinoColors.activeBlue);
expect(getBackgroundColor(tester, 1), CupertinoColors.white);
final Offset center = tester.getCenter(find.text('Child 1'));
await tester.startGesture(center);
await tester.pumpAndSettle();
expect(
getRenderSegmentedControl(tester),
paints
..rrect(
rrect: childList.elementAt(0).parentData.surroundingRect,
color: CupertinoColors.activeBlue,
),
);
expect(
getRenderSegmentedControl(tester),
paints
..rrect()
..rrect()
..rrect(
rrect: childList.elementAt(1).parentData.surroundingRect,
color: CupertinoColors.white,
),
);
expect(getBackgroundColor(tester, 0), CupertinoColors.activeBlue);
expect(getBackgroundColor(tester, 1), CupertinoColors.white);
});
testWidgets('Height of segmented control is determined by tallest widget',
(WidgetTester tester) async {
(WidgetTester tester) async {
final Map<int, Widget> children = <int, Widget>{};
children[0] = new Container(
constraints: const BoxConstraints.tightFor(height: 100.0),
......@@ -611,16 +431,15 @@ void main() {
),
);
final RenderBox buttonBox = tester
.renderObject(find.byKey(const ValueKey<String>('Segmented Control')));
final RenderBox buttonBox = tester.renderObject(
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);
});
testWidgets('Width of each child widget is the same',
(WidgetTester tester) async {
testWidgets('Width of each child widget is the same', (WidgetTester tester) async {
final Map<int, Widget> children = <int, Widget>{};
children[0] = new Container();
children[1] = const Placeholder();
......@@ -640,23 +459,22 @@ void main() {
),
);
final RenderBox segmentedControl = tester
.renderObject(find.byKey(const ValueKey<String>('Segmented Control')));
final RenderBox segmentedControl = tester.renderObject(
find.byKey(const ValueKey<String>('Segmented Control')));
// Subtract the 16.0px from each side. Remaining width should be allocated
// to each child equally.
final double childWidth = (segmentedControl.size.width - 32.0) / 3;
final dynamic childList =
getRenderSegmentedControl(tester).getChildrenAsList();
for (dynamic child in childList) {
expect(childWidth, child.parentData.surroundingRect.width);
}
expect(childWidth,
getRenderSegmentedControl(tester).getChildrenAsList()[0].parentData.surroundingRect.width);
expect(childWidth,
getRenderSegmentedControl(tester).getChildrenAsList()[1].parentData.surroundingRect.width);
expect(childWidth,
getRenderSegmentedControl(tester).getChildrenAsList()[2].parentData.surroundingRect.width);
});
testWidgets('Width is finite in unbounded space',
(WidgetTester tester) async {
testWidgets('Width is finite in unbounded space', (WidgetTester tester) async {
final Map<int, Widget> children = <int, Widget>{};
children[0] = const Text('Child 1');
children[1] = const Text('Child 2');
......@@ -679,14 +497,14 @@ void main() {
),
);
final RenderBox segmentedControl = tester
.renderObject(find.byKey(const ValueKey<String>('Segmented Control')));
final RenderBox segmentedControl = tester.renderObject(
find.byKey(const ValueKey<String>('Segmented Control')));
expect(segmentedControl.size.width.isFinite, isTrue);
});
testWidgets('Directionality test - RTL should reverse order of widgets',
(WidgetTester tester) async {
(WidgetTester tester) async {
final Map<int, Widget> children = <int, Widget>{};
children[0] = const Text('Child 1');
children[1] = const Text('Child 2');
......@@ -703,14 +521,12 @@ void main() {
),
);
expect(
tester.getTopRight(find.text('Child 1')).dx >
tester.getTopRight(find.text('Child 2')).dx,
isTrue);
expect(tester.getTopRight(find.text('Child 1')).dx >
tester.getTopRight(find.text('Child 2')).dx, isTrue);
});
testWidgets('Correct initial selection and toggling behavior - RTL',
(WidgetTester tester) async {
(WidgetTester tester) async {
final Map<int, Widget> children = <int, Widget>{};
children[0] = const Text('Child 1');
children[1] = const Text('Child 2');
......@@ -738,63 +554,19 @@ void main() {
),
);
final dynamic childList =
getRenderSegmentedControl(tester).getChildrenAsList();
expect(
getRenderSegmentedControl(tester),
paints
..rrect(
rrect: childList.elementAt(0).parentData.surroundingRect,
color: CupertinoColors.activeBlue,
),
);
expect(
getRenderSegmentedControl(tester),
paints
..rrect()
..rrect()
..rrect(
rrect: childList.elementAt(1).parentData.surroundingRect,
color: CupertinoColors.white,
),
);
expect(getBackgroundColor(tester, 0), CupertinoColors.activeBlue);
expect(getBackgroundColor(tester, 1), CupertinoColors.white);
await tester.tap(find.text('Child 2'));
await tester.pump();
await tester.pumpAndSettle();
expect(
getRenderSegmentedControl(tester),
paints
..rrect(
rrect: childList.elementAt(0).parentData.surroundingRect,
color: CupertinoColors.white,
),
);
expect(
getRenderSegmentedControl(tester),
paints
..rrect()
..rrect()
..rrect(
rrect: childList.elementAt(1).parentData.surroundingRect,
color: CupertinoColors.activeBlue,
),
);
expect(getBackgroundColor(tester, 0), CupertinoColors.white);
expect(getBackgroundColor(tester, 1), CupertinoColors.activeBlue);
await tester.tap(find.text('Child 2'));
await tester.pump();
expect(
getRenderSegmentedControl(tester),
paints
..rrect()
..rrect()
..rrect(
rrect: childList.elementAt(1).parentData.surroundingRect,
color: CupertinoColors.activeBlue,
),
);
expect(getBackgroundColor(tester, 1), CupertinoColors.activeBlue);
});
testWidgets('Segmented control semantics', (WidgetTester tester) async {
......@@ -894,6 +666,504 @@ void main() {
semantics.dispose();
});
testWidgets('Non-centered taps work on smaller widgets', (WidgetTester tester) async {
final Map<int, Widget> children = <int, Widget>{};
children[0] = const Text('A');
children[1] = const Text('B');
int sharedValue = 1;
await tester.pumpWidget(
new StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return boilerplate(
child: new SegmentedControl<int>(
key: const ValueKey<String>('Segmented Control'),
children: children,
onValueChanged: (int newValue) {
setState(() {
sharedValue = newValue;
});
},
groupValue: sharedValue,
),
);
},
),
);
expect(sharedValue, 1);
final double childWidth = getRenderSegmentedControl(tester).firstChild.size.width;
final Offset centerOfSegmentedControl = tester.getCenter(find.text('A'));
// Tap just inside segment bounds
await tester.tapAt(new Offset(childWidth - 10.0, centerOfSegmentedControl.dy));
expect(sharedValue, 0);
});
testWidgets('Animation is correct when the selected segment changes',
(WidgetTester tester) async {
await tester.pumpWidget(setupSimpleSegmentedControl());
await tester.tap(find.text('Child 2'));
await tester.pump();
expect(getBackgroundColor(tester, 0), CupertinoColors.activeBlue);
expect(getBackgroundColor(tester, 1), const Color(0x33007aff));
await tester.pump(const Duration(milliseconds: 40));
expect(getBackgroundColor(tester, 0), const Color(0xff3d9aff));
expect(getBackgroundColor(tester, 1), const Color(0x64007aff));
await tester.pump(const Duration(milliseconds: 40));
expect(getBackgroundColor(tester, 0), const Color(0xff7bbaff));
expect(getBackgroundColor(tester, 1), const Color(0x95007aff));
await tester.pump(const Duration(milliseconds: 40));
expect(getBackgroundColor(tester, 0), const Color(0xffb9daff));
expect(getBackgroundColor(tester, 1), const Color(0xc7007aff));
await tester.pump(const Duration(milliseconds: 40));
expect(getBackgroundColor(tester, 0), const Color(0xfff7faff));
expect(getBackgroundColor(tester, 1), const Color(0xf8007aff));
await tester.pump(const Duration(milliseconds: 40));
expect(getBackgroundColor(tester, 0), CupertinoColors.white);
expect(getBackgroundColor(tester, 1), CupertinoColors.activeBlue);
});
testWidgets('Animation is correct when widget is rebuilt', (WidgetTester tester) async {
final Map<int, Widget> children = <int, Widget>{};
children[0] = const Text('Child 1');
children[1] = const Text('Child 2');
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,
),
);
},
),
);
await tester.tap(find.text('Child 2'));
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,
),
);
},
),
);
expect(getBackgroundColor(tester, 0), CupertinoColors.activeBlue);
expect(getBackgroundColor(tester, 1), const Color(0x33007aff));
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,
),
);
},
),
const Duration(milliseconds: 40),
);
expect(getBackgroundColor(tester, 0), const Color(0xff3d9aff));
expect(getBackgroundColor(tester, 1), const Color(0x64007aff));
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,
),
);
},
),
const Duration(milliseconds: 40),
);
expect(getBackgroundColor(tester, 0), const Color(0xff7bbaff));
expect(getBackgroundColor(tester, 1), const Color(0x95007aff));
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,
),
);
},
),
const Duration(milliseconds: 40),
);
expect(getBackgroundColor(tester, 0), const Color(0xffb9daff));
expect(getBackgroundColor(tester, 1), const Color(0xc7007aff));
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,
),
);
},
),
const Duration(milliseconds: 40),
);
expect(getBackgroundColor(tester, 0), const Color(0xfff7faff));
expect(getBackgroundColor(tester, 1), const Color(0xf8007aff));
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,
),
);
},
),
const Duration(milliseconds: 40),
);
expect(getBackgroundColor(tester, 0), CupertinoColors.white);
expect(getBackgroundColor(tester, 1), CupertinoColors.activeBlue);
});
testWidgets('Multiple segments are pressed', (WidgetTester tester) async {
final Map<int, Widget> children = <int, Widget>{};
children[0] = const Text('A');
children[1] = const Text('B');
children[2] = const Text('C');
int sharedValue = 0;
await tester.pumpWidget(
new StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return boilerplate(
child: new SegmentedControl<int>(
key: const ValueKey<String>('Segmented Control'),
children: children,
onValueChanged: (int newValue) {
setState(() {
sharedValue = newValue;
});
},
groupValue: sharedValue,
),
);
},
),
);
expect(getBackgroundColor(tester, 1), CupertinoColors.white);
await tester.startGesture(tester.getCenter(find.text('B')));
await tester.pumpAndSettle(const Duration(milliseconds: 200));
expect(getBackgroundColor(tester, 1), const Color(0x33007aff));
expect(getBackgroundColor(tester, 2), CupertinoColors.white);
await tester.startGesture(tester.getCenter(find.text('C')));
await tester.pumpAndSettle(const Duration(milliseconds: 200));
// Press on C has no effect while B is held down.
expect(getBackgroundColor(tester, 1), const Color(0x33007aff));
expect(getBackgroundColor(tester, 2), CupertinoColors.white);
});
testWidgets('Transition is triggered while a transition is already occurring',
(WidgetTester tester) async {
final Map<int, Widget> children = <int, Widget>{};
children[0] = const Text('A');
children[1] = const Text('B');
children[2] = const Text('C');
int sharedValue = 0;
await tester.pumpWidget(
new StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return boilerplate(
child: new SegmentedControl<int>(
key: const ValueKey<String>('Segmented Control'),
children: children,
onValueChanged: (int newValue) {
setState(() {
sharedValue = newValue;
});
},
groupValue: sharedValue,
),
);
},
),
);
await tester.tap(find.text('B'));
await tester.pump();
expect(getBackgroundColor(tester, 0), CupertinoColors.activeBlue);
expect(getBackgroundColor(tester, 1), const Color(0x33007aff));
await tester.pump(const Duration(milliseconds: 40));
expect(getBackgroundColor(tester, 0), const Color(0xff3d9aff));
expect(getBackgroundColor(tester, 1), const Color(0x64007aff));
// While A to B transition is occurring, press on C.
await tester.tap(find.text('C'));
await tester.pump();
// A and B are now both transitioning to white.
expect(getBackgroundColor(tester, 0), const Color(0xff3d9aff));
expect(getBackgroundColor(tester, 1), const Color(0xffc1deff));
expect(getBackgroundColor(tester, 2), const Color(0x33007aff));
await tester.pump(const Duration(milliseconds: 40));
// B background color has reached unselected state.
expect(getBackgroundColor(tester, 0), const Color(0xff7bbaff));
expect(getBackgroundColor(tester, 1), CupertinoColors.white);
expect(getBackgroundColor(tester, 2), const Color(0x64007aff));
await tester.pump(const Duration(milliseconds: 100));
// A background color has reached unselected state.
expect(getBackgroundColor(tester, 0), CupertinoColors.white);
expect(getBackgroundColor(tester, 2), const Color(0xe0007aff));
await tester.pump(const Duration(milliseconds: 40));
// C background color has reached selected state.
expect(getBackgroundColor(tester, 2), CupertinoColors.activeBlue);
});
testWidgets('Segment is selected while it is transitioning to unselected state',
(WidgetTester tester) async {
await tester.pumpWidget(setupSimpleSegmentedControl());
await tester.tap(find.text('Child 2'));
await tester.pump();
expect(getBackgroundColor(tester, 0), CupertinoColors.activeBlue);
expect(getBackgroundColor(tester, 1), const Color(0x33007aff));
await tester.pump(const Duration(milliseconds: 40));
expect(getBackgroundColor(tester, 0), const Color(0xff3d9aff));
expect(getBackgroundColor(tester, 1), const Color(0x64007aff));
// While A to B transition is occurring, press on A again.
await tester.tap(find.text('Child 1'));
await tester.pump();
// Both transitions start to reverse.
expect(getBackgroundColor(tester, 0), const Color(0xcd007aff));
expect(getBackgroundColor(tester, 1), const Color(0xffc1deff));
await tester.pump(const Duration(milliseconds: 40));
// A and B finish transitioning.
expect(getBackgroundColor(tester, 0), CupertinoColors.activeBlue);
expect(getBackgroundColor(tester, 1), CupertinoColors.white);
});
testWidgets('Add segment while animation is running', (WidgetTester tester) async {
Map<int, Widget> children = <int, Widget>{};
children[0] = const Text('A');
children[1] = const Text('B');
children[2] = const Text('C');
int sharedValue = 0;
await tester.pumpWidget(
new StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return boilerplate(
child: new SegmentedControl<int>(
key: const ValueKey<String>('Segmented Control'),
children: children,
onValueChanged: (int newValue) {
setState(() {
sharedValue = newValue;
});
if (sharedValue == 1) {
children = new Map<int, Widget>.from(children);
children[3] = const Text('D');
}
},
groupValue: sharedValue,
),
);
},
),
);
await tester.tap(find.text('B'));
await tester.pump();
expect(getBackgroundColor(tester, 0), const Color(0xff007aff));
expect(getBackgroundColor(tester, 1), const Color(0x33007aff));
expect(getBackgroundColor(tester, 3), CupertinoColors.white);
await tester.pump(const Duration(milliseconds: 40));
expect(getBackgroundColor(tester, 0), const Color(0xff3d9aff));
expect(getBackgroundColor(tester, 1), const Color(0x64007aff));
expect(getBackgroundColor(tester, 3), CupertinoColors.white);
await tester.pump(const Duration(milliseconds: 150));
expect(getBackgroundColor(tester, 0), CupertinoColors.white);
expect(getBackgroundColor(tester, 1), CupertinoColors.activeBlue);
expect(getBackgroundColor(tester, 3), CupertinoColors.white);
});
testWidgets('Remove segment while animation is running', (WidgetTester tester) async {
Map<int, Widget> children = <int, Widget>{};
children[0] = const Text('A');
children[1] = const Text('B');
children[2] = const Text('C');
int sharedValue = 0;
await tester.pumpWidget(
new StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return boilerplate(
child: new SegmentedControl<int>(
key: const ValueKey<String>('Segmented Control'),
children: children,
onValueChanged: (int newValue) {
setState(() {
sharedValue = newValue;
});
if (sharedValue == 1) {
children.remove(2);
children = new Map<int, Widget>.from(children);
}
},
groupValue: sharedValue,
),
);
},
),
);
expect(getRenderSegmentedControl(tester).getChildrenAsList().length, 3);
await tester.tap(find.text('B'));
await tester.pump();
expect(getBackgroundColor(tester, 1), const Color(0x33007aff));
expect(getRenderSegmentedControl(tester).getChildrenAsList().length, 2);
await tester.pump(const Duration(milliseconds: 40));
expect(getBackgroundColor(tester, 1), const Color(0x64007aff));
await tester.pump(const Duration(milliseconds: 150));
expect(getBackgroundColor(tester, 1), CupertinoColors.activeBlue);
});
testWidgets('Remove currently animating segment', (WidgetTester tester) async {
Map<int, Widget> children = <int, Widget>{};
children[0] = const Text('A');
children[1] = const Text('B');
children[2] = const Text('C');
int sharedValue = 0;
await tester.pumpWidget(
new StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return boilerplate(
child: new SegmentedControl<int>(
key: const ValueKey<String>('Segmented Control'),
children: children,
onValueChanged: (int newValue) {
setState(() {
sharedValue = newValue;
});
if (sharedValue == 1) {
children.remove(1);
children = new Map<int, Widget>.from(children);
sharedValue = null;
}
},
groupValue: sharedValue,
),
);
},
),
);
expect(getRenderSegmentedControl(tester).getChildrenAsList().length, 3);
await tester.tap(find.text('B'));
await tester.pump();
expect(getRenderSegmentedControl(tester).getChildrenAsList().length, 2);
await tester.pump(const Duration(milliseconds: 40));
expect(getBackgroundColor(tester, 0), const Color(0xff3d9aff));
expect(getBackgroundColor(tester, 1), CupertinoColors.white);
await tester.pump(const Duration(milliseconds: 40));
expect(getBackgroundColor(tester, 0), const Color(0xff7bbaff));
expect(getBackgroundColor(tester, 1), CupertinoColors.white);
await tester.pump(const Duration(milliseconds: 100));
expect(getBackgroundColor(tester, 0), CupertinoColors.white);
expect(getBackgroundColor(tester, 1), CupertinoColors.white);
});
testWidgets('Golden Test Placeholder Widget', (WidgetTester tester) async {
// Different machines render this content differently. Since the golden
// files are rendered on MacOS, this test should only be run on MacOS.
......
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