Correct scroll notifications for NestedScrollView (#96482)

diff --git a/packages/flutter/lib/src/widgets/nested_scroll_view.dart b/packages/flutter/lib/src/widgets/nested_scroll_view.dart
index fb994e4..a44118b 100644
--- a/packages/flutter/lib/src/widgets/nested_scroll_view.dart
+++ b/packages/flutter/lib/src/widgets/nested_scroll_view.dart
@@ -890,6 +890,9 @@
   }
 
   void pointerScroll(double delta) {
+    // If an update is made to pointer scrolling here, consider if the same
+    // (or similar) change should be made in
+    // ScrollPositionWithSingleContext.pointerScroll.
     assert(delta != 0.0);
 
     goIdle();
@@ -897,12 +900,15 @@
         delta < 0.0 ? ScrollDirection.forward : ScrollDirection.reverse,
     );
 
-    // Set the isScrollingNotifier. Even if only one position actually receives
+    // Handle notifications. Even if only one position actually receives
     // the delta, the NestedScrollView's intention is to treat multiple
     // ScrollPositions as one.
     _outerPosition!.isScrollingNotifier.value = true;
-    for (final _NestedScrollPosition position in _innerPositions)
+    _outerPosition!.didStartScroll();
+    for (final _NestedScrollPosition position in _innerPositions) {
       position.isScrollingNotifier.value = true;
+      position.didStartScroll();
+    }
 
     if (_innerPositions.isEmpty) {
       // Does not enter overscroll.
@@ -950,6 +956,11 @@
           _outerPosition!.applyClampedPointerSignalUpdate(outerDelta);
       }
     }
+
+    _outerPosition!.didEndScroll();
+    for (final _NestedScrollPosition position in _innerPositions) {
+      position.didEndScroll();
+    }
     goBallistic(0.0);
   }
 
diff --git a/packages/flutter/lib/src/widgets/scroll_position_with_single_context.dart b/packages/flutter/lib/src/widgets/scroll_position_with_single_context.dart
index 09dea8a..ab25d87 100644
--- a/packages/flutter/lib/src/widgets/scroll_position_with_single_context.dart
+++ b/packages/flutter/lib/src/widgets/scroll_position_with_single_context.dart
@@ -205,6 +205,9 @@
 
   @override
   void pointerScroll(double delta) {
+    // If an update is made to pointer scrolling here, consider if the same
+    // (or similar) change should be made in
+    // _NestedScrollCoordinator.pointerScroll.
     assert(delta != 0.0);
 
     final double targetPixels =
diff --git a/packages/flutter/test/widgets/nested_scroll_view_test.dart b/packages/flutter/test/widgets/nested_scroll_view_test.dart
index da54000..8bf26e4 100644
--- a/packages/flutter/test/widgets/nested_scroll_view_test.dart
+++ b/packages/flutter/test/widgets/nested_scroll_view_test.dart
@@ -2491,6 +2491,68 @@
     await tester.fling(find.text('Item 25'), const Offset(0.0, -50.0), 4000.0);
     await tester.pumpAndSettle();
   });
+
+  testWidgets('NestedScrollViewCoordinator.pointerScroll dispatches correct scroll notifications', (WidgetTester tester) async {
+    int scrollEnded = 0;
+    int scrollStarted = 0;
+    bool isScrolled = false;
+
+    await tester.pumpWidget(MaterialApp(
+      home: NotificationListener<ScrollNotification>(
+        onNotification: (ScrollNotification notification) {
+          if (notification is ScrollStartNotification) {
+            scrollStarted += 1;
+          } else if (notification is ScrollEndNotification) {
+            scrollEnded += 1;
+          }
+          return false;
+        },
+        child: Scaffold(
+          body: NestedScrollView(
+            headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
+              isScrolled = innerBoxIsScrolled;
+              return <Widget>[
+                const SliverAppBar(
+                  expandedHeight: 250.0,
+                ),
+              ];
+            },
+            body: CustomScrollView(
+              physics: const BouncingScrollPhysics(),
+              slivers: <Widget>[
+                SliverPadding(
+                  padding: const EdgeInsets.all(8.0),
+                  sliver: SliverFixedExtentList(
+                    itemExtent: 48.0,
+                    delegate: SliverChildBuilderDelegate(
+                          (BuildContext context, int index) {
+                        return ListTile(
+                          title: Text('Item $index'),
+                        );
+                      },
+                      childCount: 30,
+                    ),
+                  ),
+                ),
+              ],
+            ),
+          ),
+        ),
+      ),
+    ));
+
+    final Offset scrollEventLocation = tester.getCenter(find.byType(NestedScrollView));
+    final TestPointer testPointer = TestPointer(1, ui.PointerDeviceKind.mouse);
+    // Create a hover event so that |testPointer| has a location when generating the scroll.
+    testPointer.hover(scrollEventLocation);
+    await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 300.0)));
+    await tester.pumpAndSettle();
+
+    expect(isScrolled, isTrue);
+    // There should have been a notification for each nested position (2).
+    expect(scrollStarted, 2);
+    expect(scrollEnded, 2);
+  });
 }
 
 class TestHeader extends SliverPersistentHeaderDelegate {