Report ScrollNotification depth (#4992)

diff --git a/packages/flutter/lib/src/material/overscroll_indicator.dart b/packages/flutter/lib/src/material/overscroll_indicator.dart
index 1a9df12..dff1638 100644
--- a/packages/flutter/lib/src/material/overscroll_indicator.dart
+++ b/packages/flutter/lib/src/material/overscroll_indicator.dart
@@ -188,6 +188,8 @@
   }
 
   bool _handleScrollNotification(ScrollNotification notification) {
+    if (notification.depth != 0)
+      return false;
     if (config.scrollableKey == null || config.scrollableKey == notification.scrollable.config.key) {
       final ScrollableState scrollable = notification.scrollable;
       switch(notification.kind) {
diff --git a/packages/flutter/lib/src/widgets/notification_listener.dart b/packages/flutter/lib/src/widgets/notification_listener.dart
index 5a78788..2c1c7ef 100644
--- a/packages/flutter/lib/src/widgets/notification_listener.dart
+++ b/packages/flutter/lib/src/widgets/notification_listener.dart
@@ -9,18 +9,22 @@
 
 /// A notification that can bubble up the widget tree.
 abstract class Notification {
+  /// Applied to each ancestor of the [dispatch] target. Dispatches this
+  /// Notification to ancestor [NotificationListener] widgets.
+  bool visitAncestor(Element element) {
+    if (element is StatelessElement &&
+        element.widget is NotificationListener<Notification>) {
+      final NotificationListener<Notification> widget = element.widget;
+      if (widget._dispatch(this)) // that function checks the type dynamically
+        return false;
+    }
+    return true;
+  }
+
   /// Start bubbling this notification at the given build context.
   void dispatch(BuildContext target) {
     assert(target != null); // Only call dispatch if the widget's State is still mounted.
-    target.visitAncestorElements((Element element) {
-      if (element is StatelessElement &&
-          element.widget is NotificationListener<Notification>) {
-        final NotificationListener<Notification> widget = element.widget;
-        if (widget._dispatch(this)) // that function checks the type dynamically
-          return false;
-      }
-      return true;
-    });
+    target.visitAncestorElements(visitAncestor);
   }
 }
 
diff --git a/packages/flutter/lib/src/widgets/scrollable.dart b/packages/flutter/lib/src/widgets/scrollable.dart
index 5faf123..963e5f3 100644
--- a/packages/flutter/lib/src/widgets/scrollable.dart
+++ b/packages/flutter/lib/src/widgets/scrollable.dart
@@ -710,7 +710,11 @@
   ended
 }
 
-/// Indicates that a descendant scrollable has scrolled.
+/// Indicates that a scrollable descendant is scrolling.
+///
+/// See also:
+///
+/// * [NotificationListener]
 class ScrollNotification extends Notification {
   /// Creates a notification about scrolling.
   ScrollNotification(this.scrollable, this.kind);
@@ -720,6 +724,19 @@
 
   /// The scrollable that scrolled.
   final ScrollableState scrollable;
+
+  /// The number of scrollable widgets that have already received this
+  /// notification. Typically listeners only respond to notifications
+  /// with depth = 0.
+  int get depth => _depth;
+  int _depth = 0;
+
+  @override
+  bool visitAncestor(Element element) {
+    if (element is StatefulElement && element.state is ScrollableState)
+      _depth += 1;
+    return super.visitAncestor(element);
+  }
 }
 
 /// A simple scrolling widget that has a single child.
diff --git a/packages/flutter/test/widget/scroll_notification_test.dart b/packages/flutter/test/widget/scroll_notification_test.dart
new file mode 100644
index 0000000..a044f58
--- /dev/null
+++ b/packages/flutter/test/widget/scroll_notification_test.dart
@@ -0,0 +1,86 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_test/flutter_test.dart';
+import 'package:flutter/widgets.dart';
+
+void main() {
+  testWidgets('Scroll notifcation basics', (WidgetTester tester) async {
+    ScrollNotification notification;
+
+    await tester.pumpWidget(new NotificationListener<ScrollNotification>(
+      onNotification: (ScrollNotification value) {
+        notification = value;
+        return false;
+      },
+      child: new ScrollableViewport(
+        child: new SizedBox(height: 1200.0)
+      )
+    ));
+
+    TestGesture gesture = await tester.startGesture(new Point(100.0, 100.0));
+    await tester.pump(const Duration(seconds: 1));
+    expect(notification.kind, equals(ScrollNotificationKind.started));
+    expect(notification.depth, equals(0));
+
+    await gesture.moveBy(new Offset(-10.0, -10.0));
+    await tester.pump(const Duration(seconds: 1));
+    expect(notification.kind, equals(ScrollNotificationKind.updated));
+    expect(notification.depth, equals(0));
+
+    await gesture.up();
+    await tester.pump(const Duration(seconds: 1));
+    expect(notification.kind, equals(ScrollNotificationKind.ended));
+    expect(notification.depth, equals(0));
+  });
+
+  testWidgets('Scroll notifcation depth', (WidgetTester tester) async {
+    final List<ScrollNotificationKind> depth0Kinds = <ScrollNotificationKind>[];
+    final List<ScrollNotificationKind> depth1Kinds = <ScrollNotificationKind>[];
+    final List<int> depth0Values = <int>[];
+    final List<int> depth1Values = <int>[];
+
+    await tester.pumpWidget(new NotificationListener<ScrollNotification>(
+      onNotification: (ScrollNotification value) {
+        depth1Kinds.add(value.kind);
+        depth1Values.add(value.depth);
+        return false;
+      },
+      child: new ScrollableViewport(
+        child: new SizedBox(
+          height: 1200.0,
+          child: new NotificationListener<ScrollNotification>(
+            onNotification: (ScrollNotification value) {
+              depth0Kinds.add(value.kind);
+              depth0Values.add(value.depth);
+              return false;
+            },
+            child: new Container(
+              padding: const EdgeInsets.all(50.0),
+              child: new ScrollableViewport(child: new SizedBox(height: 1200.0))
+            )
+          )
+        )
+      )
+    ));
+
+    TestGesture gesture = await tester.startGesture(new Point(100.0, 100.0));
+    await tester.pump(const Duration(seconds: 1));
+    await gesture.moveBy(new Offset(-10.0, -10.0));
+    await tester.pump(const Duration(seconds: 1));
+    await gesture.up();
+    await tester.pump(const Duration(seconds: 1));
+
+    final List<ScrollNotificationKind> kinds = <ScrollNotificationKind>[
+      ScrollNotificationKind.started,
+      ScrollNotificationKind.updated,
+      ScrollNotificationKind.ended
+    ];
+    expect(depth0Kinds, equals(kinds));
+    expect(depth1Kinds, equals(kinds));
+
+    expect(depth0Values, equals(<int>[0, 0, 0]));
+    expect(depth1Values, equals(<int>[1, 1, 1]));
+  });
+}