Update CupertinoSlidingSegmentedControl control/feedback mechanism (#43932)

This commit is contained in:
LongCatIsLooong 2019-10-31 19:35:52 -07:00 committed by GitHub
parent a192e29603
commit 3cd8c3142c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 313 additions and 376 deletions

View file

@ -94,56 +94,12 @@ class _FontWeightTween extends Tween<FontWeight> {
/// argument must be an ordered [Map] such as a [LinkedHashMap], the ordering of /// argument must be an ordered [Map] such as a [LinkedHashMap], the ordering of
/// the keys will determine the order of the widgets in the segmented control. /// the keys will determine the order of the widgets in the segmented control.
/// ///
/// When the state of the segmented control changes, the widget changes the /// When the state of the segmented control changes, the widget calls the
/// [controller]'s value to the map key associated with the newly selected widget, /// [onValueChanged] callback. The map key associated with the newly selected
/// causing all of its listeners to be notified. /// widget is returned in the [onValueChanged] callback. Typically, widgets
/// /// that use a segmented control will listen for the [onValueChanged] callback
/// {@tool dartpad --template=stateful_widget_material} /// and rebuild the segmented control with a new [groupValue] to update which
/// /// option is currently selected.
/// This sample shows two [CupertinoSlidingSegmentedControl]s that mirror each other.
///
/// ```dart
/// final Map<int, Widget> children = const <int, Widget>{
/// 0: Text('Child 1'),
/// 1: Text('Child 2'),
/// 2: Text('Child 3'),
/// };
///
/// // No segment is initially selected because the controller's value is null.
/// final ValueNotifier<int> controller = ValueNotifier<int>(null);
///
/// @override
/// void initState() {
/// super.initState();
/// // Prints a message whenever the currently selected widget changes.
/// controller.addListener(() { print('selected: ${controller.value}'); });
/// }
///
/// @override
/// Widget build(BuildContext context) {
/// return Center(
/// child: Column(
/// children: <Widget>[
/// CupertinoSlidingSegmentedControl<int>(
/// children: children,
/// controller: controller,
/// ),
/// CupertinoSlidingSegmentedControl<int>(
/// children: children,
/// controller: controller,
/// ),
/// ],
/// ),
/// );
/// }
///
/// @override
/// void dispose() {
/// controller.dispose();
/// super.dispose();
/// }
/// ```
/// {@end-tool}
/// ///
/// The [children] will be displayed in the order of the keys in the [Map]. /// The [children] will be displayed in the order of the keys in the [Map].
/// The height of the segmented control is determined by the height of the /// The height of the segmented control is determined by the height of the
@ -166,33 +122,34 @@ class _FontWeightTween extends Tween<FontWeight> {
class CupertinoSlidingSegmentedControl<T> extends StatefulWidget { class CupertinoSlidingSegmentedControl<T> extends StatefulWidget {
/// Creates an iOS-style segmented control bar. /// Creates an iOS-style segmented control bar.
/// ///
/// The [children] and [controller] arguments must not be null. The [children] /// The [children] and [onValueChanged] arguments must not be null. The
/// argument must be an ordered [Map] such as a [LinkedHashMap]. Further, the /// [children] argument must be an ordered [Map] such as a [LinkedHashMap].
/// length of the [children] list must be greater than one. /// Further, the length of the [children] list must be greater than one.
/// ///
/// Each widget value in the map of [children] must have an associated [Map] key /// Each widget value in the map of [children] must have an associated key
/// of type [T] that uniquely identifies this widget. This key will become the /// that uniquely identifies this widget. This key is what will be returned
/// [controller]'s new value, when the corresponding child widget from the /// in the [onValueChanged] callback when a new value from the [children] map
/// [children] map is selected. /// is selected.
/// ///
/// The [controller]'s [ValueNotifier.value] is the currently selected value for /// The [groupValue] is the currently selected value for the segmented control.
/// the segmented control. If it is null, no widget will appear as selected. The /// If no [groupValue] is provided, or the [groupValue] is null, no widget will
/// [controller]'s value must be either null or one of the keys in the [children] /// appear as selected. The [groupValue] must be either null or one of the keys
/// map. /// in the [children] map.
CupertinoSlidingSegmentedControl({ CupertinoSlidingSegmentedControl({
Key key, Key key,
@required this.children, @required this.children,
@required this.controller, @required this.onValueChanged,
this.groupValue,
this.thumbColor = _kThumbColor, this.thumbColor = _kThumbColor,
this.padding = _kHorizontalItemPadding, this.padding = _kHorizontalItemPadding,
this.backgroundColor = CupertinoColors.tertiarySystemFill, this.backgroundColor = CupertinoColors.tertiarySystemFill,
}) : assert(children != null), }) : assert(children != null),
assert(children.length >= 2), assert(children.length >= 2),
assert(padding != null), assert(padding != null),
assert(controller != null), assert(onValueChanged != null),
assert( assert(
controller.value == null || children.keys.any((T child) => child == controller.value), groupValue == null || children.keys.contains(groupValue),
"The controller's value must be either null or one of the keys in the children map.", 'The groupValue must be either null or one of the keys in the children map.',
), ),
super(key: key); super(key: key);
@ -203,16 +160,58 @@ class CupertinoSlidingSegmentedControl<T> extends StatefulWidget {
/// This attribute must be an ordered [Map] such as a [LinkedHashMap]. /// This attribute must be an ordered [Map] such as a [LinkedHashMap].
final Map<T, Widget> children; final Map<T, Widget> children;
/// A [ValueNotifier]<[T]> that controls the currently selected child. /// The identifier of the widget that is currently selected.
/// ///
/// Its value must be one of the keys in the [Map] of [children], or null, in /// This must be one of the keys in the [Map] of [children].
/// which case no widget will be selected. /// If this attribute is null, no widget will be initially selected.
final T groupValue;
/// The callback that is called when a new option is tapped.
/// ///
/// The [controller]'s value changes when the user drags the thumb to a different /// This attribute must not be null.
/// child widget, or taps on a different child widget. Its value can also be ///
/// changed programmatically, in which case all sliding animations will play as /// The segmented control passes the newly selected widget's associated key
/// if the new selected child widget was tapped on. /// to the callback but does not actually change state until the parent
final ValueNotifier<T> controller; /// widget rebuilds the segmented control with the new [groupValue].
///
/// The callback provided to [onValueChanged] should update the state of
/// the parent [StatefulWidget] using the [State.setState] method, so that
/// the parent gets rebuilt; for example:
///
/// {@tool sample}
///
/// ```dart
/// class SegmentedControlExample extends StatefulWidget {
/// @override
/// State createState() => SegmentedControlExampleState();
/// }
///
/// class SegmentedControlExampleState extends State<SegmentedControlExample> {
/// final Map<int, Widget> children = const {
/// 0: Text('Child 1'),
/// 1: Text('Child 2'),
/// };
///
/// int currentValue;
///
/// @override
/// Widget build(BuildContext context) {
/// return Container(
/// child: CupertinoSlidingSegmentedControl<int>(
/// children: children,
/// onValueChanged: (int newValue) {
/// setState(() {
/// currentValue = newValue;
/// });
/// },
/// groupValue: currentValue,
/// ),
/// );
/// }
/// }
/// ```
/// {@end-tool}
final ValueChanged<T> onValueChanged;
/// The color used to paint the rounded rect behind the [children] and the separators. /// The color used to paint the rounded rect behind the [children] and the separators.
/// ///
@ -240,7 +239,7 @@ class _SegmentedControlState<T> extends State<CupertinoSlidingSegmentedControl<T
with TickerProviderStateMixin<CupertinoSlidingSegmentedControl<T>> { with TickerProviderStateMixin<CupertinoSlidingSegmentedControl<T>> {
final Map<T, AnimationController> _highlightControllers = <T, AnimationController>{}; final Map<T, AnimationController> _highlightControllers = <T, AnimationController>{};
final Tween<FontWeight> _highlightTween = _FontWeightTween(begin: FontWeight.normal, end: FontWeight.w600); final Tween<FontWeight> _highlightTween = _FontWeightTween(begin: FontWeight.normal, end: FontWeight.w500);
final Map<T, AnimationController> _pressControllers = <T, AnimationController>{}; final Map<T, AnimationController> _pressControllers = <T, AnimationController>{};
final Tween<double> _pressTween = Tween<double>(begin: 1, end: 0.2); final Tween<double> _pressTween = Tween<double>(begin: 1, end: 0.2);
@ -255,8 +254,6 @@ class _SegmentedControlState<T> extends State<CupertinoSlidingSegmentedControl<T
final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer(); final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer();
final LongPressGestureRecognizer longPress = LongPressGestureRecognizer(); final LongPressGestureRecognizer longPress = LongPressGestureRecognizer();
ValueNotifier<T> controller;
AnimationController _createHighlightAnimationController({ bool isCompleted = false }) { AnimationController _createHighlightAnimationController({ bool isCompleted = false }) {
return AnimationController( return AnimationController(
duration: _kHighlightAnimationDuration, duration: _kHighlightAnimationDuration,
@ -284,9 +281,7 @@ class _SegmentedControlState<T> extends State<CupertinoSlidingSegmentedControl<T
drag.team = team; drag.team = team;
team.captain = drag; team.captain = drag;
controller = widget.controller; _highlighted = widget.groupValue;
controller.addListener(_didChangeControllerValue);
_highlighted = controller.value;
thumbController = AnimationController( thumbController = AnimationController(
duration: _kSpringAnimationDuration, duration: _kSpringAnimationDuration,
@ -308,7 +303,7 @@ class _SegmentedControlState<T> extends State<CupertinoSlidingSegmentedControl<T
for (T currentKey in widget.children.keys) { for (T currentKey in widget.children.keys) {
_highlightControllers[currentKey] = _createHighlightAnimationController( _highlightControllers[currentKey] = _createHighlightAnimationController(
isCompleted: currentKey == controller.value, // Highlight the current selection. isCompleted: currentKey == widget.groupValue, // Highlight the current selection.
); );
_pressControllers[currentKey] = _createFadeoutAnimationController(); _pressControllers[currentKey] = _createFadeoutAnimationController();
} }
@ -336,15 +331,7 @@ class _SegmentedControlState<T> extends State<CupertinoSlidingSegmentedControl<T
} }
} }
if (controller != widget.controller) { highlighted = widget.groupValue;
controller.removeListener(_didChangeControllerValue);
controller = widget.controller;
controller.addListener(_didChangeControllerValue);
}
if (controller.value != oldWidget.controller.value) {
highlighted = widget.controller.value;
}
} }
@override @override
@ -368,18 +355,6 @@ class _SegmentedControlState<T> extends State<CupertinoSlidingSegmentedControl<T
super.dispose(); super.dispose();
} }
void _didChangeControllerValue() {
assert(
controller.value == null || widget.children.keys.contains(controller.value),
"The controller's value ${controller.value} must be either null "
'or one of the keys in the children map: ${widget.children.keys}',
);
setState(() {
// Mark the state as dirty.
});
}
// Play highlight animation for the child located at _highlightControllers[at]. // Play highlight animation for the child located at _highlightControllers[at].
void _animateHighlightController({ T at, bool forward }) { void _animateHighlightController({ T at, bool forward }) {
if (at == null) if (at == null)
@ -413,7 +388,7 @@ class _SegmentedControlState<T> extends State<CupertinoSlidingSegmentedControl<T
} }
void didChangeSelectedViaGesture() { void didChangeSelectedViaGesture() {
controller.value = _highlighted; widget.onValueChanged(_highlighted);
} }
T indexToKey(int index) => index == null ? null : keys[index]; T indexToKey(int index) => index == null ? null : keys[index];
@ -447,9 +422,9 @@ class _SegmentedControlState<T> extends State<CupertinoSlidingSegmentedControl<T
style: textStyle, style: textStyle,
child: Semantics( child: Semantics(
button: true, button: true,
onTap: () { controller.value = currentKey; }, onTap: () { widget.onValueChanged(currentKey); },
inMutuallyExclusiveGroup: true, inMutuallyExclusiveGroup: true,
selected: controller.value == currentKey, selected: widget.groupValue == currentKey,
child: Opacity( child: Opacity(
opacity: _pressTween.evaluate(_pressControllers[currentKey]), opacity: _pressTween.evaluate(_pressControllers[currentKey]),
// Expand the hitTest area to be as large as the Opacity widget. // Expand the hitTest area to be as large as the Opacity widget.
@ -464,7 +439,7 @@ class _SegmentedControlState<T> extends State<CupertinoSlidingSegmentedControl<T
children.add(child); children.add(child);
} }
final int selectedIndex = controller.value == null ? null : keys.indexOf(controller.value); final int selectedIndex = widget.groupValue == null ? null : keys.indexOf(widget.groupValue);
final Widget box = _SegmentedControlRenderWidget<T>( final Widget box = _SegmentedControlRenderWidget<T>(
children: children, children: children,

View file

@ -36,32 +36,51 @@ Widget setupSimpleSegmentedControl() {
0: Text('Child 1'), 0: Text('Child 1'),
1: Text('Child 2'), 1: Text('Child 2'),
}; };
final ValueNotifier<int> controller = ValueNotifier<int>(0);
return boilerplate( return boilerplate(
child: CupertinoSlidingSegmentedControl<int>( builder: (BuildContext context) {
children: children, return CupertinoSlidingSegmentedControl<int>(
controller: controller, children: children,
groupValue: groupValue,
onValueChanged: defaultCallback,
);
},
);
}
StateSetter setState;
int groupValue = 0;
void defaultCallback(int newValue) {
setState(() { groupValue = newValue; });
}
Widget boilerplate({ WidgetBuilder builder }) {
return Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: StatefulBuilder(builder: (BuildContext context, StateSetter setter) {
setState = setter;
return builder(context);
}),
), ),
); );
} }
Widget boilerplate({ Widget child }) {
return Directionality(
textDirection: TextDirection.ltr,
child: Center(child: child),
);
}
void main() { void main() {
testWidgets('Children and controller and padding arguments can not be null', (WidgetTester tester) async {
setUp(() {
setState = null;
groupValue = 0;
});
testWidgets('Children and onValueChanged and padding arguments can not be null', (WidgetTester tester) async {
groupValue = null;
try { try {
await tester.pumpWidget( await tester.pumpWidget(
boilerplate( CupertinoSlidingSegmentedControl<int>(
child: CupertinoSlidingSegmentedControl<int>( children: null,
children: null, groupValue: groupValue,
controller: ValueNotifier<int>(null), onValueChanged: defaultCallback,
),
), ),
); );
fail('Should not be possible to create segmented control with null children'); fail('Should not be possible to create segmented control with null children');
@ -76,26 +95,24 @@ void main() {
try { try {
await tester.pumpWidget( await tester.pumpWidget(
boilerplate( CupertinoSlidingSegmentedControl<int>(
child: CupertinoSlidingSegmentedControl<int>( children: children,
children: children, groupValue: groupValue,
controller: null, onValueChanged: null,
),
), ),
); );
fail('Should not be possible to create segmented control without a controller'); fail('Should not be possible to create segmented control without an onValueChanged');
} on AssertionError catch (e) { } on AssertionError catch (e) {
expect(e.toString(), contains('controller')); expect(e.toString(), contains('onValueChanged'));
} }
try { try {
await tester.pumpWidget( await tester.pumpWidget(
boilerplate( CupertinoSlidingSegmentedControl<int>(
child: CupertinoSlidingSegmentedControl<int>( children: children,
children: children, groupValue: groupValue,
controller: ValueNotifier<int>(null), onValueChanged: defaultCallback,
padding: null, padding: null,
),
), ),
); );
fail('Should not be possible to create segmented control with null padding'); fail('Should not be possible to create segmented control with null padding');
@ -106,13 +123,13 @@ void main() {
testWidgets('Need at least 2 children', (WidgetTester tester) async { testWidgets('Need at least 2 children', (WidgetTester tester) async {
final Map<int, Widget> children = <int, Widget>{}; final Map<int, Widget> children = <int, Widget>{};
groupValue = null;
try { try {
await tester.pumpWidget( await tester.pumpWidget(
boilerplate( CupertinoSlidingSegmentedControl<int>(
child: CupertinoSlidingSegmentedControl<int>( children: children,
children: children, groupValue: groupValue,
controller: ValueNotifier<int>(null), onValueChanged: defaultCallback,
),
), ),
); );
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');
@ -123,11 +140,10 @@ void main() {
children[0] = const Text('Child 1'); children[0] = const Text('Child 1');
await tester.pumpWidget( await tester.pumpWidget(
boilerplate( CupertinoSlidingSegmentedControl<int>(
child: CupertinoSlidingSegmentedControl<int>( children: children,
children: children, groupValue: groupValue,
controller: ValueNotifier<int>(null), onValueChanged: defaultCallback,
),
), ),
); );
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');
@ -135,20 +151,20 @@ void main() {
expect(e.toString(), contains('children.length')); expect(e.toString(), contains('children.length'));
} }
groupValue = -1;
try { try {
children[1] = const Text('Child 2'); children[1] = const Text('Child 2');
children[2] = const Text('Child 3'); children[2] = const Text('Child 3');
await tester.pumpWidget( await tester.pumpWidget(
boilerplate( CupertinoSlidingSegmentedControl<int>(
child: CupertinoSlidingSegmentedControl<int>( children: children,
children: children, groupValue: groupValue,
controller: ValueNotifier<int>(-1), onValueChanged: defaultCallback,
),
), ),
); );
fail('Should not be possible to create a segmented control with a controller pointing to a non-existent child'); fail('Should not be possible to create a segmented control with a groupValue pointing to a non-existent child');
} on AssertionError catch (e) { } on AssertionError catch (e) {
expect(e.toString(), contains('value must be either null or one of the keys in the children map')); expect(e.toString(), contains('groupValue must be either null or one of the keys in the children map'));
} }
}); });
@ -185,11 +201,14 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
boilerplate( boilerplate(
child: CupertinoSlidingSegmentedControl<int>( builder: (BuildContext context) {
key: key, return CupertinoSlidingSegmentedControl<int>(
children: children, key: key,
controller: ValueNotifier<int>(null), children: children,
), groupValue: groupValue,
onValueChanged: defaultCallback,
);
},
), ),
); );
@ -204,12 +223,15 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
boilerplate( boilerplate(
child: CupertinoSlidingSegmentedControl<int>( builder: (BuildContext context) {
key: key, return CupertinoSlidingSegmentedControl<int>(
padding: const EdgeInsets.fromLTRB(1, 3, 5, 7), key: key,
children: children, padding: const EdgeInsets.fromLTRB(1, 3, 5, 7),
controller: ValueNotifier<int>(null), children: children,
), groupValue: groupValue,
onValueChanged: defaultCallback,
);
},
), ),
); );
@ -230,119 +252,29 @@ void main() {
2: Text('Child 3'), 2: Text('Child 3'),
}; };
final ValueNotifier<int> controller = ValueNotifier<int>(0);
await tester.pumpWidget( await tester.pumpWidget(
boilerplate( boilerplate(
child: CupertinoSlidingSegmentedControl<int>( builder: (BuildContext context) {
key: const ValueKey<String>('Segmented Control'), return CupertinoSlidingSegmentedControl<int>(
children: children, key: const ValueKey<String>('Segmented Control'),
controller: controller, children: children,
), groupValue: groupValue,
), onValueChanged: defaultCallback,
);
expect(controller.value, 0);
await tester.tap(find.text('Child 2'));
expect(controller.value, 1);
// Tapping the currently selected item should not change controller's value.
bool valueChanged = false;
controller.addListener(() { valueChanged = true; });
await tester.tap(find.text('Child 2'));
expect(valueChanged, isFalse);
expect(controller.value, 1);
});
testWidgets('Changing controller works', (WidgetTester tester) async {
const Map<int, Widget> children = <int, Widget>{
0: Text('Child 1'),
1: Text('Child 2'),
2: Text('Child 3'),
};
final ValueNotifier<int> controller = ValueNotifier<int>(0);
final ValueNotifier<int> newControlelr = ValueNotifier<int>(null);
await tester.pumpWidget(
boilerplate(
child: CupertinoSlidingSegmentedControl<int>(
key: const ValueKey<String>('Segmented Control'),
children: children,
controller: controller,
),
),
);
expect(
currentUnscaledThumbRect(tester, useGlobalCoordinate: true).center,
offsetMoreOrLessEquals(tester.getCenter(find.text('Child 1'))),
);
await tester.pumpWidget(
boilerplate(
child: CupertinoSlidingSegmentedControl<int>(
key: const ValueKey<String>('Segmented Control'),
children: children,
controller: newControlelr,
),
),
);
expect(
currentUnscaledThumbRect(tester, useGlobalCoordinate: true),
isNull,
);
});
testWidgets('Can change controller value in build method', (WidgetTester tester) async {
const Map<int, Widget> children = <int, Widget>{
0: Text('Child 1'),
1: Text('Child 2'),
2: Text('Child 3'),
};
int currentIndex = 0;
StateSetter setState;
final ValueNotifier<int> controller = ValueNotifier<int>(currentIndex);
await tester.pumpWidget(
StatefulBuilder(
builder: (BuildContext context, StateSetter setter) {
setState = setter;
if (controller.value != currentIndex)
controller.value = currentIndex;
return boilerplate(
child: CupertinoSlidingSegmentedControl<int>(
key: const ValueKey<String>('Segmented Control'),
children: children,
controller: controller,
),
); );
}, },
), ),
); );
expect( expect(groupValue, 0);
currentUnscaledThumbRect(tester, useGlobalCoordinate: true).center,
offsetMoreOrLessEquals(tester.getCenter(find.text('Child 1'))),
);
setState(() { await tester.tap(find.text('Child 2'));
currentIndex = 2;
});
await tester.pump(); expect(groupValue, 1);
await tester.pumpAndSettle();
expect( // Tapping the currently selected item should not change groupValue.
currentUnscaledThumbRect(tester, useGlobalCoordinate: true).center, await tester.tap(find.text('Child 2'));
offsetMoreOrLessEquals(tester.getCenter(find.text('Child 3')), epsilon: 0.01),
); expect(groupValue, 1);
}); });
testWidgets( testWidgets(
@ -353,16 +285,15 @@ void main() {
1: Icon(IconData(1)), 1: Icon(IconData(1)),
}; };
final ValueNotifier<int> controller = ValueNotifier<int>(0);
await tester.pumpWidget( await tester.pumpWidget(
CupertinoApp( CupertinoApp(
theme: const CupertinoThemeData(brightness: Brightness.dark), theme: const CupertinoThemeData(brightness: Brightness.dark),
home: StatefulBuilder( home: boilerplate(
builder: (BuildContext context, StateSetter setState) { builder: (BuildContext context) {
return CupertinoSlidingSegmentedControl<int>( return CupertinoSlidingSegmentedControl<int>(
children: children, children: children,
controller: controller, groupValue: groupValue,
onValueChanged: defaultCallback,
); );
}, },
), ),
@ -371,7 +302,7 @@ void main() {
DefaultTextStyle textStyle = tester.widget(find.widgetWithText(DefaultTextStyle, 'Child 1').first); DefaultTextStyle textStyle = tester.widget(find.widgetWithText(DefaultTextStyle, 'Child 1').first);
expect(textStyle.style.fontWeight, FontWeight.w600); expect(textStyle.style.fontWeight, FontWeight.w500);
await tester.tap(find.byIcon(const IconData(1))); await tester.tap(find.byIcon(const IconData(1)));
await tester.pump(); await tester.pump();
@ -379,6 +310,7 @@ void main() {
textStyle = tester.widget(find.widgetWithText(DefaultTextStyle, 'Child 1').first); textStyle = tester.widget(find.widgetWithText(DefaultTextStyle, 'Child 1').first);
expect(groupValue, 1);
expect(textStyle.style.fontWeight, FontWeight.normal); expect(textStyle.style.fontWeight, FontWeight.normal);
}, },
); );
@ -389,7 +321,6 @@ void main() {
1: Icon(IconData(1)), 1: Icon(IconData(1)),
}; };
final ValueNotifier<int> controller = ValueNotifier<int>(0);
Brightness brightness = Brightness.light; Brightness brightness = Brightness.light;
StateSetter setState; StateSetter setState;
@ -400,12 +331,15 @@ void main() {
return MediaQuery( return MediaQuery(
data: MediaQueryData(platformBrightness: brightness), data: MediaQueryData(platformBrightness: brightness),
child: boilerplate( child: boilerplate(
child: CupertinoSlidingSegmentedControl<int>( builder: (BuildContext context) {
children: children, return CupertinoSlidingSegmentedControl<int>(
controller: controller, children: children,
thumbColor: CupertinoColors.systemGreen, groupValue: groupValue,
backgroundColor: CupertinoColors.systemRed, onValueChanged: defaultCallback,
), thumbColor: CupertinoColors.systemGreen,
backgroundColor: CupertinoColors.systemRed,
);
},
), ),
); );
}, },
@ -443,14 +377,15 @@ void main() {
2: Placeholder(), 2: Placeholder(),
}; };
final ValueNotifier<int> controller = ValueNotifier<int>(0);
await tester.pumpWidget( await tester.pumpWidget(
boilerplate( boilerplate(
child: CupertinoSlidingSegmentedControl<int>( builder: (BuildContext context) {
children: children, return CupertinoSlidingSegmentedControl<int>(
controller: controller, children: children,
), groupValue: groupValue,
onValueChanged: defaultCallback,
);
},
), ),
); );
}, },
@ -468,16 +403,18 @@ void main() {
1: Text('Child 2'), 1: Text('Child 2'),
}; };
final ValueNotifier<int> controller = ValueNotifier<int>(null); groupValue = null;
await tester.pumpWidget( await tester.pumpWidget(
StatefulBuilder( StatefulBuilder(
builder: (BuildContext context, StateSetter setState) { builder: (BuildContext context, StateSetter setState) {
return boilerplate( return boilerplate(
child: CupertinoSlidingSegmentedControl<int>( builder: (BuildContext context) {
children: children, return CupertinoSlidingSegmentedControl<int>(
controller: controller, children: children,
), groupValue: groupValue,
onValueChanged: defaultCallback,
);
},
); );
}, },
), ),
@ -496,14 +433,17 @@ void main() {
}; };
// Child 3 is intially selected. // Child 3 is intially selected.
final ValueNotifier<int> controller = ValueNotifier<int>(2); groupValue = 2;
await tester.pumpWidget( await tester.pumpWidget(
boilerplate( boilerplate(
child: CupertinoSlidingSegmentedControl<int>( builder: (BuildContext context) {
children: children, return CupertinoSlidingSegmentedControl<int>(
controller: controller, children: children,
), groupValue: groupValue,
onValueChanged: defaultCallback,
);
},
), ),
); );
@ -571,15 +511,16 @@ void main() {
2: Container(constraints: const BoxConstraints.tightFor(height: 200.0)), 2: Container(constraints: const BoxConstraints.tightFor(height: 200.0)),
}; };
final ValueNotifier<int> controller = ValueNotifier<int>(null);
await tester.pumpWidget( await tester.pumpWidget(
boilerplate( boilerplate(
child: CupertinoSlidingSegmentedControl<int>( builder: (BuildContext context) {
key: const ValueKey<String>('Segmented Control'), return CupertinoSlidingSegmentedControl<int>(
children: children, key: const ValueKey<String>('Segmented Control'),
controller: controller, children: children,
), groupValue: groupValue,
onValueChanged: defaultCallback,
);
},
), ),
); );
@ -600,14 +541,16 @@ void main() {
2: Container(constraints: const BoxConstraints.tightFor(width: 200.0)), 2: Container(constraints: const BoxConstraints.tightFor(width: 200.0)),
}; };
final ValueNotifier<int> controller = ValueNotifier<int>(null);
await tester.pumpWidget( await tester.pumpWidget(
boilerplate( boilerplate(
child: CupertinoSlidingSegmentedControl<int>( builder: (BuildContext context) {
key: const ValueKey<String>('Segmented Control'), return CupertinoSlidingSegmentedControl<int>(
children: children, key: const ValueKey<String>('Segmented Control'),
controller: controller, children: children,
), groupValue: groupValue,
onValueChanged: defaultCallback,
);
},
), ),
); );
@ -628,19 +571,20 @@ void main() {
1: SizedBox(width: 70), 1: SizedBox(width: 70),
}; };
final ValueNotifier<int> controller = ValueNotifier<int>(null);
await tester.pumpWidget( await tester.pumpWidget(
boilerplate( boilerplate(
child: Row( builder: (BuildContext context) {
children: <Widget>[ return Row(
CupertinoSlidingSegmentedControl<int>( children: <Widget>[
key: const ValueKey<String>('Segmented Control'), CupertinoSlidingSegmentedControl<int>(
children: children, key: const ValueKey<String>('Segmented Control'),
controller: controller, children: children,
), groupValue: groupValue,
], onValueChanged: defaultCallback,
), ),
],
);
},
), ),
); );
@ -660,14 +604,19 @@ void main() {
1: Text('Child 2'), 1: Text('Child 2'),
}; };
final ValueNotifier<int> controller = ValueNotifier<int>(null);
await tester.pumpWidget( await tester.pumpWidget(
Directionality( Directionality(
textDirection: TextDirection.rtl, textDirection: TextDirection.rtl,
child: Center( child: Center(
child: CupertinoSlidingSegmentedControl<int>( child: StatefulBuilder(
children: children, builder: (BuildContext context, StateSetter setter) {
controller: controller, setState = setter;
return CupertinoSlidingSegmentedControl<int>(
children: children,
groupValue: groupValue,
onValueChanged: defaultCallback,
);
},
), ),
), ),
), ),
@ -682,16 +631,19 @@ void main() {
0: Text('Child 1'), 0: Text('Child 1'),
1: Text('Child 2'), 1: Text('Child 2'),
}; };
final ValueNotifier<int> controller = ValueNotifier<int>(0);
await tester.pumpWidget( await tester.pumpWidget(
Directionality( Directionality(
textDirection: TextDirection.rtl, textDirection: TextDirection.rtl,
child: Center( child: Center(
child: CupertinoSlidingSegmentedControl<int>( child: StatefulBuilder(
children: children, builder: (BuildContext context, StateSetter setter) {
controller: controller, setState = setter;
return CupertinoSlidingSegmentedControl<int>(
children: children,
groupValue: groupValue,
onValueChanged: defaultCallback,
);
},
), ),
), ),
), ),
@ -718,17 +670,15 @@ void main() {
1: Text('Child 2'), 1: Text('Child 2'),
}; };
final ValueNotifier<int> controller = ValueNotifier<int>(0);
await tester.pumpWidget( await tester.pumpWidget(
Directionality( boilerplate(
textDirection: TextDirection.ltr, builder: (BuildContext context) {
child: Center( return CupertinoSlidingSegmentedControl<int>(
child: CupertinoSlidingSegmentedControl<int>(
children: children, children: children,
controller: controller, groupValue: groupValue,
), onValueChanged: defaultCallback,
), );
},
), ),
); );
@ -810,25 +760,26 @@ void main() {
children[0] = const Text('Child 1'); children[0] = const Text('Child 1');
children[1] = const SizedBox(); children[1] = const SizedBox();
final ValueNotifier<int> controller = ValueNotifier<int>(0);
await tester.pumpWidget( await tester.pumpWidget(
boilerplate( boilerplate(
child: CupertinoSlidingSegmentedControl<int>( builder: (BuildContext context) {
key: const ValueKey<String>('Segmented Control'), return CupertinoSlidingSegmentedControl<int>(
children: children, key: const ValueKey<String>('Segmented Control'),
controller: controller, children: children,
), groupValue: groupValue,
onValueChanged: defaultCallback,
);
},
), ),
); );
expect(controller.value, 0); expect(groupValue, 0);
final Offset centerOfTwo = tester.getCenter(find.byWidget(children[1])); final Offset centerOfTwo = tester.getCenter(find.byWidget(children[1]));
// Tap just inside segment bounds // Tap just inside segment bounds
await tester.tapAt(centerOfTwo + const Offset(10, 0)); await tester.tapAt(centerOfTwo + const Offset(10, 0));
expect(controller.value, 1); expect(groupValue, 1);
}); });
testWidgets('Thumb animation is correct when the selected segment changes', (WidgetTester tester) async { testWidgets('Thumb animation is correct when the selected segment changes', (WidgetTester tester) async {
@ -917,20 +868,23 @@ void main() {
1: Text('B'), 1: Text('B'),
2: Text('C'), 2: Text('C'),
}; };
final ValueNotifier<int> controller = ValueNotifier<int>(0);
await tester.pumpWidget( await tester.pumpWidget(
boilerplate( boilerplate(
child: CupertinoSlidingSegmentedControl<int>( builder: (BuildContext context) {
key: const ValueKey<String>('Segmented Control'), return CupertinoSlidingSegmentedControl<int>(
children: children, key: const ValueKey<String>('Segmented Control'),
controller: controller, children: children,
), groupValue: groupValue,
onValueChanged: defaultCallback,
);
},
), ),
); );
await tester.tap(find.text('B')); await tester.tap(find.text('B'));
await tester.pump(); await tester.pump();
await tester.pump();
await tester.pump(const Duration(milliseconds: 40)); await tester.pump(const Duration(milliseconds: 40));
// Between A and B. // Between A and B.
@ -965,15 +919,16 @@ void main() {
children[2] = const Text('C'); children[2] = const Text('C');
children[3] = const Text('D'); children[3] = const Text('D');
final ValueNotifier<int> controller = ValueNotifier<int>(0);
await tester.pumpWidget( await tester.pumpWidget(
boilerplate( boilerplate(
child: CupertinoSlidingSegmentedControl<int>( builder: (BuildContext context) {
key: const ValueKey<String>('Segmented Control'), return CupertinoSlidingSegmentedControl<int>(
children: children, key: const ValueKey<String>('Segmented Control'),
controller: controller, children: children,
), groupValue: groupValue,
onValueChanged: defaultCallback,
);
},
), ),
); );
@ -984,11 +939,14 @@ void main() {
children[1] = const Text('B'); children[1] = const Text('B');
await tester.pumpWidget( await tester.pumpWidget(
boilerplate( boilerplate(
child: CupertinoSlidingSegmentedControl<int>( builder: (BuildContext context) {
key: const ValueKey<String>('Segmented Control'), return CupertinoSlidingSegmentedControl<int>(
children: children, key: const ValueKey<String>('Segmented Control'),
controller: controller, children: children,
), groupValue: groupValue,
onValueChanged: defaultCallback,
);
},
), ),
); );
@ -1005,7 +963,6 @@ void main() {
0: Text('Child 1'), 0: Text('Child 1'),
1: Text('Child 2'), 1: Text('Child 2'),
}; };
final ValueNotifier<int> controller = ValueNotifier<int>(0);
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
await tester.pumpWidget( await tester.pumpWidget(
@ -1015,9 +972,14 @@ void main() {
controller: scrollController, controller: scrollController,
children: <Widget>[ children: <Widget>[
const SizedBox(height: 100), const SizedBox(height: 100),
CupertinoSlidingSegmentedControl<int>( boilerplate(
children: children, builder: (BuildContext context) {
controller: controller, return CupertinoSlidingSegmentedControl<int>(
children: children,
groupValue: groupValue,
onValueChanged: defaultCallback,
);
},
), ),
const SizedBox(height: 1000), const SizedBox(height: 1000),
], ],
@ -1029,7 +991,7 @@ void main() {
await tester.tap(find.text('Child 2')); await tester.tap(find.text('Child 2'));
await tester.pump(); await tester.pump();
expect(controller.value, 1); expect(groupValue, 1);
// Vertical drag works for the scroll view. // Vertical drag works for the scroll view.
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('Child 1'))); final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('Child 1')));
@ -1043,14 +1005,14 @@ void main() {
expect(scrollController.offset, 100); expect(scrollController.offset, 100);
// Does not affect the segmented control. // Does not affect the segmented control.
expect(controller.value, 1); expect(groupValue, 1);
await gesture.moveBy(const Offset(0, 100)); await gesture.moveBy(const Offset(0, 100));
await gesture.up(); await gesture.up();
await tester.pump(); await tester.pump();
expect(scrollController.offset, 0); expect(scrollController.offset, 0);
expect(controller.value, 1); expect(groupValue, 1);
// Long press vertical drag is recognized by the segmented control. // Long press vertical drag is recognized by the segmented control.
await gesture.down(tester.getCenter(find.text('Child 1'))); await gesture.down(tester.getCenter(find.text('Child 1')));
@ -1061,7 +1023,7 @@ void main() {
// Should not scroll. // Should not scroll.
expect(scrollController.offset, 0); expect(scrollController.offset, 0);
expect(controller.value, 1); expect(groupValue, 1);
await gesture.moveBy(const Offset(0, 100)); await gesture.moveBy(const Offset(0, 100));
await gesture.moveBy(const Offset(0, 100)); await gesture.moveBy(const Offset(0, 100));
@ -1069,7 +1031,7 @@ void main() {
await tester.pump(); await tester.pump();
expect(scrollController.offset, 0); expect(scrollController.offset, 0);
expect(controller.value, 0); expect(groupValue, 0);
// Horizontal drag is recognized by the segmentedControl. // Horizontal drag is recognized by the segmentedControl.
await gesture.down(tester.getCenter(find.text('Child 1'))); await gesture.down(tester.getCenter(find.text('Child 1')));
@ -1079,6 +1041,6 @@ void main() {
await tester.pump(); await tester.pump();
expect(scrollController.offset, 0); expect(scrollController.offset, 0);
expect(controller.value, 1); expect(groupValue, 1);
}); });
} }