Unverified Commit 7619c686 authored by Hans Muller's avatar Hans Muller Committed by GitHub

Add Dismissible.confirmDismiss callback (#26901)

parent c37b7c53
......@@ -24,6 +24,12 @@ const double _kDismissThreshold = 0.4;
/// Used by [Dismissible.onDismissed].
typedef DismissDirectionCallback = void Function(DismissDirection direction);
/// Signature used by [Dismissible] to give the application an opportunity to
/// confirm or veto a dismiss gesture.
///
/// Used by [Dismissible.confirmDismiss].
typedef ConfirmDismissCallback = Future<bool> Function(DismissDirection direction);
/// The direction in which a [Dismissible] can be dismissed.
enum DismissDirection {
/// The [Dismissible] can be dismissed by dragging either up or down.
......@@ -77,6 +83,7 @@ class Dismissible extends StatefulWidget {
@required this.child,
this.background,
this.secondaryBackground,
this.confirmDismiss,
this.onResize,
this.onDismissed,
this.direction = DismissDirection.horizontal,
......@@ -105,6 +112,15 @@ class Dismissible extends StatefulWidget {
/// has also been specified.
final Widget secondaryBackground;
/// Gives the app an opportunity to confirm or veto a pending dismissal.
///
/// If the returned Future<bool> completes true, then this widget will be
/// dismissed, otherwise it will be moved back to its original location.
///
/// If the returned Future<bool> completes to false or null the [onResize]
/// and [onDismissed] callbacks will not run.
final ConfirmDismissCallback confirmDismiss;
/// Called when the widget changes size (i.e., when contracting before being dismissed).
final VoidCallback onResize;
......@@ -386,11 +402,11 @@ class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin
return _FlingGestureKind.reverse;
}
void _handleDragEnd(DragEndDetails details) {
Future<void> _handleDragEnd(DragEndDetails details) async {
if (!_isActive || _moveController.isAnimating)
return;
_dragUnderway = false;
if (_moveController.isCompleted) {
if (_moveController.isCompleted && await _confirmStartResizeAnimation() == true) {
_startResizeAnimation();
return;
}
......@@ -424,12 +440,25 @@ class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin
}
}
void _handleDismissStatusChanged(AnimationStatus status) {
if (status == AnimationStatus.completed && !_dragUnderway)
_startResizeAnimation();
Future<void> _handleDismissStatusChanged(AnimationStatus status) async {
if (status == AnimationStatus.completed && !_dragUnderway) {
if (await _confirmStartResizeAnimation() == true)
_startResizeAnimation();
else
_moveController.reverse();
}
updateKeepAlive();
}
Future<bool> _confirmStartResizeAnimation() async {
if (widget.confirmDismiss != null) {
final DismissDirection direction = _dismissDirection;
assert(direction != null);
return widget.confirmDismiss(direction);
}
return true;
}
void _startResizeAnimation() {
assert(_moveController != null);
assert(_moveController.isCompleted);
......@@ -550,4 +579,3 @@ class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin
);
}
}
......@@ -476,4 +476,135 @@ void main() {
semantics.dispose();
});
testWidgets('Dismissable.confirmDismiss defers to an AlertDialog', (WidgetTester tester) async {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
final List<int> dismissedItems = <int>[];
// Dismiss is confirmed IFF confirmDismiss() returns true.
Future<bool> confirmDismiss (DismissDirection dismissDirection) {
return showDialog<bool>(
context: _scaffoldKey.currentContext,
barrierDismissible: true, // showDialog() returns null if tapped outside the dialog
builder: (BuildContext context) {
return AlertDialog(
actions: <Widget>[
FlatButton(
child: const Text('TRUE'),
onPressed: () {
Navigator.pop(context, true); // showDialog() returns true
},
),
FlatButton(
child: const Text('FALSE'),
onPressed: () {
Navigator.pop(context, false); // showDialog() returns false
},
),
],
);
},
);
}
Widget buildDismissibleItem(int item, StateSetter setState) {
return Dismissible(
key: ValueKey<int>(item),
confirmDismiss: confirmDismiss,
onDismissed: (DismissDirection direction) {
setState(() {
expect(dismissedItems.contains(item), isFalse);
dismissedItems.add(item);
});
},
child: SizedBox(
height: 100.0,
child: Text(item.toString()),
),
);
}
Widget buildFrame() {
return MaterialApp(
home: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Scaffold(
key: _scaffoldKey,
body: Padding(
padding: const EdgeInsets.all(16.0),
child: ListView(
itemExtent: 100.0,
children: <int>[0, 1, 2, 3, 4]
.where((int i) => !dismissedItems.contains(i))
.map<Widget>((int item) => buildDismissibleItem(item, setState)).toList(),
),
),
);
},
),
);
}
Future<void> dismissItem(WidgetTester tester, int item) async {
await tester.fling(find.text(item.toString()), const Offset(300.0, 0.0), 1000.0); // fling to the right
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
}
// Dismiss item 0 is confirmed via the AlertDialog
await tester.pumpWidget(buildFrame());
expect(dismissedItems, isEmpty);
await dismissItem(tester, 0); // Causes the AlertDialog to appear per confirmDismiss
await tester.pumpAndSettle();
await tester.tap(find.text('TRUE')); // AlertDialog action
await tester.pumpAndSettle();
expect(find.text('TRUE'), findsNothing); // Dialog was dismissed
expect(find.text('FALSE'), findsNothing);
expect(dismissedItems, <int>[0]);
expect(find.text('0'), findsNothing);
// Dismiss item 1 is not confirmed via the AlertDialog
await tester.pumpWidget(buildFrame());
expect(dismissedItems, <int>[0]);
await dismissItem(tester, 1); // Causes the AlertDialog to appear per confirmDismiss
await tester.pumpAndSettle();
await tester.tap(find.text('FALSE')); // AlertDialog action
await tester.pumpAndSettle();
expect(find.text('TRUE'), findsNothing); // Dialog was dismissed
expect(find.text('FALSE'), findsNothing);
expect(dismissedItems, <int>[0]);
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
// Dismiss item 1 is not confirmed via the AlertDialog
await tester.pumpWidget(buildFrame());
expect(dismissedItems, <int>[0]);
await dismissItem(tester, 1); // Causes the AlertDialog to appear per confirmDismiss
await tester.pumpAndSettle();
expect(find.text('FALSE'), findsOneWidget);
expect(find.text('TRUE'), findsOneWidget);
await tester.tapAt(Offset.zero); // Tap outside of the AlertDialog
await tester.pumpAndSettle();
expect(dismissedItems, <int>[0]);
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
expect(find.text('TRUE'), findsNothing); // Dialog was dismissed
expect(find.text('FALSE'), findsNothing);
// Dismiss item 1 is confirmed via the AlertDialog
await tester.pumpWidget(buildFrame());
expect(dismissedItems, <int>[0]);
await dismissItem(tester, 1); // Causes the AlertDialog to appear per confirmDismiss
await tester.pumpAndSettle();
await tester.tap(find.text('TRUE')); // AlertDialog action
await tester.pumpAndSettle();
expect(find.text('TRUE'), findsNothing); // Dialog was dismissed
expect(find.text('FALSE'), findsNothing);
expect(dismissedItems, <int>[0, 1]);
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsNothing);
});
}
......@@ -15,7 +15,11 @@ List<int> dismissedItems = <int>[];
Widget background;
const double crossAxisEndOffset = 0.5;
Widget buildTest({ double startToEndThreshold, TextDirection textDirection = TextDirection.ltr }) {
Widget buildTest({
double startToEndThreshold,
TextDirection textDirection = TextDirection.ltr,
Future<bool> Function(BuildContext context, DismissDirection direction) confirmDismiss,
}) {
return Directionality(
textDirection: textDirection,
child: StatefulBuilder(
......@@ -25,6 +29,9 @@ Widget buildTest({ double startToEndThreshold, TextDirection textDirection = Tex
dragStartBehavior: DragStartBehavior.down,
key: ValueKey<int>(item),
direction: dismissDirection,
confirmDismiss: confirmDismiss == null ? null : (DismissDirection direction) {
return confirmDismiss(context, direction);
},
onDismissed: (DismissDirection direction) {
setState(() {
reportedDismissDirection = direction;
......@@ -660,4 +667,63 @@ void main() {
expect(find.text('1'), findsOneWidget);
expect(dismissedItems, isEmpty);
});
testWidgets('confirmDismiss returns values: true, false, null', (WidgetTester tester) async {
scrollDirection = Axis.vertical;
dismissDirection = DismissDirection.horizontal;
DismissDirection confirmDismissDirection;
Widget buildFrame(bool confirmDismissValue) {
return buildTest(
confirmDismiss: (BuildContext context, DismissDirection dismissDirection) {
confirmDismissDirection = dismissDirection;
return Future<bool>.value(confirmDismissValue);
}
);
}
// Dismiss is confirmed IFF confirmDismiss() returns true.
await tester.pumpWidget(buildFrame(true));
expect(dismissedItems, isEmpty);
await dismissItem(tester, 0, gestureDirection: AxisDirection.right, mechanism: flingElement);
expect(find.text('0'), findsNothing);
expect(dismissedItems, equals(<int>[0]));
expect(reportedDismissDirection, DismissDirection.startToEnd);
expect(confirmDismissDirection, DismissDirection.startToEnd);
await dismissItem(tester, 1, gestureDirection: AxisDirection.left, mechanism: flingElement);
expect(find.text('1'), findsNothing);
expect(dismissedItems, equals(<int>[0, 1]));
expect(reportedDismissDirection, DismissDirection.endToStart);
expect(confirmDismissDirection, DismissDirection.endToStart);
// Dismiss is not confirmed if confirmDismiss() returns false
dismissedItems = <int>[];
await tester.pumpWidget(buildFrame(false));
await dismissItem(tester, 0, gestureDirection: AxisDirection.right, mechanism: flingElement);
expect(find.text('0'), findsOneWidget);
expect(dismissedItems, isEmpty);
expect(confirmDismissDirection, DismissDirection.startToEnd);
await dismissItem(tester, 1, gestureDirection: AxisDirection.left, mechanism: flingElement);
expect(find.text('1'), findsOneWidget);
expect(dismissedItems, isEmpty);
expect(confirmDismissDirection, DismissDirection.endToStart);
// Dismiss is not confirmed if confirmDismiss() returns null
dismissedItems = <int>[];
await tester.pumpWidget(buildFrame(null));
await dismissItem(tester, 0, gestureDirection: AxisDirection.right, mechanism: flingElement);
expect(find.text('0'), findsOneWidget);
expect(dismissedItems, isEmpty);
expect(confirmDismissDirection, DismissDirection.startToEnd);
await dismissItem(tester, 1, gestureDirection: AxisDirection.left, mechanism: flingElement);
expect(find.text('1'), findsOneWidget);
expect(dismissedItems, isEmpty);
expect(confirmDismissDirection, DismissDirection.endToStart);
});
}
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