| // Copyright 2014 The Flutter 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 'dart:collection'; |
| |
| import 'package:flutter/foundation.dart'; |
| |
| import 'framework.dart'; |
| import 'notification_listener.dart'; |
| import 'scroll_notification.dart'; |
| import 'scroll_position.dart'; |
| |
| // Examples can assume: |
| // void _listener(ScrollNotification notification) { } |
| // late BuildContext context; |
| |
| /// A [ScrollNotification] listener for [ScrollNotificationObserver]. |
| /// |
| /// [ScrollNotificationObserver] is similar to |
| /// [NotificationListener]. It supports a listener list instead of |
| /// just a single listener and its listeners run unconditionally, they |
| /// do not require a gating boolean return value. |
| typedef ScrollNotificationCallback = void Function(ScrollNotification notification); |
| |
| class _ScrollNotificationObserverScope extends InheritedWidget { |
| const _ScrollNotificationObserverScope({ |
| required super.child, |
| required ScrollNotificationObserverState scrollNotificationObserverState, |
| }) : _scrollNotificationObserverState = scrollNotificationObserverState; |
| |
| final ScrollNotificationObserverState _scrollNotificationObserverState; |
| |
| @override |
| bool updateShouldNotify(_ScrollNotificationObserverScope old) => _scrollNotificationObserverState != old._scrollNotificationObserverState; |
| } |
| |
| class _ListenerEntry extends LinkedListEntry<_ListenerEntry> { |
| _ListenerEntry(this.listener); |
| final ScrollNotificationCallback listener; |
| } |
| |
| /// Notifies its listeners when a descendant scrolls. |
| /// |
| /// To add a listener to a [ScrollNotificationObserver] ancestor: |
| /// |
| /// ```dart |
| /// ScrollNotificationObserver.of(context)!.addListener(_listener); |
| /// ``` |
| /// |
| /// To remove the listener from a [ScrollNotificationObserver] ancestor: |
| /// |
| /// ```dart |
| /// ScrollNotificationObserver.of(context)!.removeListener(_listener); |
| /// ``` |
| /// |
| /// Stateful widgets that share an ancestor [ScrollNotificationObserver] typically |
| /// add a listener in [State.didChangeDependencies] (removing the old one |
| /// if necessary) and remove the listener in their [State.dispose] method. |
| /// |
| /// Any function with the [ScrollNotificationCallback] signature can act as a |
| /// listener: |
| /// |
| /// ```dart |
| /// // (e.g. in a stateful widget) |
| /// void _listener(ScrollNotification notification) { |
| /// // Do something, maybe setState() |
| /// } |
| /// ``` |
| /// |
| /// This widget is similar to [NotificationListener]. It supports a listener |
| /// list instead of just a single listener and its listeners run |
| /// unconditionally, they do not require a gating boolean return value. |
| class ScrollNotificationObserver extends StatefulWidget { |
| /// Create a [ScrollNotificationObserver]. |
| /// |
| /// The [child] parameter must not be null. |
| const ScrollNotificationObserver({ |
| super.key, |
| required this.child, |
| }) : assert(child != null); |
| |
| /// The subtree below this widget. |
| final Widget child; |
| |
| /// The closest instance of this class that encloses the given context. |
| /// |
| /// If there is no enclosing [ScrollNotificationObserver] widget, then null is returned. |
| static ScrollNotificationObserverState? of(BuildContext context) { |
| return context.dependOnInheritedWidgetOfExactType<_ScrollNotificationObserverScope>()?._scrollNotificationObserverState; |
| } |
| |
| @override |
| ScrollNotificationObserverState createState() => ScrollNotificationObserverState(); |
| } |
| |
| /// The listener list state for a [ScrollNotificationObserver] returned by |
| /// [ScrollNotificationObserver.of]. |
| /// |
| /// [ScrollNotificationObserver] is similar to |
| /// [NotificationListener]. It supports a listener list instead of |
| /// just a single listener and its listeners run unconditionally, they |
| /// do not require a gating boolean return value. |
| class ScrollNotificationObserverState extends State<ScrollNotificationObserver> { |
| LinkedList<_ListenerEntry>? _listeners = LinkedList<_ListenerEntry>(); |
| |
| bool _debugAssertNotDisposed() { |
| assert(() { |
| if (_listeners == null) { |
| throw FlutterError( |
| 'A $runtimeType was used after being disposed.\n' |
| 'Once you have called dispose() on a $runtimeType, it can no longer be used.', |
| ); |
| } |
| return true; |
| }()); |
| return true; |
| } |
| |
| /// Add a [ScrollNotificationCallback] that will be called each time |
| /// a descendant scrolls. |
| void addListener(ScrollNotificationCallback listener) { |
| assert(_debugAssertNotDisposed()); |
| _listeners!.add(_ListenerEntry(listener)); |
| } |
| |
| /// Remove the specified [ScrollNotificationCallback]. |
| void removeListener(ScrollNotificationCallback listener) { |
| assert(_debugAssertNotDisposed()); |
| for (final _ListenerEntry entry in _listeners!) { |
| if (entry.listener == listener) { |
| entry.unlink(); |
| return; |
| } |
| } |
| } |
| |
| void _notifyListeners(ScrollNotification notification) { |
| assert(_debugAssertNotDisposed()); |
| if (_listeners!.isEmpty) { |
| return; |
| } |
| |
| final List<_ListenerEntry> localListeners = List<_ListenerEntry>.of(_listeners!); |
| for (final _ListenerEntry entry in localListeners) { |
| try { |
| if (entry.list != null) { |
| entry.listener(notification); |
| } |
| } catch (exception, stack) { |
| FlutterError.reportError(FlutterErrorDetails( |
| exception: exception, |
| stack: stack, |
| library: 'widget library', |
| context: ErrorDescription('while dispatching notifications for $runtimeType'), |
| informationCollector: () => <DiagnosticsNode>[ |
| DiagnosticsProperty<ScrollNotificationObserverState>( |
| 'The $runtimeType sending notification was', |
| this, |
| style: DiagnosticsTreeStyle.errorProperty, |
| ), |
| ], |
| )); |
| } |
| } |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| // A ScrollMetricsNotification allows listeners to be notified for an |
| // initial state, as well as if the content dimensions change without |
| // scrolling. |
| return NotificationListener<ScrollMetricsNotification>( |
| onNotification: (ScrollMetricsNotification notification) { |
| _notifyListeners(_ConvertedScrollMetricsNotification( |
| metrics: notification.metrics, |
| context: notification.context, |
| depth: notification.depth, |
| )); |
| return false; |
| }, |
| child: NotificationListener<ScrollNotification>( |
| onNotification: (ScrollNotification notification) { |
| _notifyListeners(notification); |
| return false; |
| }, |
| child: _ScrollNotificationObserverScope( |
| scrollNotificationObserverState: this, |
| child: widget.child, |
| ), |
| ), |
| ); |
| } |
| |
| @override |
| void dispose() { |
| assert(_debugAssertNotDisposed()); |
| _listeners = null; |
| super.dispose(); |
| } |
| } |
| |
| class _ConvertedScrollMetricsNotification extends ScrollUpdateNotification { |
| _ConvertedScrollMetricsNotification({ |
| required super.metrics, |
| required super.context, |
| required super.depth, |
| }); |
| } |