Add Dismissible.confirmDismiss callback (#26901)
This commit is contained in:
parent
c37b7c535c
commit
7619c68655
@ -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);
|
||||
});
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user