Fix Dismissible confirmDismiss errors (#88672)
* Fix Dismissible confirmDismiss errors * + * refactor test * fix rebase * remove new line
This commit is contained in:
parent
0fd498f11d
commit
49332da76a
@ -128,6 +128,8 @@ class Dismissible extends StatefulWidget {
|
|||||||
|
|
||||||
/// Gives the app an opportunity to confirm or veto a pending dismissal.
|
/// Gives the app an opportunity to confirm or veto a pending dismissal.
|
||||||
///
|
///
|
||||||
|
/// The widget cannot be dragged again until the returned future resolves.
|
||||||
|
///
|
||||||
/// If the returned Future<bool> completes true, then this widget will be
|
/// If the returned Future<bool> completes true, then this widget will be
|
||||||
/// dismissed, otherwise it will be moved back to its original location.
|
/// dismissed, otherwise it will be moved back to its original location.
|
||||||
///
|
///
|
||||||
@ -263,6 +265,7 @@ class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin
|
|||||||
Animation<double>? _resizeAnimation;
|
Animation<double>? _resizeAnimation;
|
||||||
|
|
||||||
double _dragExtent = 0.0;
|
double _dragExtent = 0.0;
|
||||||
|
bool _confirming = false;
|
||||||
bool _dragUnderway = false;
|
bool _dragUnderway = false;
|
||||||
Size? _sizePriorToCollapse;
|
Size? _sizePriorToCollapse;
|
||||||
|
|
||||||
@ -308,6 +311,8 @@ class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _handleDragStart(DragStartDetails details) {
|
void _handleDragStart(DragStartDetails details) {
|
||||||
|
if (_confirming)
|
||||||
|
return;
|
||||||
_dragUnderway = true;
|
_dragUnderway = true;
|
||||||
if (_moveController!.isAnimating) {
|
if (_moveController!.isAnimating) {
|
||||||
_dragExtent = _moveController!.value * _overallDragAxisExtent * _dragExtent.sign;
|
_dragExtent = _moveController!.value * _overallDragAxisExtent * _dragExtent.sign;
|
||||||
@ -426,12 +431,12 @@ class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin
|
|||||||
return _FlingGestureKind.reverse;
|
return _FlingGestureKind.reverse;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _handleDragEnd(DragEndDetails details) async {
|
void _handleDragEnd(DragEndDetails details) {
|
||||||
if (!_isActive || _moveController!.isAnimating)
|
if (!_isActive || _moveController!.isAnimating)
|
||||||
return;
|
return;
|
||||||
_dragUnderway = false;
|
_dragUnderway = false;
|
||||||
if (_moveController!.isCompleted && await _confirmStartResizeAnimation() == true) {
|
if (_moveController!.isCompleted) {
|
||||||
_startResizeAnimation();
|
_handleMoveCompleted();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final double flingVelocity = _directionIsXAxis ? details.velocity.pixelsPerSecond.dx : details.velocity.pixelsPerSecond.dy;
|
final double flingVelocity = _directionIsXAxis ? details.velocity.pixelsPerSecond.dx : details.velocity.pixelsPerSecond.dy;
|
||||||
@ -466,24 +471,41 @@ class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin
|
|||||||
|
|
||||||
Future<void> _handleDismissStatusChanged(AnimationStatus status) async {
|
Future<void> _handleDismissStatusChanged(AnimationStatus status) async {
|
||||||
if (status == AnimationStatus.completed && !_dragUnderway) {
|
if (status == AnimationStatus.completed && !_dragUnderway) {
|
||||||
if (await _confirmStartResizeAnimation() == true)
|
await _handleMoveCompleted();
|
||||||
|
}
|
||||||
|
if (mounted) {
|
||||||
|
updateKeepAlive();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handleMoveCompleted() async {
|
||||||
|
if ((widget.dismissThresholds[_dismissDirection] ?? _kDismissThreshold) >= 1.0) {
|
||||||
|
_moveController!.reverse();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final bool result = await _confirmStartResizeAnimation();
|
||||||
|
if (mounted) {
|
||||||
|
if (result)
|
||||||
_startResizeAnimation();
|
_startResizeAnimation();
|
||||||
else
|
else
|
||||||
_moveController!.reverse();
|
_moveController!.reverse();
|
||||||
}
|
}
|
||||||
updateKeepAlive();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool?> _confirmStartResizeAnimation() async {
|
Future<bool> _confirmStartResizeAnimation() async {
|
||||||
if (widget.confirmDismiss != null) {
|
if (widget.confirmDismiss != null) {
|
||||||
|
_confirming = true;
|
||||||
final DismissDirection direction = _dismissDirection;
|
final DismissDirection direction = _dismissDirection;
|
||||||
return widget.confirmDismiss!(direction);
|
try {
|
||||||
|
return await widget.confirmDismiss!(direction) ?? false;
|
||||||
|
} finally {
|
||||||
|
_confirming = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _startResizeAnimation() {
|
void _startResizeAnimation() {
|
||||||
assert(_moveController != null);
|
|
||||||
assert(_moveController!.isCompleted);
|
assert(_moveController!.isCompleted);
|
||||||
assert(_resizeController == null);
|
assert(_resizeController == null);
|
||||||
assert(_sizePriorToCollapse == null);
|
assert(_sizePriorToCollapse == null);
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/gestures.dart' show DragStartBehavior;
|
import 'package:flutter/gestures.dart' show DragStartBehavior;
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
@ -19,6 +21,7 @@ Widget buildTest({
|
|||||||
TextDirection textDirection = TextDirection.ltr,
|
TextDirection textDirection = TextDirection.ltr,
|
||||||
Future<bool?> Function(BuildContext context, DismissDirection direction)? confirmDismiss,
|
Future<bool?> Function(BuildContext context, DismissDirection direction)? confirmDismiss,
|
||||||
ScrollController? controller,
|
ScrollController? controller,
|
||||||
|
ScrollPhysics? scrollPhysics,
|
||||||
Widget? background,
|
Widget? background,
|
||||||
}) {
|
}) {
|
||||||
return Directionality(
|
return Directionality(
|
||||||
@ -59,6 +62,7 @@ Widget buildTest({
|
|||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(10.0),
|
padding: const EdgeInsets.all(10.0),
|
||||||
child: ListView(
|
child: ListView(
|
||||||
|
physics: scrollPhysics,
|
||||||
controller: controller,
|
controller: controller,
|
||||||
dragStartBehavior: DragStartBehavior.down,
|
dragStartBehavior: DragStartBehavior.down,
|
||||||
scrollDirection: scrollDirection,
|
scrollDirection: scrollDirection,
|
||||||
@ -83,23 +87,23 @@ Future<void> dismissElement(WidgetTester tester, Finder finder, { required AxisD
|
|||||||
// getTopRight() returns a point that's just beyond itemWidget's right
|
// getTopRight() returns a point that's just beyond itemWidget's right
|
||||||
// edge and outside the Dismissible event listener's bounds.
|
// edge and outside the Dismissible event listener's bounds.
|
||||||
downLocation = tester.getTopRight(finder) + const Offset(-0.1, 0.0);
|
downLocation = tester.getTopRight(finder) + const Offset(-0.1, 0.0);
|
||||||
upLocation = tester.getTopLeft(finder);
|
upLocation = tester.getTopLeft(finder) + const Offset(-0.1, 0.0);
|
||||||
break;
|
break;
|
||||||
case AxisDirection.right:
|
case AxisDirection.right:
|
||||||
// we do the same thing here to keep the test symmetric
|
// we do the same thing here to keep the test symmetric
|
||||||
downLocation = tester.getTopLeft(finder) + const Offset(0.1, 0.0);
|
downLocation = tester.getTopLeft(finder) + const Offset(0.1, 0.0);
|
||||||
upLocation = tester.getTopRight(finder);
|
upLocation = tester.getTopRight(finder) + const Offset(0.1, 0.0);
|
||||||
break;
|
break;
|
||||||
case AxisDirection.up:
|
case AxisDirection.up:
|
||||||
// getBottomLeft() returns a point that's just below itemWidget's bottom
|
// getBottomLeft() returns a point that's just below itemWidget's bottom
|
||||||
// edge and outside the Dismissible event listener's bounds.
|
// edge and outside the Dismissible event listener's bounds.
|
||||||
downLocation = tester.getBottomLeft(finder) + const Offset(0.0, -0.1);
|
downLocation = tester.getBottomLeft(finder) + const Offset(0.0, -0.1);
|
||||||
upLocation = tester.getTopLeft(finder);
|
upLocation = tester.getTopLeft(finder) + const Offset(0.0, -0.1);
|
||||||
break;
|
break;
|
||||||
case AxisDirection.down:
|
case AxisDirection.down:
|
||||||
// again with doing the same here for symmetry
|
// again with doing the same here for symmetry
|
||||||
downLocation = tester.getTopLeft(finder) + const Offset(0.1, 0.0);
|
downLocation = tester.getTopLeft(finder) + const Offset(0.1, 0.0);
|
||||||
upLocation = tester.getBottomLeft(finder);
|
upLocation = tester.getBottomLeft(finder) + const Offset(0.1, 0.0);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,12 +150,7 @@ Future<void> dismissItem(
|
|||||||
expect(itemFinder, findsOneWidget);
|
expect(itemFinder, findsOneWidget);
|
||||||
|
|
||||||
await mechanism(tester, itemFinder, gestureDirection: gestureDirection);
|
await mechanism(tester, itemFinder, gestureDirection: gestureDirection);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
await tester.pump(); // start the slide
|
|
||||||
await tester.pump(const Duration(seconds: 1)); // finish the slide and start shrinking...
|
|
||||||
await tester.pump(); // first frame of shrinking animation
|
|
||||||
await tester.pump(const Duration(seconds: 1)); // finish the shrinking and call the callback...
|
|
||||||
await tester.pump(); // rebuild after the callback removes the entry
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> checkFlingItemBeforeMovementEnd(
|
Future<void> checkFlingItemBeforeMovementEnd(
|
||||||
@ -779,7 +778,145 @@ void main() {
|
|||||||
expect(confirmDismissDirection, DismissDirection.endToStart);
|
expect(confirmDismissDirection, DismissDirection.endToStart);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('setState that does not remove the Dismissible from tree should throws Error', (WidgetTester tester) async {
|
testWidgets('Pending confirmDismiss does not cause errors', (WidgetTester tester) async {
|
||||||
|
// Regression test for https://github.com/flutter/flutter/issues/54990
|
||||||
|
|
||||||
|
late Completer<bool?> completer;
|
||||||
|
Widget buildFrame() {
|
||||||
|
completer = Completer<bool?>();
|
||||||
|
return buildTest(
|
||||||
|
confirmDismiss: (BuildContext context, DismissDirection dismissDirection) {
|
||||||
|
return completer.future;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// false for _handleDragEnd - when dragged to the end and released
|
||||||
|
|
||||||
|
await tester.pumpWidget(buildFrame());
|
||||||
|
|
||||||
|
await dismissItem(tester, 0, gestureDirection: AxisDirection.right);
|
||||||
|
expect(find.text('0'), findsOneWidget);
|
||||||
|
expect(dismissedItems, isEmpty);
|
||||||
|
|
||||||
|
await tester.pumpWidget(const SizedBox());
|
||||||
|
completer.complete(false);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
// true for _handleDragEnd - when dragged to the end and released
|
||||||
|
|
||||||
|
await tester.pumpWidget(buildFrame());
|
||||||
|
|
||||||
|
await dismissItem(tester, 0, gestureDirection: AxisDirection.right);
|
||||||
|
expect(find.text('0'), findsOneWidget);
|
||||||
|
expect(dismissedItems, isEmpty);
|
||||||
|
|
||||||
|
await tester.pumpWidget(const SizedBox());
|
||||||
|
completer.complete(true);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
// false for _handleDismissStatusChanged - when fling reaches the end
|
||||||
|
|
||||||
|
await tester.pumpWidget(buildFrame());
|
||||||
|
|
||||||
|
await dismissItem(tester, 0, gestureDirection: AxisDirection.right, mechanism: flingElement);
|
||||||
|
expect(find.text('0'), findsOneWidget);
|
||||||
|
expect(dismissedItems, isEmpty);
|
||||||
|
|
||||||
|
await tester.pumpWidget(const SizedBox());
|
||||||
|
completer.complete(false);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
// true for _handleDismissStatusChanged - when fling reaches the end
|
||||||
|
|
||||||
|
await tester.pumpWidget(buildFrame());
|
||||||
|
|
||||||
|
await dismissItem(tester, 0, gestureDirection: AxisDirection.right, mechanism: flingElement);
|
||||||
|
expect(find.text('0'), findsOneWidget);
|
||||||
|
expect(dismissedItems, isEmpty);
|
||||||
|
|
||||||
|
await tester.pumpWidget(const SizedBox());
|
||||||
|
completer.complete(true);
|
||||||
|
await tester.pump();
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Dismissible cannot be dragged with pending confirmDismiss', (WidgetTester tester) async {
|
||||||
|
final Completer<bool?> completer = Completer<bool?>();
|
||||||
|
await tester.pumpWidget(
|
||||||
|
buildTest(
|
||||||
|
confirmDismiss: (BuildContext context, DismissDirection dismissDirection) {
|
||||||
|
return completer.future;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Trigger confirmDismiss call.
|
||||||
|
await dismissItem(tester, 0, gestureDirection: AxisDirection.right);
|
||||||
|
final Offset position = tester.getTopLeft(find.text('0'));
|
||||||
|
|
||||||
|
// Try to move and verify it has not moved.
|
||||||
|
Offset dragAt = tester.getTopLeft(find.text('0'));
|
||||||
|
dragAt = Offset(100.0, dragAt.dy);
|
||||||
|
final TestGesture gesture = await tester.startGesture(dragAt);
|
||||||
|
await gesture.moveTo(dragAt + const Offset(100.0, 0.0));
|
||||||
|
await gesture.up();
|
||||||
|
await tester.pump();
|
||||||
|
expect(tester.getTopLeft(find.text('0')), position);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Drag to end and release - items does not get stuck if confirmDismiss returns false', (WidgetTester tester) async {
|
||||||
|
// Regression test for https://github.com/flutter/flutter/issues/87556
|
||||||
|
|
||||||
|
final Completer<bool?> completer = Completer<bool?>();
|
||||||
|
await tester.pumpWidget(
|
||||||
|
buildTest(
|
||||||
|
confirmDismiss: (BuildContext context, DismissDirection dismissDirection) {
|
||||||
|
return completer.future;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Offset position = tester.getTopLeft(find.text('0'));
|
||||||
|
await dismissItem(tester, 0, gestureDirection: AxisDirection.right);
|
||||||
|
completer.complete(false);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(tester.getTopLeft(find.text('0')), position);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Dismissible with null resizeDuration calls onDismissed immediately', (WidgetTester tester) async {
|
||||||
|
bool resized = false;
|
||||||
|
bool dismissed = false;
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
Directionality(
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
child: Dismissible(
|
||||||
|
dragStartBehavior: DragStartBehavior.down,
|
||||||
|
key: UniqueKey(),
|
||||||
|
direction: DismissDirection.horizontal,
|
||||||
|
resizeDuration: null,
|
||||||
|
onDismissed: (DismissDirection direction) {
|
||||||
|
dismissed = true;
|
||||||
|
},
|
||||||
|
onResize: () {
|
||||||
|
resized = true;
|
||||||
|
},
|
||||||
|
child: const SizedBox(
|
||||||
|
width: 100.0,
|
||||||
|
height: 100.0,
|
||||||
|
child: Text('0'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await dismissElement(tester, find.text('0'), gestureDirection: AxisDirection.right);
|
||||||
|
await tester.pump();
|
||||||
|
expect(dismissed, true);
|
||||||
|
expect(resized, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('setState that does not remove the Dismissible from tree should throw Error', (WidgetTester tester) async {
|
||||||
const Axis scrollDirection = Axis.vertical;
|
const Axis scrollDirection = Axis.vertical;
|
||||||
const DismissDirection dismissDirection = DismissDirection.horizontal;
|
const DismissDirection dismissDirection = DismissDirection.horizontal;
|
||||||
|
|
||||||
@ -912,7 +1049,10 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('DismissDirection.none does not trigger dismiss', (WidgetTester tester) async {
|
testWidgets('DismissDirection.none does not trigger dismiss', (WidgetTester tester) async {
|
||||||
await tester.pumpWidget(buildTest(dismissDirection: DismissDirection.none));
|
await tester.pumpWidget(buildTest(
|
||||||
|
dismissDirection: DismissDirection.none,
|
||||||
|
scrollPhysics: const NeverScrollableScrollPhysics(),
|
||||||
|
));
|
||||||
expect(dismissedItems, isEmpty);
|
expect(dismissedItems, isEmpty);
|
||||||
|
|
||||||
await dismissItem(tester, 0, gestureDirection: AxisDirection.left);
|
await dismissItem(tester, 0, gestureDirection: AxisDirection.left);
|
||||||
@ -941,7 +1081,7 @@ void main() {
|
|||||||
await dismissItem(tester, 0, gestureDirection: AxisDirection.down);
|
await dismissItem(tester, 0, gestureDirection: AxisDirection.down);
|
||||||
expect(controller.offset, 0.0);
|
expect(controller.offset, 0.0);
|
||||||
await dismissItem(tester, 0, gestureDirection: AxisDirection.up);
|
await dismissItem(tester, 0, gestureDirection: AxisDirection.up);
|
||||||
expect(controller.offset, 99.9);
|
expect(controller.offset, 100.0);
|
||||||
controller.dispose();
|
controller.dispose();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user