Let cupertino & material switches move to the right state after dragging (#51606)
diff --git a/packages/flutter/lib/src/cupertino/switch.dart b/packages/flutter/lib/src/cupertino/switch.dart
index 3d39e07..0988de1 100644
--- a/packages/flutter/lib/src/cupertino/switch.dart
+++ b/packages/flutter/lib/src/cupertino/switch.dart
@@ -140,8 +140,158 @@
}
class _CupertinoSwitchState extends State<CupertinoSwitch> with TickerProviderStateMixin {
+ TapGestureRecognizer _tap;
+ HorizontalDragGestureRecognizer _drag;
+
+ AnimationController _positionController;
+ CurvedAnimation position;
+
+ AnimationController _reactionController;
+ Animation<double> _reaction;
+
+ bool get isInteractive => widget.onChanged != null;
+
+ // A non-null boolean value that changes to true at the end of a drag if the
+ // switch must be animated to the position indicated by the widget's value.
+ bool needsPositionAnimation = false;
+
+ @override
+ void initState() {
+ super.initState();
+
+ _tap = TapGestureRecognizer()
+ ..onTapDown = _handleTapDown
+ ..onTapUp = _handleTapUp
+ ..onTap = _handleTap
+ ..onTapCancel = _handleTapCancel;
+ _drag = HorizontalDragGestureRecognizer()
+ ..onStart = _handleDragStart
+ ..onUpdate = _handleDragUpdate
+ ..onEnd = _handleDragEnd
+ ..dragStartBehavior = widget.dragStartBehavior;
+
+ _positionController = AnimationController(
+ duration: _kToggleDuration,
+ value: widget.value ? 1.0 : 0.0,
+ vsync: this,
+ );
+ position = CurvedAnimation(
+ parent: _positionController,
+ curve: Curves.linear,
+ );
+ _reactionController = AnimationController(
+ duration: _kReactionDuration,
+ vsync: this,
+ );
+ _reaction = CurvedAnimation(
+ parent: _reactionController,
+ curve: Curves.ease,
+ );
+ }
+
+ @override
+ void didUpdateWidget(CupertinoSwitch oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ _drag.dragStartBehavior = widget.dragStartBehavior;
+
+ if (needsPositionAnimation || oldWidget.value != widget.value)
+ _resumePositionAnimation(isLinear: needsPositionAnimation);
+ }
+
+ // `isLinear` must be true if the position animation is trying to move the
+ // thumb to the closest end after the most recent drag animation, so the curve
+ // does not change when the controller's value is not 0 or 1.
+ //
+ // It can be set to false when it's an implicit animation triggered by
+ // widget.value changes.
+ void _resumePositionAnimation({ bool isLinear = true }) {
+ needsPositionAnimation = false;
+ position
+ ..curve = isLinear ? null : Curves.ease
+ ..reverseCurve = isLinear ? null : Curves.ease.flipped;
+ if (widget.value)
+ _positionController.forward();
+ else
+ _positionController.reverse();
+ }
+
+ void _handleTapDown(TapDownDetails details) {
+ if (isInteractive)
+ needsPositionAnimation = false;
+ _reactionController.forward();
+ }
+
+ void _handleTap() {
+ if (isInteractive) {
+ widget.onChanged(!widget.value);
+ _emitVibration();
+ }
+ }
+
+ void _handleTapUp(TapUpDetails details) {
+ if (isInteractive) {
+ needsPositionAnimation = false;
+ _reactionController.reverse();
+ }
+ }
+
+ void _handleTapCancel() {
+ if (isInteractive)
+ _reactionController.reverse();
+ }
+
+ void _handleDragStart(DragStartDetails details) {
+ if (isInteractive) {
+ needsPositionAnimation = false;
+ _reactionController.forward();
+ _emitVibration();
+ }
+ }
+
+ void _handleDragUpdate(DragUpdateDetails details) {
+ if (isInteractive) {
+ position
+ ..curve = null
+ ..reverseCurve = null;
+ final double delta = details.primaryDelta / _kTrackInnerLength;
+ switch (Directionality.of(context)) {
+ case TextDirection.rtl:
+ _positionController.value -= delta;
+ break;
+ case TextDirection.ltr:
+ _positionController.value += delta;
+ break;
+ }
+ }
+ }
+
+ void _handleDragEnd(DragEndDetails details) {
+ // Deferring the animation to the next build phase.
+ setState(() { needsPositionAnimation = true; });
+ // Call onChanged when the user's intent to change value is clear.
+ if (position.value >= 0.5 != widget.value)
+ widget.onChanged(!widget.value);
+ _reactionController.reverse();
+ }
+
+ void _emitVibration() {
+ switch (defaultTargetPlatform) {
+ case TargetPlatform.iOS:
+ HapticFeedback.lightImpact();
+ break;
+ case TargetPlatform.android:
+ case TargetPlatform.fuchsia:
+ case TargetPlatform.linux:
+ case TargetPlatform.macOS:
+ case TargetPlatform.windows:
+ break;
+ }
+ }
+
@override
Widget build(BuildContext context) {
+ if (needsPositionAnimation)
+ _resumePositionAnimation();
return Opacity(
opacity: widget.onChanged == null ? _kCupertinoSwitchDisabledOpacity : 1.0,
child: _CupertinoSwitchRenderObjectWidget(
@@ -152,11 +302,21 @@
),
trackColor: CupertinoDynamicColor.resolve(widget.trackColor ?? CupertinoColors.secondarySystemFill, context),
onChanged: widget.onChanged,
- vsync: this,
- dragStartBehavior: widget.dragStartBehavior,
+ textDirection: Directionality.of(context),
+ state: this,
),
);
}
+
+ @override
+ void dispose() {
+ _tap.dispose();
+ _drag.dispose();
+
+ _positionController.dispose();
+ _reactionController.dispose();
+ super.dispose();
+ }
}
class _CupertinoSwitchRenderObjectWidget extends LeafRenderObjectWidget {
@@ -166,16 +326,16 @@
this.activeColor,
this.trackColor,
this.onChanged,
- this.vsync,
- this.dragStartBehavior = DragStartBehavior.start,
+ this.textDirection,
+ this.state,
}) : super(key: key);
final bool value;
final Color activeColor;
final Color trackColor;
final ValueChanged<bool> onChanged;
- final TickerProvider vsync;
- final DragStartBehavior dragStartBehavior;
+ final _CupertinoSwitchState state;
+ final TextDirection textDirection;
@override
_RenderCupertinoSwitch createRenderObject(BuildContext context) {
@@ -184,9 +344,8 @@
activeColor: activeColor,
trackColor: trackColor,
onChanged: onChanged,
- textDirection: Directionality.of(context),
- vsync: vsync,
- dragStartBehavior: dragStartBehavior,
+ textDirection: textDirection,
+ state: state,
);
}
@@ -197,9 +356,7 @@
..activeColor = activeColor
..trackColor = trackColor
..onChanged = onChanged
- ..textDirection = Directionality.of(context)
- ..vsync = vsync
- ..dragStartBehavior = dragStartBehavior;
+ ..textDirection = textDirection;
}
}
@@ -224,53 +381,22 @@
@required Color trackColor,
ValueChanged<bool> onChanged,
@required TextDirection textDirection,
- @required TickerProvider vsync,
- DragStartBehavior dragStartBehavior = DragStartBehavior.start,
+ @required _CupertinoSwitchState state,
}) : assert(value != null),
assert(activeColor != null),
- assert(vsync != null),
+ assert(state != null),
_value = value,
_activeColor = activeColor,
_trackColor = trackColor,
_onChanged = onChanged,
_textDirection = textDirection,
- _vsync = vsync,
+ _state = state,
super(additionalConstraints: const BoxConstraints.tightFor(width: _kSwitchWidth, height: _kSwitchHeight)) {
- _tap = TapGestureRecognizer()
- ..onTapDown = _handleTapDown
- ..onTap = _handleTap
- ..onTapUp = _handleTapUp
- ..onTapCancel = _handleTapCancel;
- _drag = HorizontalDragGestureRecognizer()
- ..onStart = _handleDragStart
- ..onUpdate = _handleDragUpdate
- ..onEnd = _handleDragEnd
- ..dragStartBehavior = dragStartBehavior;
- _positionController = AnimationController(
- duration: _kToggleDuration,
- value: value ? 1.0 : 0.0,
- vsync: vsync,
- );
- _position = CurvedAnimation(
- parent: _positionController,
- curve: Curves.linear,
- )..addListener(markNeedsPaint)
- ..addStatusListener(_handlePositionStateChanged);
- _reactionController = AnimationController(
- duration: _kReactionDuration,
- vsync: vsync,
- );
- _reaction = CurvedAnimation(
- parent: _reactionController,
- curve: Curves.ease,
- )..addListener(markNeedsPaint);
+ state.position.addListener(markNeedsPaint);
+ state._reaction.addListener(markNeedsPaint);
}
- AnimationController _positionController;
- CurvedAnimation _position;
-
- AnimationController _reactionController;
- Animation<double> _reaction;
+ final _CupertinoSwitchState _state;
bool get value => _value;
bool _value;
@@ -280,24 +406,6 @@
return;
_value = value;
markNeedsSemanticsUpdate();
- _position
- ..curve = Curves.ease
- ..reverseCurve = Curves.ease.flipped;
- if (value)
- _positionController.forward();
- else
- _positionController.reverse();
- }
-
- TickerProvider get vsync => _vsync;
- TickerProvider _vsync;
- set vsync(TickerProvider value) {
- assert(value != null);
- if (value == _vsync)
- return;
- _vsync = value;
- _positionController.resync(vsync);
- _reactionController.resync(vsync);
}
Color get activeColor => _activeColor;
@@ -343,126 +451,8 @@
markNeedsPaint();
}
- DragStartBehavior get dragStartBehavior => _drag.dragStartBehavior;
- set dragStartBehavior(DragStartBehavior value) {
- assert(value != null);
- if (_drag.dragStartBehavior == value)
- return;
- _drag.dragStartBehavior = value;
- }
-
bool get isInteractive => onChanged != null;
- TapGestureRecognizer _tap;
- HorizontalDragGestureRecognizer _drag;
-
- @override
- void attach(PipelineOwner owner) {
- super.attach(owner);
- if (value)
- _positionController.forward();
- else
- _positionController.reverse();
- if (isInteractive) {
- switch (_reactionController.status) {
- case AnimationStatus.forward:
- _reactionController.forward();
- break;
- case AnimationStatus.reverse:
- _reactionController.reverse();
- break;
- case AnimationStatus.dismissed:
- case AnimationStatus.completed:
- // nothing to do
- break;
- }
- }
- }
-
- @override
- void detach() {
- _positionController.stop();
- _reactionController.stop();
- super.detach();
- }
-
- void _handlePositionStateChanged(AnimationStatus status) {
- if (isInteractive) {
- if (status == AnimationStatus.completed && !_value)
- onChanged(true);
- else if (status == AnimationStatus.dismissed && _value)
- onChanged(false);
- }
- }
-
- void _handleTapDown(TapDownDetails details) {
- if (isInteractive)
- _reactionController.forward();
- }
-
- void _handleTap() {
- if (isInteractive) {
- onChanged(!_value);
- _emitVibration();
- }
- }
-
- void _handleTapUp(TapUpDetails details) {
- if (isInteractive)
- _reactionController.reverse();
- }
-
- void _handleTapCancel() {
- if (isInteractive)
- _reactionController.reverse();
- }
-
- void _handleDragStart(DragStartDetails details) {
- if (isInteractive) {
- _reactionController.forward();
- _emitVibration();
- }
- }
-
- void _handleDragUpdate(DragUpdateDetails details) {
- if (isInteractive) {
- _position
- ..curve = null
- ..reverseCurve = null;
- final double delta = details.primaryDelta / _kTrackInnerLength;
- switch (textDirection) {
- case TextDirection.rtl:
- _positionController.value -= delta;
- break;
- case TextDirection.ltr:
- _positionController.value += delta;
- break;
- }
- }
- }
-
- void _handleDragEnd(DragEndDetails details) {
- if (_position.value >= 0.5)
- _positionController.forward();
- else
- _positionController.reverse();
- _reactionController.reverse();
- }
-
- void _emitVibration() {
- switch (defaultTargetPlatform) {
- case TargetPlatform.iOS:
- HapticFeedback.lightImpact();
- break;
- case TargetPlatform.android:
- case TargetPlatform.fuchsia:
- case TargetPlatform.linux:
- case TargetPlatform.macOS:
- case TargetPlatform.windows:
- break;
- }
- }
-
@override
bool hitTestSelf(Offset position) => true;
@@ -470,8 +460,8 @@
void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
assert(debugHandleEvent(event, entry));
if (event is PointerDownEvent && isInteractive) {
- _drag.addPointer(event);
- _tap.addPointer(event);
+ _state._drag.addPointer(event);
+ _state._tap.addPointer(event);
}
}
@@ -480,7 +470,7 @@
super.describeSemanticsConfiguration(config);
if (isInteractive)
- config.onTap = _handleTap;
+ config.onTap = _state._handleTap;
config.isEnabled = isInteractive;
config.isToggled = _value;
@@ -490,8 +480,8 @@
void paint(PaintingContext context, Offset offset) {
final Canvas canvas = context.canvas;
- final double currentValue = _position.value;
- final double currentReactionValue = _reaction.value;
+ final double currentValue = _state.position.value;
+ final double currentReactionValue = _state._reaction.value;
double visualPosition;
switch (textDirection) {
diff --git a/packages/flutter/lib/src/material/switch.dart b/packages/flutter/lib/src/material/switch.dart
index ad229ec..094ed1e 100644
--- a/packages/flutter/lib/src/material/switch.dart
+++ b/packages/flutter/lib/src/material/switch.dart
@@ -267,6 +267,12 @@
bool get enabled => widget.onChanged != null;
+ void _didFinishDragging() {
+ // The user has finished dragging the thumb of this switch. Rebuild the switch
+ // to update the animation.
+ setState(() {});
+ }
+
Widget buildMaterialSwitch(BuildContext context) {
assert(debugCheckHasMaterial(context));
final ThemeData theme = Theme.of(context);
@@ -313,7 +319,7 @@
additionalConstraints: BoxConstraints.tight(getSwitchSize(theme)),
hasFocus: _focused,
hovering: _hovering,
- vsync: this,
+ state: this,
);
},
),
@@ -380,11 +386,11 @@
this.inactiveTrackColor,
this.configuration,
this.onChanged,
- this.vsync,
this.additionalConstraints,
this.dragStartBehavior,
this.hasFocus,
this.hovering,
+ this.state,
}) : super(key: key);
final bool value;
@@ -398,11 +404,11 @@
final Color inactiveTrackColor;
final ImageConfiguration configuration;
final ValueChanged<bool> onChanged;
- final TickerProvider vsync;
final BoxConstraints additionalConstraints;
final DragStartBehavior dragStartBehavior;
final bool hasFocus;
final bool hovering;
+ final _SwitchState state;
@override
_RenderSwitch createRenderObject(BuildContext context) {
@@ -423,7 +429,7 @@
additionalConstraints: additionalConstraints,
hasFocus: hasFocus,
hovering: hovering,
- vsync: vsync,
+ state: state,
);
}
@@ -446,7 +452,7 @@
..dragStartBehavior = dragStartBehavior
..hasFocus = hasFocus
..hovering = hovering
- ..vsync = vsync;
+ ..vsync = state;
}
}
@@ -468,7 +474,7 @@
DragStartBehavior dragStartBehavior,
bool hasFocus,
bool hovering,
- @required TickerProvider vsync,
+ @required this.state,
}) : assert(textDirection != null),
_activeThumbImage = activeThumbImage,
_inactiveThumbImage = inactiveThumbImage,
@@ -487,7 +493,7 @@
additionalConstraints: additionalConstraints,
hasFocus: hasFocus,
hovering: hovering,
- vsync: vsync,
+ vsync: state,
) {
_drag = HorizontalDragGestureRecognizer()
..onStart = _handleDragStart
@@ -562,6 +568,26 @@
_drag.dragStartBehavior = value;
}
+ _SwitchState state;
+
+ @override
+ set value(bool newValue) {
+ assert(value != null);
+ super.value = newValue;
+ // The widget is rebuilt and we have pending position animation to play.
+ if (_needsPositionAnimation) {
+ _needsPositionAnimation = false;
+ position
+ ..curve = null
+ ..reverseCurve = null;
+ if (newValue)
+ positionController.forward();
+ else
+ positionController.reverse();
+ }
+ }
+
+
@override
void detach() {
_cachedThumbPainter?.dispose();
@@ -573,6 +599,8 @@
HorizontalDragGestureRecognizer _drag;
+ bool _needsPositionAnimation = false;
+
void _handleDragStart(DragStartDetails details) {
if (isInteractive)
reactionController.forward();
@@ -596,11 +624,12 @@
}
void _handleDragEnd(DragEndDetails details) {
- if (position.value >= 0.5)
- positionController.forward();
- else
- positionController.reverse();
+ _needsPositionAnimation = true;
+
+ if (position.value >= 0.5 != value)
+ onChanged(!value);
reactionController.reverse();
+ state._didFinishDragging();
}
@override
diff --git a/packages/flutter/lib/src/material/toggleable.dart b/packages/flutter/lib/src/material/toggleable.dart
index 443696e..fb69be4 100644
--- a/packages/flutter/lib/src/material/toggleable.dart
+++ b/packages/flutter/lib/src/material/toggleable.dart
@@ -70,8 +70,7 @@
_position = CurvedAnimation(
parent: _positionController,
curve: Curves.linear,
- )..addListener(markNeedsPaint)
- ..addStatusListener(_handlePositionStateChanged);
+ )..addListener(markNeedsPaint);
_reactionController = AnimationController(
duration: kRadialReactionDuration,
vsync: vsync,
@@ -335,9 +334,7 @@
/// Called when the control changes value.
///
/// If the control is tapped, [onChanged] is called immediately with the new
- /// value. If the control changes value due to an animation (see
- /// [positionController]), the callback is called when the animation
- /// completes.
+ /// value.
///
/// The control is considered interactive (see [isInteractive]) if this
/// callback is non-null. If the callback is null, then the control is
@@ -397,19 +394,6 @@
super.detach();
}
- // Handle the case where the _positionController's value changes because
- // the user dragged the toggleable: we may reach 0.0 or 1.0 without
- // seeing a tap. The Switch does this.
- void _handlePositionStateChanged(AnimationStatus status) {
- if (isInteractive && !tristate) {
- if (status == AnimationStatus.completed && _value == false) {
- onChanged(true);
- } else if (status == AnimationStatus.dismissed && _value != false) {
- onChanged(false);
- }
- }
- }
-
void _handleTapDown(TapDownDetails details) {
if (isInteractive) {
_downPosition = globalToLocal(details.globalPosition);
diff --git a/packages/flutter/test/cupertino/switch_test.dart b/packages/flutter/test/cupertino/switch_test.dart
index 3857a26..3dc5495 100644
--- a/packages/flutter/test/cupertino/switch_test.dart
+++ b/packages/flutter/test/cupertino/switch_test.dart
@@ -342,27 +342,32 @@
);
await tester.pumpAndSettle();
final Rect switchRect = tester.getRect(find.byType(CupertinoSwitch));
+ expect(value, isFalse);
TestGesture gesture = await tester.startGesture(switchRect.center);
// We have to execute the drag in two frames because the first update will
// just set the start position.
await gesture.moveBy(const Offset(20.0, 0.0));
await gesture.moveBy(const Offset(20.0, 0.0));
- expect(value, isTrue);
- await gesture.up();
- await tester.pump();
-
- gesture = await tester.startGesture(switchRect.center);
- await gesture.moveBy(const Offset(20.0, 0.0));
- await gesture.moveBy(const Offset(20.0, 0.0));
- expect(value, isTrue);
- await gesture.up();
- await tester.pump();
-
- gesture = await tester.startGesture(switchRect.center);
- await gesture.moveBy(const Offset(-20.0, 0.0));
- await gesture.moveBy(const Offset(-20.0, 0.0));
expect(value, isFalse);
+ await gesture.up();
+ expect(value, isTrue);
+ await tester.pump();
+
+ gesture = await tester.startGesture(switchRect.center);
+ await gesture.moveBy(const Offset(20.0, 0.0));
+ await gesture.moveBy(const Offset(20.0, 0.0));
+ expect(value, isTrue);
+ await gesture.up();
+ await tester.pump();
+
+ gesture = await tester.startGesture(switchRect.center);
+ await gesture.moveBy(const Offset(-20.0, 0.0));
+ await gesture.moveBy(const Offset(-20.0, 0.0));
+ expect(value, isTrue);
+ await gesture.up();
+ expect(value, isFalse);
+ await tester.pump();
});
testWidgets('Switch can drag (RTL)', (WidgetTester tester) async {
@@ -410,6 +415,77 @@
expect(value, isFalse);
});
+ testWidgets('can veto switch dragging result', (WidgetTester tester) async {
+ bool value = false;
+
+ await tester.pumpWidget(
+ Directionality(
+ textDirection: TextDirection.ltr,
+ child: StatefulBuilder(
+ builder: (BuildContext context, StateSetter setState) {
+ return Material(
+ child: Center(
+ child: CupertinoSwitch(
+ dragStartBehavior: DragStartBehavior.down,
+ value: value,
+ onChanged: (bool newValue) {
+ setState(() {
+ value = value || newValue;
+ });
+ },
+ ),
+ ),
+ );
+ },
+ ),
+ ),
+ );
+
+ // Move a little to the right, not past the middle.
+ TestGesture gesture = await tester.startGesture(tester.getRect(find.byType(CupertinoSwitch)).center);
+ await gesture.moveBy(const Offset(kTouchSlop + 0.1, 0.0));
+ await tester.pump();
+ await gesture.moveBy(const Offset(-kTouchSlop + 5.1, 0.0));
+ await tester.pump();
+ await gesture.up();
+ await tester.pump();
+ expect(value, isFalse);
+ final CurvedAnimation position = (tester.state(find.byType(CupertinoSwitch)) as dynamic).position as CurvedAnimation;
+ expect(position.value, lessThan(0.5));
+ await tester.pump();
+ await tester.pumpAndSettle();
+ expect(value, isFalse);
+ expect(position.value, 0);
+
+ // Move past the middle.
+ gesture = await tester.startGesture(tester.getRect(find.byType(CupertinoSwitch)).center);
+ await gesture.moveBy(const Offset(kTouchSlop + 0.1, 0.0));
+ await tester.pump();
+ await gesture.up();
+ await tester.pump();
+ expect(value, isTrue);
+ expect(position.value, greaterThan(0.5));
+
+ await tester.pump();
+ await tester.pumpAndSettle();
+ expect(value, isTrue);
+ expect(position.value, 1.0);
+
+ // Now move back to the left, the revert animation should play.
+ gesture = await tester.startGesture(tester.getRect(find.byType(CupertinoSwitch)).center);
+ await gesture.moveBy(const Offset(-kTouchSlop - 0.1, 0.0));
+ await tester.pump();
+ await gesture.up();
+ await tester.pump();
+ expect(value, isTrue);
+ expect(position.value, lessThan(0.5));
+
+ await tester.pump();
+ await tester.pumpAndSettle();
+ expect(value, isTrue);
+ expect(position.value, 1.0);
+ });
+
testWidgets('Switch is translucent when disabled', (WidgetTester tester) async {
await tester.pumpWidget(
const Directionality(
diff --git a/packages/flutter/test/material/switch_test.dart b/packages/flutter/test/material/switch_test.dart
index 4f5cb22..6358654 100644
--- a/packages/flutter/test/material/switch_test.dart
+++ b/packages/flutter/test/material/switch_test.dart
@@ -205,8 +205,9 @@
// just set the start position.
await gesture.moveBy(const Offset(20.0, 0.0));
await gesture.moveBy(const Offset(20.0, 0.0));
- expect(value, isTrue);
+ expect(value, isFalse);
await gesture.up();
+ expect(value, isTrue);
await tester.pump();
gesture = await tester.startGesture(switchRect.center);
@@ -214,11 +215,14 @@
await gesture.moveBy(const Offset(20.0, 0.0));
expect(value, isTrue);
await gesture.up();
+ expect(value, isTrue);
await tester.pump();
gesture = await tester.startGesture(switchRect.center);
await gesture.moveBy(const Offset(-20.0, 0.0));
await gesture.moveBy(const Offset(-20.0, 0.0));
+ expect(value, isTrue);
+ await gesture.up();
expect(value, isFalse);
});
@@ -489,6 +493,84 @@
expect(tester.hasRunningAnimations, false);
});
+ testWidgets('can veto switch dragging result', (WidgetTester tester) async {
+ bool value = false;
+
+ await tester.pumpWidget(
+ Directionality(
+ textDirection: TextDirection.ltr,
+ child: StatefulBuilder(
+ builder: (BuildContext context, StateSetter setState) {
+ return Material(
+ child: Center(
+ child: Switch(
+ dragStartBehavior: DragStartBehavior.down,
+ value: value,
+ onChanged: (bool newValue) {
+ setState(() {
+ value = value || newValue;
+ });
+ },
+ ),
+ ),
+ );
+ },
+ ),
+ ),
+ );
+
+ // Move a little to the right, not past the middle.
+ TestGesture gesture = await tester.startGesture(tester.getRect(find.byType(Switch)).center);
+ await gesture.moveBy(const Offset(kTouchSlop + 0.1, 0.0));
+ await tester.pump();
+ await gesture.moveBy(const Offset(-kTouchSlop + 5.1, 0.0));
+ await tester.pump();
+ await gesture.up();
+ await tester.pump();
+ expect(value, isFalse);
+ final RenderToggleable renderObject = tester.renderObject<RenderToggleable>(
+ find.descendant(
+ of: find.byType(Switch),
+ matching: find.byWidgetPredicate(
+ (Widget widget) => widget.runtimeType.toString() == '_SwitchRenderObjectWidget',
+ ),
+ ),
+ );
+ expect(renderObject.position.value, lessThan(0.5));
+ await tester.pump();
+ await tester.pumpAndSettle();
+ expect(value, isFalse);
+ expect(renderObject.position.value, 0);
+
+ // Move past the middle.
+ gesture = await tester.startGesture(tester.getRect(find.byType(Switch)).center);
+ await gesture.moveBy(const Offset(kTouchSlop + 0.1, 0.0));
+ await tester.pump();
+ await gesture.up();
+ await tester.pump();
+ expect(value, isTrue);
+ expect(renderObject.position.value, greaterThan(0.5));
+
+ await tester.pump();
+ await tester.pumpAndSettle();
+ expect(value, isTrue);
+ expect(renderObject.position.value, 1.0);
+
+ // Now move back to the left, the revert animation should play.
+ gesture = await tester.startGesture(tester.getRect(find.byType(Switch)).center);
+ await gesture.moveBy(const Offset(-kTouchSlop - 0.1, 0.0));
+ await tester.pump();
+ await gesture.up();
+ await tester.pump();
+ expect(value, isTrue);
+ expect(renderObject.position.value, lessThan(0.5));
+
+ await tester.pump();
+ await tester.pumpAndSettle();
+ expect(value, isTrue);
+ expect(renderObject.position.value, 1.0);
+ });
+
testWidgets('switch has semantic events', (WidgetTester tester) async {
dynamic semanticEvent;
bool value = false;