diff --git a/packages/flutter/lib/src/material/overscroll_indicator.dart b/packages/flutter/lib/src/material/overscroll_indicator.dart index 1a9df12d1f..dff16388c1 100644 --- a/packages/flutter/lib/src/material/overscroll_indicator.dart +++ b/packages/flutter/lib/src/material/overscroll_indicator.dart @@ -188,6 +188,8 @@ class _OverscrollIndicatorState extends State { } 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 5a787881f3..2c1c7ef816 100644 --- a/packages/flutter/lib/src/widgets/notification_listener.dart +++ b/packages/flutter/lib/src/widgets/notification_listener.dart @@ -9,18 +9,22 @@ typedef bool NotificationListenerCallback(T notification /// 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) { + final NotificationListener 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) { - final NotificationListener 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 5faf123de0..963e5f32d4 100644 --- a/packages/flutter/lib/src/widgets/scrollable.dart +++ b/packages/flutter/lib/src/widgets/scrollable.dart @@ -710,7 +710,11 @@ enum ScrollNotificationKind { 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 @@ class ScrollNotification extends Notification { /// 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 0000000000..a044f5811b --- /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( + 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 depth0Kinds = []; + final List depth1Kinds = []; + final List depth0Values = []; + final List depth1Values = []; + + await tester.pumpWidget(new NotificationListener( + 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( + 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 kinds = [ + ScrollNotificationKind.started, + ScrollNotificationKind.updated, + ScrollNotificationKind.ended + ]; + expect(depth0Kinds, equals(kinds)); + expect(depth1Kinds, equals(kinds)); + + expect(depth0Values, equals([0, 0, 0])); + expect(depth1Values, equals([1, 1, 1])); + }); +}