Add callback when dismiss threshold is reached (#88736)
This commit is contained in:
parent
ab5dfe1d2e
commit
30cc01fb23
@ -30,6 +30,11 @@ typedef DismissDirectionCallback = void Function(DismissDirection direction);
|
||||
/// Used by [Dismissible.confirmDismiss].
|
||||
typedef ConfirmDismissCallback = Future<bool?> Function(DismissDirection direction);
|
||||
|
||||
/// Signature used by [Dismissible] to indicate that the dismissible has been dragged.
|
||||
///
|
||||
/// Used by [Dismissible.onUpdate].
|
||||
typedef DismissUpdateCallback = void Function(DismissUpdateDetails details);
|
||||
|
||||
/// The direction in which a [Dismissible] can be dismissed.
|
||||
enum DismissDirection {
|
||||
/// The [Dismissible] can be dismissed by dragging either up or down.
|
||||
@ -98,6 +103,7 @@ class Dismissible extends StatefulWidget {
|
||||
this.secondaryBackground,
|
||||
this.confirmDismiss,
|
||||
this.onResize,
|
||||
this.onUpdate,
|
||||
this.onDismissed,
|
||||
this.direction = DismissDirection.horizontal,
|
||||
this.resizeDuration = const Duration(milliseconds: 300),
|
||||
@ -205,10 +211,44 @@ class Dismissible extends StatefulWidget {
|
||||
/// This defaults to [HitTestBehavior.opaque].
|
||||
final HitTestBehavior behavior;
|
||||
|
||||
/// Called when the dismissible widget has been dragged.
|
||||
///
|
||||
/// If [onUpdate] is not null, then it will be invoked for every pointer event
|
||||
/// to dispatch the latest state of the drag. For example, this callback
|
||||
/// can be used to for example change the color of the background widget
|
||||
/// depending on whether the dismiss threshold is currently reached.
|
||||
final DismissUpdateCallback? onUpdate;
|
||||
|
||||
@override
|
||||
State<Dismissible> createState() => _DismissibleState();
|
||||
}
|
||||
|
||||
/// Details for [DismissUpdateCallback].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [Dismissible.onUpdate], which receives this information.
|
||||
class DismissUpdateDetails {
|
||||
/// Create a new instance of [DismissUpdateDetails].
|
||||
DismissUpdateDetails({
|
||||
this.direction = DismissDirection.horizontal,
|
||||
this.reached = false,
|
||||
this.previousReached = false
|
||||
});
|
||||
|
||||
/// The direction that the dismissible is being dragged.
|
||||
final DismissDirection direction;
|
||||
|
||||
/// Whether the dismiss threshold is currently reached.
|
||||
final bool reached;
|
||||
|
||||
/// Whether the dismiss threshold was reached the last time this callback was invoked.
|
||||
///
|
||||
/// This can be used in conjunction with [DismissUpdateDetails.reached] to catch the moment
|
||||
/// that the [Dismissible] is dragged across the threshold.
|
||||
final bool previousReached;
|
||||
}
|
||||
|
||||
class _DismissibleClipper extends CustomClipper<Rect> {
|
||||
_DismissibleClipper({
|
||||
required this.axis,
|
||||
@ -254,7 +294,8 @@ class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin
|
||||
void initState() {
|
||||
super.initState();
|
||||
_moveController = AnimationController(duration: widget.movementDuration, vsync: this)
|
||||
..addStatusListener(_handleDismissStatusChanged);
|
||||
..addStatusListener(_handleDismissStatusChanged)
|
||||
..addListener(_handleDismissUpdateValueChanged);
|
||||
_updateMoveAnimation();
|
||||
}
|
||||
|
||||
@ -268,6 +309,7 @@ class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin
|
||||
bool _confirming = false;
|
||||
bool _dragUnderway = false;
|
||||
Size? _sizePriorToCollapse;
|
||||
bool _dismissThresholdReached = false;
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => _moveController?.isAnimating == true || _resizeController?.isAnimating == true;
|
||||
@ -388,6 +430,19 @@ class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin
|
||||
}
|
||||
}
|
||||
|
||||
void _handleDismissUpdateValueChanged() {
|
||||
if(widget.onUpdate != null) {
|
||||
final bool oldDismissThresholdReached = _dismissThresholdReached;
|
||||
_dismissThresholdReached = _moveController!.value > (widget.dismissThresholds[_dismissDirection] ?? _kDismissThreshold);
|
||||
final DismissUpdateDetails details = DismissUpdateDetails(
|
||||
direction: _dismissDirection,
|
||||
reached: _dismissThresholdReached,
|
||||
previousReached: oldDismissThresholdReached,
|
||||
);
|
||||
widget.onUpdate!(details);
|
||||
}
|
||||
}
|
||||
|
||||
void _updateMoveAnimation() {
|
||||
final double end = _dragExtent.sign;
|
||||
_moveAnimation = _moveController!.drive(
|
||||
|
@ -10,6 +10,9 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
const DismissDirection defaultDismissDirection = DismissDirection.horizontal;
|
||||
const double crossAxisEndOffset = 0.5;
|
||||
bool reportedDismissUpdateReached = false;
|
||||
bool reportedDismissUpdatePreviousReached = false;
|
||||
late DismissDirection reportedDismissUpdateReachedDirection;
|
||||
|
||||
DismissDirection reportedDismissDirection = DismissDirection.horizontal;
|
||||
List<int> dismissedItems = <int>[];
|
||||
@ -46,6 +49,11 @@ Widget buildTest({
|
||||
onResize: () {
|
||||
expect(dismissedItems.contains(item), isFalse);
|
||||
},
|
||||
onUpdate: (DismissUpdateDetails details) {
|
||||
reportedDismissUpdateReachedDirection = details.direction;
|
||||
reportedDismissUpdateReached = details.reached;
|
||||
reportedDismissUpdatePreviousReached = details.previousReached;
|
||||
},
|
||||
background: background,
|
||||
dismissThresholds: startToEndThreshold == null
|
||||
? <DismissDirection, double>{}
|
||||
@ -1053,4 +1061,50 @@ void main() {
|
||||
expect(controller.offset, 100.0);
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
testWidgets('onUpdate', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(buildTest(
|
||||
scrollDirection: Axis.horizontal,
|
||||
));
|
||||
expect(dismissedItems, isEmpty);
|
||||
|
||||
// Successful dismiss therefore threshold has been reached
|
||||
await dismissItem(tester, 0, mechanism: flingElement, gestureDirection: AxisDirection.left);
|
||||
expect(find.text('0'), findsNothing);
|
||||
expect(dismissedItems, equals(<int>[0]));
|
||||
expect(reportedDismissUpdateReachedDirection, DismissDirection.endToStart);
|
||||
expect(reportedDismissUpdateReached, true);
|
||||
expect(reportedDismissUpdatePreviousReached, true);
|
||||
|
||||
// Unsuccessful dismiss, threshold has not been reached
|
||||
await checkFlingItemAfterMovement(tester, 1, gestureDirection: AxisDirection.right);
|
||||
expect(find.text('1'), findsOneWidget);
|
||||
expect(dismissedItems, equals(<int>[0]));
|
||||
expect(reportedDismissUpdateReachedDirection, DismissDirection.startToEnd);
|
||||
expect(reportedDismissUpdateReached, false);
|
||||
expect(reportedDismissUpdatePreviousReached, false);
|
||||
|
||||
// Another successful dismiss from another direction
|
||||
await dismissItem(tester, 1, mechanism: flingElement, gestureDirection: AxisDirection.right);
|
||||
expect(find.text('1'), findsNothing);
|
||||
expect(dismissedItems, equals(<int>[0, 1]));
|
||||
expect(reportedDismissUpdateReachedDirection, DismissDirection.startToEnd);
|
||||
expect(reportedDismissUpdateReached, true);
|
||||
expect(reportedDismissUpdatePreviousReached, true);
|
||||
|
||||
await tester.pumpWidget(buildTest(
|
||||
scrollDirection: Axis.horizontal,
|
||||
confirmDismiss: (BuildContext context, DismissDirection dismissDirection) {
|
||||
return Future<bool>.value(false);
|
||||
},
|
||||
));
|
||||
|
||||
// Threshold has been reached but dismiss was not confirmed
|
||||
await dismissItem(tester, 2, mechanism: flingElement, gestureDirection: AxisDirection.right);
|
||||
expect(find.text('2'), findsOneWidget);
|
||||
expect(dismissedItems, equals(<int>[0, 1]));
|
||||
expect(reportedDismissUpdateReachedDirection, DismissDirection.startToEnd);
|
||||
expect(reportedDismissUpdateReached, false);
|
||||
expect(reportedDismissUpdatePreviousReached, false);
|
||||
});
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user