Unverified Commit 87cbdddd authored by LongCatIsLooong's avatar LongCatIsLooong Committed by GitHub

Let cupertino & material switches move to the right state after dragging (#51606)

parent b1664a27
...@@ -140,8 +140,158 @@ class CupertinoSwitch extends StatefulWidget { ...@@ -140,8 +140,158 @@ class CupertinoSwitch extends StatefulWidget {
} }
class _CupertinoSwitchState extends State<CupertinoSwitch> with TickerProviderStateMixin { 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (needsPositionAnimation)
_resumePositionAnimation();
return Opacity( return Opacity(
opacity: widget.onChanged == null ? _kCupertinoSwitchDisabledOpacity : 1.0, opacity: widget.onChanged == null ? _kCupertinoSwitchDisabledOpacity : 1.0,
child: _CupertinoSwitchRenderObjectWidget( child: _CupertinoSwitchRenderObjectWidget(
...@@ -152,11 +302,21 @@ class _CupertinoSwitchState extends State<CupertinoSwitch> with TickerProviderSt ...@@ -152,11 +302,21 @@ class _CupertinoSwitchState extends State<CupertinoSwitch> with TickerProviderSt
), ),
trackColor: CupertinoDynamicColor.resolve(widget.trackColor ?? CupertinoColors.secondarySystemFill, context), trackColor: CupertinoDynamicColor.resolve(widget.trackColor ?? CupertinoColors.secondarySystemFill, context),
onChanged: widget.onChanged, onChanged: widget.onChanged,
vsync: this, textDirection: Directionality.of(context),
dragStartBehavior: widget.dragStartBehavior, state: this,
), ),
); );
} }
@override
void dispose() {
_tap.dispose();
_drag.dispose();
_positionController.dispose();
_reactionController.dispose();
super.dispose();
}
} }
class _CupertinoSwitchRenderObjectWidget extends LeafRenderObjectWidget { class _CupertinoSwitchRenderObjectWidget extends LeafRenderObjectWidget {
...@@ -166,16 +326,16 @@ class _CupertinoSwitchRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -166,16 +326,16 @@ class _CupertinoSwitchRenderObjectWidget extends LeafRenderObjectWidget {
this.activeColor, this.activeColor,
this.trackColor, this.trackColor,
this.onChanged, this.onChanged,
this.vsync, this.textDirection,
this.dragStartBehavior = DragStartBehavior.start, this.state,
}) : super(key: key); }) : super(key: key);
final bool value; final bool value;
final Color activeColor; final Color activeColor;
final Color trackColor; final Color trackColor;
final ValueChanged<bool> onChanged; final ValueChanged<bool> onChanged;
final TickerProvider vsync; final _CupertinoSwitchState state;
final DragStartBehavior dragStartBehavior; final TextDirection textDirection;
@override @override
_RenderCupertinoSwitch createRenderObject(BuildContext context) { _RenderCupertinoSwitch createRenderObject(BuildContext context) {
...@@ -184,9 +344,8 @@ class _CupertinoSwitchRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -184,9 +344,8 @@ class _CupertinoSwitchRenderObjectWidget extends LeafRenderObjectWidget {
activeColor: activeColor, activeColor: activeColor,
trackColor: trackColor, trackColor: trackColor,
onChanged: onChanged, onChanged: onChanged,
textDirection: Directionality.of(context), textDirection: textDirection,
vsync: vsync, state: state,
dragStartBehavior: dragStartBehavior,
); );
} }
...@@ -197,9 +356,7 @@ class _CupertinoSwitchRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -197,9 +356,7 @@ class _CupertinoSwitchRenderObjectWidget extends LeafRenderObjectWidget {
..activeColor = activeColor ..activeColor = activeColor
..trackColor = trackColor ..trackColor = trackColor
..onChanged = onChanged ..onChanged = onChanged
..textDirection = Directionality.of(context) ..textDirection = textDirection;
..vsync = vsync
..dragStartBehavior = dragStartBehavior;
} }
} }
...@@ -224,53 +381,22 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox { ...@@ -224,53 +381,22 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox {
@required Color trackColor, @required Color trackColor,
ValueChanged<bool> onChanged, ValueChanged<bool> onChanged,
@required TextDirection textDirection, @required TextDirection textDirection,
@required TickerProvider vsync, @required _CupertinoSwitchState state,
DragStartBehavior dragStartBehavior = DragStartBehavior.start,
}) : assert(value != null), }) : assert(value != null),
assert(activeColor != null), assert(activeColor != null),
assert(vsync != null), assert(state != null),
_value = value, _value = value,
_activeColor = activeColor, _activeColor = activeColor,
_trackColor = trackColor, _trackColor = trackColor,
_onChanged = onChanged, _onChanged = onChanged,
_textDirection = textDirection, _textDirection = textDirection,
_vsync = vsync, _state = state,
super(additionalConstraints: const BoxConstraints.tightFor(width: _kSwitchWidth, height: _kSwitchHeight)) { super(additionalConstraints: const BoxConstraints.tightFor(width: _kSwitchWidth, height: _kSwitchHeight)) {
_tap = TapGestureRecognizer() state.position.addListener(markNeedsPaint);
..onTapDown = _handleTapDown state._reaction.addListener(markNeedsPaint);
..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);
} }
AnimationController _positionController; final _CupertinoSwitchState _state;
CurvedAnimation _position;
AnimationController _reactionController;
Animation<double> _reaction;
bool get value => _value; bool get value => _value;
bool _value; bool _value;
...@@ -280,24 +406,6 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox { ...@@ -280,24 +406,6 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox {
return; return;
_value = value; _value = value;
markNeedsSemanticsUpdate(); 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; Color get activeColor => _activeColor;
...@@ -343,126 +451,8 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox { ...@@ -343,126 +451,8 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox {
markNeedsPaint(); 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; 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 @override
bool hitTestSelf(Offset position) => true; bool hitTestSelf(Offset position) => true;
...@@ -470,8 +460,8 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox { ...@@ -470,8 +460,8 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox {
void handleEvent(PointerEvent event, BoxHitTestEntry entry) { void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
assert(debugHandleEvent(event, entry)); assert(debugHandleEvent(event, entry));
if (event is PointerDownEvent && isInteractive) { if (event is PointerDownEvent && isInteractive) {
_drag.addPointer(event); _state._drag.addPointer(event);
_tap.addPointer(event); _state._tap.addPointer(event);
} }
} }
...@@ -480,7 +470,7 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox { ...@@ -480,7 +470,7 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox {
super.describeSemanticsConfiguration(config); super.describeSemanticsConfiguration(config);
if (isInteractive) if (isInteractive)
config.onTap = _handleTap; config.onTap = _state._handleTap;
config.isEnabled = isInteractive; config.isEnabled = isInteractive;
config.isToggled = _value; config.isToggled = _value;
...@@ -490,8 +480,8 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox { ...@@ -490,8 +480,8 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox {
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
final Canvas canvas = context.canvas; final Canvas canvas = context.canvas;
final double currentValue = _position.value; final double currentValue = _state.position.value;
final double currentReactionValue = _reaction.value; final double currentReactionValue = _state._reaction.value;
double visualPosition; double visualPosition;
switch (textDirection) { switch (textDirection) {
......
...@@ -267,6 +267,12 @@ class _SwitchState extends State<Switch> with TickerProviderStateMixin { ...@@ -267,6 +267,12 @@ class _SwitchState extends State<Switch> with TickerProviderStateMixin {
bool get enabled => widget.onChanged != null; 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) { Widget buildMaterialSwitch(BuildContext context) {
assert(debugCheckHasMaterial(context)); assert(debugCheckHasMaterial(context));
final ThemeData theme = Theme.of(context); final ThemeData theme = Theme.of(context);
...@@ -313,7 +319,7 @@ class _SwitchState extends State<Switch> with TickerProviderStateMixin { ...@@ -313,7 +319,7 @@ class _SwitchState extends State<Switch> with TickerProviderStateMixin {
additionalConstraints: BoxConstraints.tight(getSwitchSize(theme)), additionalConstraints: BoxConstraints.tight(getSwitchSize(theme)),
hasFocus: _focused, hasFocus: _focused,
hovering: _hovering, hovering: _hovering,
vsync: this, state: this,
); );
}, },
), ),
...@@ -380,11 +386,11 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -380,11 +386,11 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
this.inactiveTrackColor, this.inactiveTrackColor,
this.configuration, this.configuration,
this.onChanged, this.onChanged,
this.vsync,
this.additionalConstraints, this.additionalConstraints,
this.dragStartBehavior, this.dragStartBehavior,
this.hasFocus, this.hasFocus,
this.hovering, this.hovering,
this.state,
}) : super(key: key); }) : super(key: key);
final bool value; final bool value;
...@@ -398,11 +404,11 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -398,11 +404,11 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
final Color inactiveTrackColor; final Color inactiveTrackColor;
final ImageConfiguration configuration; final ImageConfiguration configuration;
final ValueChanged<bool> onChanged; final ValueChanged<bool> onChanged;
final TickerProvider vsync;
final BoxConstraints additionalConstraints; final BoxConstraints additionalConstraints;
final DragStartBehavior dragStartBehavior; final DragStartBehavior dragStartBehavior;
final bool hasFocus; final bool hasFocus;
final bool hovering; final bool hovering;
final _SwitchState state;
@override @override
_RenderSwitch createRenderObject(BuildContext context) { _RenderSwitch createRenderObject(BuildContext context) {
...@@ -423,7 +429,7 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -423,7 +429,7 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
additionalConstraints: additionalConstraints, additionalConstraints: additionalConstraints,
hasFocus: hasFocus, hasFocus: hasFocus,
hovering: hovering, hovering: hovering,
vsync: vsync, state: state,
); );
} }
...@@ -446,7 +452,7 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -446,7 +452,7 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
..dragStartBehavior = dragStartBehavior ..dragStartBehavior = dragStartBehavior
..hasFocus = hasFocus ..hasFocus = hasFocus
..hovering = hovering ..hovering = hovering
..vsync = vsync; ..vsync = state;
} }
} }
...@@ -468,7 +474,7 @@ class _RenderSwitch extends RenderToggleable { ...@@ -468,7 +474,7 @@ class _RenderSwitch extends RenderToggleable {
DragStartBehavior dragStartBehavior, DragStartBehavior dragStartBehavior,
bool hasFocus, bool hasFocus,
bool hovering, bool hovering,
@required TickerProvider vsync, @required this.state,
}) : assert(textDirection != null), }) : assert(textDirection != null),
_activeThumbImage = activeThumbImage, _activeThumbImage = activeThumbImage,
_inactiveThumbImage = inactiveThumbImage, _inactiveThumbImage = inactiveThumbImage,
...@@ -487,7 +493,7 @@ class _RenderSwitch extends RenderToggleable { ...@@ -487,7 +493,7 @@ class _RenderSwitch extends RenderToggleable {
additionalConstraints: additionalConstraints, additionalConstraints: additionalConstraints,
hasFocus: hasFocus, hasFocus: hasFocus,
hovering: hovering, hovering: hovering,
vsync: vsync, vsync: state,
) { ) {
_drag = HorizontalDragGestureRecognizer() _drag = HorizontalDragGestureRecognizer()
..onStart = _handleDragStart ..onStart = _handleDragStart
...@@ -562,6 +568,26 @@ class _RenderSwitch extends RenderToggleable { ...@@ -562,6 +568,26 @@ class _RenderSwitch extends RenderToggleable {
_drag.dragStartBehavior = value; _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 @override
void detach() { void detach() {
_cachedThumbPainter?.dispose(); _cachedThumbPainter?.dispose();
...@@ -573,6 +599,8 @@ class _RenderSwitch extends RenderToggleable { ...@@ -573,6 +599,8 @@ class _RenderSwitch extends RenderToggleable {
HorizontalDragGestureRecognizer _drag; HorizontalDragGestureRecognizer _drag;
bool _needsPositionAnimation = false;
void _handleDragStart(DragStartDetails details) { void _handleDragStart(DragStartDetails details) {
if (isInteractive) if (isInteractive)
reactionController.forward(); reactionController.forward();
...@@ -596,11 +624,12 @@ class _RenderSwitch extends RenderToggleable { ...@@ -596,11 +624,12 @@ class _RenderSwitch extends RenderToggleable {
} }
void _handleDragEnd(DragEndDetails details) { void _handleDragEnd(DragEndDetails details) {
if (position.value >= 0.5) _needsPositionAnimation = true;
positionController.forward();
else if (position.value >= 0.5 != value)
positionController.reverse(); onChanged(!value);
reactionController.reverse(); reactionController.reverse();
state._didFinishDragging();
} }
@override @override
......
...@@ -70,8 +70,7 @@ abstract class RenderToggleable extends RenderConstrainedBox { ...@@ -70,8 +70,7 @@ abstract class RenderToggleable extends RenderConstrainedBox {
_position = CurvedAnimation( _position = CurvedAnimation(
parent: _positionController, parent: _positionController,
curve: Curves.linear, curve: Curves.linear,
)..addListener(markNeedsPaint) )..addListener(markNeedsPaint);
..addStatusListener(_handlePositionStateChanged);
_reactionController = AnimationController( _reactionController = AnimationController(
duration: kRadialReactionDuration, duration: kRadialReactionDuration,
vsync: vsync, vsync: vsync,
...@@ -335,9 +334,7 @@ abstract class RenderToggleable extends RenderConstrainedBox { ...@@ -335,9 +334,7 @@ abstract class RenderToggleable extends RenderConstrainedBox {
/// Called when the control changes value. /// Called when the control changes value.
/// ///
/// If the control is tapped, [onChanged] is called immediately with the new /// If the control is tapped, [onChanged] is called immediately with the new
/// value. If the control changes value due to an animation (see /// value.
/// [positionController]), the callback is called when the animation
/// completes.
/// ///
/// The control is considered interactive (see [isInteractive]) if this /// The control is considered interactive (see [isInteractive]) if this
/// callback is non-null. If the callback is null, then the control is /// callback is non-null. If the callback is null, then the control is
...@@ -397,19 +394,6 @@ abstract class RenderToggleable extends RenderConstrainedBox { ...@@ -397,19 +394,6 @@ abstract class RenderToggleable extends RenderConstrainedBox {
super.detach(); 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) { void _handleTapDown(TapDownDetails details) {
if (isInteractive) { if (isInteractive) {
_downPosition = globalToLocal(details.globalPosition); _downPosition = globalToLocal(details.globalPosition);
......
...@@ -342,14 +342,16 @@ void main() { ...@@ -342,14 +342,16 @@ void main() {
); );
await tester.pumpAndSettle(); await tester.pumpAndSettle();
final Rect switchRect = tester.getRect(find.byType(CupertinoSwitch)); final Rect switchRect = tester.getRect(find.byType(CupertinoSwitch));
expect(value, isFalse);
TestGesture gesture = await tester.startGesture(switchRect.center); TestGesture gesture = await tester.startGesture(switchRect.center);
// We have to execute the drag in two frames because the first update will // We have to execute the drag in two frames because the first update will
// just set the start position. // just set the start position.
await gesture.moveBy(const Offset(20.0, 0.0)); await gesture.moveBy(const Offset(20.0, 0.0));
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(); await gesture.up();
expect(value, isTrue);
await tester.pump(); await tester.pump();
gesture = await tester.startGesture(switchRect.center); gesture = await tester.startGesture(switchRect.center);
...@@ -362,7 +364,10 @@ void main() { ...@@ -362,7 +364,10 @@ void main() {
gesture = await tester.startGesture(switchRect.center); gesture = await tester.startGesture(switchRect.center);
await gesture.moveBy(const Offset(-20.0, 0.0)); await gesture.moveBy(const Offset(-20.0, 0.0));
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); expect(value, isFalse);
await tester.pump();
}); });
testWidgets('Switch can drag (RTL)', (WidgetTester tester) async { testWidgets('Switch can drag (RTL)', (WidgetTester tester) async {
...@@ -410,6 +415,77 @@ void main() { ...@@ -410,6 +415,77 @@ void main() {
expect(value, isFalse); 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 { testWidgets('Switch is translucent when disabled', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
const Directionality( const Directionality(
......
...@@ -205,8 +205,9 @@ void main() { ...@@ -205,8 +205,9 @@ void main() {
// just set the start position. // just set the start position.
await gesture.moveBy(const Offset(20.0, 0.0)); await gesture.moveBy(const Offset(20.0, 0.0));
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(); await gesture.up();
expect(value, isTrue);
await tester.pump(); await tester.pump();
gesture = await tester.startGesture(switchRect.center); gesture = await tester.startGesture(switchRect.center);
...@@ -214,11 +215,14 @@ void main() { ...@@ -214,11 +215,14 @@ void main() {
await gesture.moveBy(const Offset(20.0, 0.0)); await gesture.moveBy(const Offset(20.0, 0.0));
expect(value, isTrue); expect(value, isTrue);
await gesture.up(); await gesture.up();
expect(value, isTrue);
await tester.pump(); await tester.pump();
gesture = await tester.startGesture(switchRect.center); gesture = await tester.startGesture(switchRect.center);
await gesture.moveBy(const Offset(-20.0, 0.0)); await gesture.moveBy(const Offset(-20.0, 0.0));
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); expect(value, isFalse);
}); });
...@@ -489,6 +493,84 @@ void main() { ...@@ -489,6 +493,84 @@ void main() {
expect(tester.hasRunningAnimations, false); 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 { testWidgets('switch has semantic events', (WidgetTester tester) async {
dynamic semanticEvent; dynamic semanticEvent;
bool value = false; bool value = false;
......
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