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
......@@ -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,14 +342,16 @@ 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);
expect(value, isFalse);
await gesture.up();
expect(value, isTrue);
await tester.pump();
gesture = await tester.startGesture(switchRect.center);
......@@ -362,7 +364,10 @@ void main() {
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;
......
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