Let cupertino & material switches move to the right state after dragging (#51606)
This commit is contained in:
parent
b1664a27d9
commit
87cbddddd8
@ -140,8 +140,158 @@ class CupertinoSwitch extends StatefulWidget {
|
||||
}
|
||||
|
||||
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 @@ class _CupertinoSwitchState extends State<CupertinoSwitch> with TickerProviderSt
|
||||
),
|
||||
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 @@ class _CupertinoSwitchRenderObjectWidget extends LeafRenderObjectWidget {
|
||||
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 @@ class _CupertinoSwitchRenderObjectWidget extends LeafRenderObjectWidget {
|
||||
activeColor: activeColor,
|
||||
trackColor: trackColor,
|
||||
onChanged: onChanged,
|
||||
textDirection: Directionality.of(context),
|
||||
vsync: vsync,
|
||||
dragStartBehavior: dragStartBehavior,
|
||||
textDirection: textDirection,
|
||||
state: state,
|
||||
);
|
||||
}
|
||||
|
||||
@ -197,9 +356,7 @@ class _CupertinoSwitchRenderObjectWidget extends LeafRenderObjectWidget {
|
||||
..activeColor = activeColor
|
||||
..trackColor = trackColor
|
||||
..onChanged = onChanged
|
||||
..textDirection = Directionality.of(context)
|
||||
..vsync = vsync
|
||||
..dragStartBehavior = dragStartBehavior;
|
||||
..textDirection = textDirection;
|
||||
}
|
||||
}
|
||||
|
||||
@ -224,53 +381,22 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox {
|
||||
@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 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox {
|
||||
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 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox {
|
||||
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 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox {
|
||||
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 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox {
|
||||
super.describeSemanticsConfiguration(config);
|
||||
|
||||
if (isInteractive)
|
||||
config.onTap = _handleTap;
|
||||
config.onTap = _state._handleTap;
|
||||
|
||||
config.isEnabled = isInteractive;
|
||||
config.isToggled = _value;
|
||||
@ -490,8 +480,8 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox {
|
||||
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) {
|
||||
|
@ -267,6 +267,12 @@ class _SwitchState extends State<Switch> with TickerProviderStateMixin {
|
||||
|
||||
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 @@ class _SwitchState extends State<Switch> with TickerProviderStateMixin {
|
||||
additionalConstraints: BoxConstraints.tight(getSwitchSize(theme)),
|
||||
hasFocus: _focused,
|
||||
hovering: _hovering,
|
||||
vsync: this,
|
||||
state: this,
|
||||
);
|
||||
},
|
||||
),
|
||||
@ -380,11 +386,11 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
|
||||
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 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
|
||||
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 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
|
||||
additionalConstraints: additionalConstraints,
|
||||
hasFocus: hasFocus,
|
||||
hovering: hovering,
|
||||
vsync: vsync,
|
||||
state: state,
|
||||
);
|
||||
}
|
||||
|
||||
@ -446,7 +452,7 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
|
||||
..dragStartBehavior = dragStartBehavior
|
||||
..hasFocus = hasFocus
|
||||
..hovering = hovering
|
||||
..vsync = vsync;
|
||||
..vsync = state;
|
||||
}
|
||||
}
|
||||
|
||||
@ -468,7 +474,7 @@ class _RenderSwitch extends RenderToggleable {
|
||||
DragStartBehavior dragStartBehavior,
|
||||
bool hasFocus,
|
||||
bool hovering,
|
||||
@required TickerProvider vsync,
|
||||
@required this.state,
|
||||
}) : assert(textDirection != null),
|
||||
_activeThumbImage = activeThumbImage,
|
||||
_inactiveThumbImage = inactiveThumbImage,
|
||||
@ -487,7 +493,7 @@ class _RenderSwitch extends RenderToggleable {
|
||||
additionalConstraints: additionalConstraints,
|
||||
hasFocus: hasFocus,
|
||||
hovering: hovering,
|
||||
vsync: vsync,
|
||||
vsync: state,
|
||||
) {
|
||||
_drag = HorizontalDragGestureRecognizer()
|
||||
..onStart = _handleDragStart
|
||||
@ -562,6 +568,26 @@ class _RenderSwitch extends RenderToggleable {
|
||||
_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 @@ class _RenderSwitch extends RenderToggleable {
|
||||
|
||||
HorizontalDragGestureRecognizer _drag;
|
||||
|
||||
bool _needsPositionAnimation = false;
|
||||
|
||||
void _handleDragStart(DragStartDetails details) {
|
||||
if (isInteractive)
|
||||
reactionController.forward();
|
||||
@ -596,11 +624,12 @@ class _RenderSwitch extends RenderToggleable {
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -70,8 +70,7 @@ abstract class RenderToggleable extends RenderConstrainedBox {
|
||||
_position = CurvedAnimation(
|
||||
parent: _positionController,
|
||||
curve: Curves.linear,
|
||||
)..addListener(markNeedsPaint)
|
||||
..addStatusListener(_handlePositionStateChanged);
|
||||
)..addListener(markNeedsPaint);
|
||||
_reactionController = AnimationController(
|
||||
duration: kRadialReactionDuration,
|
||||
vsync: vsync,
|
||||
@ -335,9 +334,7 @@ abstract class RenderToggleable extends RenderConstrainedBox {
|
||||
/// 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 @@ abstract class RenderToggleable extends RenderConstrainedBox {
|
||||
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);
|
||||
|
@ -342,27 +342,32 @@ void main() {
|
||||
);
|
||||
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 @@ void main() {
|
||||
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(
|
||||
|
@ -205,8 +205,9 @@ void main() {
|
||||
// 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 @@ void main() {
|
||||
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 @@ void main() {
|
||||
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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user