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 {
|
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
|
|||||||
),
|
),
|
||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
..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 {
|
|||||||
@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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
|
|
||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
additionalConstraints: additionalConstraints,
|
additionalConstraints: additionalConstraints,
|
||||||
hasFocus: hasFocus,
|
hasFocus: hasFocus,
|
||||||
hovering: hovering,
|
hovering: hovering,
|
||||||
vsync: vsync,
|
state: state,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
_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 {
|
|||||||
|
|
||||||
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 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
|||||||
_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 {
|
|||||||
/// 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 {
|
|||||||
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,27 +342,32 @@ 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);
|
|
||||||
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);
|
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 {
|
testWidgets('Switch can drag (RTL)', (WidgetTester tester) async {
|
||||||
@ -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() {
|
|||||||
// 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() {
|
|||||||
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() {
|
|||||||
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;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user