Report progress on Dismissible update callback (#95504)

diff --git a/packages/flutter/lib/src/widgets/dismissible.dart b/packages/flutter/lib/src/widgets/dismissible.dart
index 0a58d93..9210c4d 100644
--- a/packages/flutter/lib/src/widgets/dismissible.dart
+++ b/packages/flutter/lib/src/widgets/dismissible.dart
@@ -233,7 +233,8 @@
   DismissUpdateDetails({
     this.direction = DismissDirection.horizontal,
     this.reached = false,
-    this.previousReached = false
+    this.previousReached = false,
+    this.progress = 0.0,
   });
 
   /// The direction that the dismissible is being dragged.
@@ -247,6 +248,15 @@
   /// This can be used in conjunction with [DismissUpdateDetails.reached] to catch the moment
   /// that the [Dismissible] is dragged across the threshold.
   final bool previousReached;
+
+  /// The offset ratio of the dismissible in its parent container.
+  ///
+  /// A value of 0.0 represents the normal position and 1.0 means the child is
+  /// completely outside its parent.
+  ///
+  /// This can be used to synchronize other elements to what the dismissible is doing on screen,
+  /// e.g. using this value to set the opacity thereby fading dismissible as it's dragged offscreen.
+  final double progress;
 }
 
 class _DismissibleClipper extends CustomClipper<Rect> {
@@ -438,6 +448,7 @@
           direction: _dismissDirection,
           reached: _dismissThresholdReached,
           previousReached: oldDismissThresholdReached,
+          progress: _moveController!.value,
       );
       widget.onUpdate!(details);
     }
diff --git a/packages/flutter/test/widgets/dismissible_test.dart b/packages/flutter/test/widgets/dismissible_test.dart
index c8b3511..0b807d8 100644
--- a/packages/flutter/test/widgets/dismissible_test.dart
+++ b/packages/flutter/test/widgets/dismissible_test.dart
@@ -12,6 +12,7 @@
 const double crossAxisEndOffset = 0.5;
 bool reportedDismissUpdateReached = false;
 bool reportedDismissUpdatePreviousReached = false;
+double reportedDismissUpdateProgress = 0.0;
 late DismissDirection reportedDismissUpdateReachedDirection;
 
 DismissDirection reportedDismissDirection = DismissDirection.horizontal;
@@ -53,6 +54,7 @@
               reportedDismissUpdateReachedDirection = details.direction;
               reportedDismissUpdateReached = details.reached;
               reportedDismissUpdatePreviousReached = details.previousReached;
+              reportedDismissUpdateProgress = details.progress;
             },
             background: background,
             dismissThresholds: startToEndThreshold == null
@@ -120,6 +122,25 @@
   await gesture.up();
 }
 
+Future<void> dragElement(WidgetTester tester, Finder finder, { required AxisDirection gestureDirection, required double amount }) async {
+  Offset delta;
+  switch (gestureDirection) {
+    case AxisDirection.left:
+      delta = Offset(-amount, 0.0);
+      break;
+    case AxisDirection.right:
+      delta = Offset(amount, 0.0);
+      break;
+    case AxisDirection.up:
+      delta = Offset(0.0, -amount);
+      break;
+    case AxisDirection.down:
+      delta = Offset(0.0, amount);
+      break;
+  }
+  await tester.drag(finder, delta);
+}
+
 Future<void> flingElement(WidgetTester tester, Finder finder, { required AxisDirection gestureDirection, double initialOffsetFactor = 0.0 }) async {
   Offset delta;
   switch (gestureDirection) {
@@ -161,6 +182,20 @@
   await tester.pumpAndSettle();
 }
 
+Future<void> dragItem(
+    WidgetTester tester,
+    int item, {
+      required AxisDirection gestureDirection,
+      required double amount,
+    }) async {
+  assert(gestureDirection != null);
+  final Finder itemFinder = find.text(item.toString());
+  expect(itemFinder, findsOneWidget);
+
+  await dragElement(tester, itemFinder, gestureDirection: gestureDirection, amount: amount);
+  await tester.pump();
+}
+
 Future<void> checkFlingItemBeforeMovementEnd(
   WidgetTester tester,
   int item, {
@@ -1068,6 +1103,10 @@
     ));
     expect(dismissedItems, isEmpty);
 
+    // Unsuccessful dismiss, fractional progress reported
+    await dragItem(tester, 0, gestureDirection: AxisDirection.right, amount: 20);
+    expect(reportedDismissUpdateProgress, 0.2);
+
     // Successful dismiss therefore threshold has been reached
     await dismissItem(tester, 0, mechanism: flingElement, gestureDirection: AxisDirection.left);
     expect(find.text('0'), findsNothing);
@@ -1075,6 +1114,7 @@
     expect(reportedDismissUpdateReachedDirection, DismissDirection.endToStart);
     expect(reportedDismissUpdateReached, true);
     expect(reportedDismissUpdatePreviousReached, true);
+    expect(reportedDismissUpdateProgress, 1.0);
 
     // Unsuccessful dismiss, threshold has not been reached
     await checkFlingItemAfterMovement(tester, 1, gestureDirection: AxisDirection.right);
@@ -1083,6 +1123,7 @@
     expect(reportedDismissUpdateReachedDirection, DismissDirection.startToEnd);
     expect(reportedDismissUpdateReached, false);
     expect(reportedDismissUpdatePreviousReached, false);
+    expect(reportedDismissUpdateProgress, 0.0);
 
     // Another successful dismiss from another direction
     await dismissItem(tester, 1, mechanism: flingElement, gestureDirection: AxisDirection.right);
@@ -1091,6 +1132,7 @@
     expect(reportedDismissUpdateReachedDirection, DismissDirection.startToEnd);
     expect(reportedDismissUpdateReached, true);
     expect(reportedDismissUpdatePreviousReached, true);
+    expect(reportedDismissUpdateProgress, 1.0);
 
     await tester.pumpWidget(buildTest(
       scrollDirection: Axis.horizontal,
@@ -1106,5 +1148,6 @@
     expect(reportedDismissUpdateReachedDirection, DismissDirection.startToEnd);
     expect(reportedDismissUpdateReached, false);
     expect(reportedDismissUpdatePreviousReached, false);
+    expect(reportedDismissUpdateProgress, 0.0);
   });
 }