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].
|
/// Used by [Dismissible.onDismissed].
|
||||||
typedef DismissDirectionCallback = void Function(DismissDirection direction);
|
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.
|
/// The direction in which a [Dismissible] can be dismissed.
|
||||||
enum DismissDirection {
|
enum DismissDirection {
|
||||||
/// The [Dismissible] can be dismissed by dragging either up or down.
|
/// The [Dismissible] can be dismissed by dragging either up or down.
|
||||||
@ -77,6 +83,7 @@ class Dismissible extends StatefulWidget {
|
|||||||
@required this.child,
|
@required this.child,
|
||||||
this.background,
|
this.background,
|
||||||
this.secondaryBackground,
|
this.secondaryBackground,
|
||||||
|
this.confirmDismiss,
|
||||||
this.onResize,
|
this.onResize,
|
||||||
this.onDismissed,
|
this.onDismissed,
|
||||||
this.direction = DismissDirection.horizontal,
|
this.direction = DismissDirection.horizontal,
|
||||||
@ -105,6 +112,15 @@ class Dismissible extends StatefulWidget {
|
|||||||
/// has also been specified.
|
/// has also been specified.
|
||||||
final Widget secondaryBackground;
|
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).
|
/// Called when the widget changes size (i.e., when contracting before being dismissed).
|
||||||
final VoidCallback onResize;
|
final VoidCallback onResize;
|
||||||
|
|
||||||
@ -386,11 +402,11 @@ class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin
|
|||||||
return _FlingGestureKind.reverse;
|
return _FlingGestureKind.reverse;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleDragEnd(DragEndDetails details) {
|
Future<void> _handleDragEnd(DragEndDetails details) async {
|
||||||
if (!_isActive || _moveController.isAnimating)
|
if (!_isActive || _moveController.isAnimating)
|
||||||
return;
|
return;
|
||||||
_dragUnderway = false;
|
_dragUnderway = false;
|
||||||
if (_moveController.isCompleted) {
|
if (_moveController.isCompleted && await _confirmStartResizeAnimation() == true) {
|
||||||
_startResizeAnimation();
|
_startResizeAnimation();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -424,12 +440,25 @@ class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleDismissStatusChanged(AnimationStatus status) {
|
Future<void> _handleDismissStatusChanged(AnimationStatus status) async {
|
||||||
if (status == AnimationStatus.completed && !_dragUnderway)
|
if (status == AnimationStatus.completed && !_dragUnderway) {
|
||||||
_startResizeAnimation();
|
if (await _confirmStartResizeAnimation() == true)
|
||||||
|
_startResizeAnimation();
|
||||||
|
else
|
||||||
|
_moveController.reverse();
|
||||||
|
}
|
||||||
updateKeepAlive();
|
updateKeepAlive();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> _confirmStartResizeAnimation() async {
|
||||||
|
if (widget.confirmDismiss != null) {
|
||||||
|
final DismissDirection direction = _dismissDirection;
|
||||||
|
assert(direction != null);
|
||||||
|
return widget.confirmDismiss(direction);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void _startResizeAnimation() {
|
void _startResizeAnimation() {
|
||||||
assert(_moveController != null);
|
assert(_moveController != null);
|
||||||
assert(_moveController.isCompleted);
|
assert(_moveController.isCompleted);
|
||||||
@ -550,4 +579,3 @@ class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -476,4 +476,135 @@ void main() {
|
|||||||
|
|
||||||
semantics.dispose();
|
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;
|
Widget background;
|
||||||
const double crossAxisEndOffset = 0.5;
|
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(
|
return Directionality(
|
||||||
textDirection: textDirection,
|
textDirection: textDirection,
|
||||||
child: StatefulBuilder(
|
child: StatefulBuilder(
|
||||||
@ -25,6 +29,9 @@ Widget buildTest({ double startToEndThreshold, TextDirection textDirection = Tex
|
|||||||
dragStartBehavior: DragStartBehavior.down,
|
dragStartBehavior: DragStartBehavior.down,
|
||||||
key: ValueKey<int>(item),
|
key: ValueKey<int>(item),
|
||||||
direction: dismissDirection,
|
direction: dismissDirection,
|
||||||
|
confirmDismiss: confirmDismiss == null ? null : (DismissDirection direction) {
|
||||||
|
return confirmDismiss(context, direction);
|
||||||
|
},
|
||||||
onDismissed: (DismissDirection direction) {
|
onDismissed: (DismissDirection direction) {
|
||||||
setState(() {
|
setState(() {
|
||||||
reportedDismissDirection = direction;
|
reportedDismissDirection = direction;
|
||||||
@ -660,4 +667,63 @@ void main() {
|
|||||||
expect(find.text('1'), findsOneWidget);
|
expect(find.text('1'), findsOneWidget);
|
||||||
expect(dismissedItems, isEmpty);
|
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