// 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 { 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: () => [ DiagnosticsProperty( '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( onNotification: (ScrollMetricsNotification notification) { _notifyListeners(_ConvertedScrollMetricsNotification( metrics: notification.metrics, context: notification.context, depth: notification.depth, )); return false; }, child: NotificationListener( 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, }); }