1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
2 | // Use of this source code is governed by a BSD-style license that can be |
3 | // found in the LICENSE file. |
4 | |
5 | /// @docImport 'scroll_view.dart'; |
6 | library; |
7 | |
8 | import 'dart:collection'; |
9 | |
10 | import 'package:flutter/foundation.dart'; |
11 | |
12 | import 'framework.dart'; |
13 | import 'notification_listener.dart'; |
14 | import 'scroll_notification.dart'; |
15 | import 'scroll_position.dart'; |
16 | |
17 | // Examples can assume: |
18 | // void _listener(ScrollNotification notification) { } |
19 | // late BuildContext context; |
20 | |
21 | /// A [ScrollNotification] listener for [ScrollNotificationObserver]. |
22 | /// |
23 | /// [ScrollNotificationObserver] is similar to |
24 | /// [NotificationListener]. It supports a listener list instead of |
25 | /// just a single listener and its listeners run unconditionally, they |
26 | /// do not require a gating boolean return value. |
27 | typedef ScrollNotificationCallback = void Function(ScrollNotification notification); |
28 | |
29 | class _ScrollNotificationObserverScope extends InheritedWidget { |
30 | const _ScrollNotificationObserverScope({ |
31 | required super.child, |
32 | required ScrollNotificationObserverState scrollNotificationObserverState, |
33 | }) : _scrollNotificationObserverState = scrollNotificationObserverState; |
34 | |
35 | final ScrollNotificationObserverState _scrollNotificationObserverState; |
36 | |
37 | @override |
38 | bool updateShouldNotify(_ScrollNotificationObserverScope old) => _scrollNotificationObserverState != old._scrollNotificationObserverState; |
39 | } |
40 | |
41 | final class _ListenerEntry extends LinkedListEntry<_ListenerEntry> { |
42 | _ListenerEntry(this.listener); |
43 | final ScrollNotificationCallback listener; |
44 | } |
45 | |
46 | /// Notifies its listeners when a descendant scrolls. |
47 | /// |
48 | /// To add a listener to a [ScrollNotificationObserver] ancestor: |
49 | /// |
50 | /// ```dart |
51 | /// ScrollNotificationObserver.of(context).addListener(_listener); |
52 | /// ``` |
53 | /// |
54 | /// To remove the listener from a [ScrollNotificationObserver] ancestor: |
55 | /// |
56 | /// ```dart |
57 | /// ScrollNotificationObserver.of(context).removeListener(_listener); |
58 | /// ``` |
59 | /// |
60 | /// Stateful widgets that share an ancestor [ScrollNotificationObserver] typically |
61 | /// add a listener in [State.didChangeDependencies] (removing the old one |
62 | /// if necessary) and remove the listener in their [State.dispose] method. |
63 | /// |
64 | /// Any function with the [ScrollNotificationCallback] signature can act as a |
65 | /// listener: |
66 | /// |
67 | /// ```dart |
68 | /// // (e.g. in a stateful widget) |
69 | /// void _listener(ScrollNotification notification) { |
70 | /// // Do something, maybe setState() |
71 | /// } |
72 | /// ``` |
73 | /// |
74 | /// This widget is similar to [NotificationListener]. It supports a listener |
75 | /// list instead of just a single listener and its listeners run |
76 | /// unconditionally, they do not require a gating boolean return value. |
77 | /// |
78 | /// {@tool dartpad} |
79 | /// This sample shows a "Scroll to top" button that uses [ScrollNotificationObserver] |
80 | /// to listen for scroll notifications from [ListView]. The button is only visible |
81 | /// when the user has scrolled down. When pressed, the button animates the scroll |
82 | /// position of the [ListView] back to the top. |
83 | /// |
84 | /// ** See code in examples/api/lib/widgets/scroll_notification_observer/scroll_notification_observer.0.dart ** |
85 | /// {@end-tool} |
86 | class ScrollNotificationObserver extends StatefulWidget { |
87 | /// Create a [ScrollNotificationObserver]. |
88 | const ScrollNotificationObserver({ |
89 | super.key, |
90 | required this.child, |
91 | }); |
92 | |
93 | /// The subtree below this widget. |
94 | final Widget child; |
95 | |
96 | /// The closest instance of this class that encloses the given context. |
97 | /// |
98 | /// If there is no enclosing [ScrollNotificationObserver] widget, then null is |
99 | /// returned. |
100 | /// |
101 | /// Calling this method will create a dependency on the closest |
102 | /// [ScrollNotificationObserver] in the [context], if there is one. |
103 | /// |
104 | /// See also: |
105 | /// |
106 | /// * [ScrollNotificationObserver.of], which is similar to this method, but |
107 | /// asserts if no [ScrollNotificationObserver] ancestor is found. |
108 | static ScrollNotificationObserverState? maybeOf(BuildContext context) { |
109 | return context.dependOnInheritedWidgetOfExactType<_ScrollNotificationObserverScope>()?._scrollNotificationObserverState; |
110 | } |
111 | |
112 | /// The closest instance of this class that encloses the given context. |
113 | /// |
114 | /// If no ancestor is found, this method will assert in debug mode, and throw |
115 | /// an exception in release mode. |
116 | /// |
117 | /// Calling this method will create a dependency on the closest |
118 | /// [ScrollNotificationObserver] in the [context]. |
119 | /// |
120 | /// See also: |
121 | /// |
122 | /// * [ScrollNotificationObserver.maybeOf], which is similar to this method, |
123 | /// but returns null if no [ScrollNotificationObserver] ancestor is found. |
124 | static ScrollNotificationObserverState of(BuildContext context) { |
125 | final ScrollNotificationObserverState? observerState = maybeOf(context); |
126 | assert(() { |
127 | if (observerState == null) { |
128 | throw FlutterError( |
129 | 'ScrollNotificationObserver.of() was called with a context that does not contain a ' |
130 | 'ScrollNotificationObserver widget.\n' |
131 | 'No ScrollNotificationObserver widget ancestor could be found starting from the ' |
132 | 'context that was passed to ScrollNotificationObserver.of(). This can happen ' |
133 | 'because you are using a widget that looks for a ScrollNotificationObserver ' |
134 | 'ancestor, but no such ancestor exists.\n' |
135 | 'The context used was:\n' |
136 | ' $context' , |
137 | ); |
138 | } |
139 | return true; |
140 | }()); |
141 | return observerState!; |
142 | } |
143 | |
144 | @override |
145 | ScrollNotificationObserverState createState() => ScrollNotificationObserverState(); |
146 | } |
147 | |
148 | /// The listener list state for a [ScrollNotificationObserver] returned by |
149 | /// [ScrollNotificationObserver.of]. |
150 | /// |
151 | /// [ScrollNotificationObserver] is similar to |
152 | /// [NotificationListener]. It supports a listener list instead of |
153 | /// just a single listener and its listeners run unconditionally, they |
154 | /// do not require a gating boolean return value. |
155 | class ScrollNotificationObserverState extends State<ScrollNotificationObserver> { |
156 | LinkedList<_ListenerEntry>? _listeners = LinkedList<_ListenerEntry>(); |
157 | |
158 | bool _debugAssertNotDisposed() { |
159 | assert(() { |
160 | if (_listeners == null) { |
161 | throw FlutterError( |
162 | 'A $runtimeType was used after being disposed.\n' |
163 | 'Once you have called dispose() on a $runtimeType, it can no longer be used.' , |
164 | ); |
165 | } |
166 | return true; |
167 | }()); |
168 | return true; |
169 | } |
170 | |
171 | /// Add a [ScrollNotificationCallback] that will be called each time |
172 | /// a descendant scrolls. |
173 | void addListener(ScrollNotificationCallback listener) { |
174 | assert(_debugAssertNotDisposed()); |
175 | _listeners!.add(_ListenerEntry(listener)); |
176 | } |
177 | |
178 | /// Remove the specified [ScrollNotificationCallback]. |
179 | void removeListener(ScrollNotificationCallback listener) { |
180 | assert(_debugAssertNotDisposed()); |
181 | for (final _ListenerEntry entry in _listeners!) { |
182 | if (entry.listener == listener) { |
183 | entry.unlink(); |
184 | return; |
185 | } |
186 | } |
187 | } |
188 | |
189 | void _notifyListeners(ScrollNotification notification) { |
190 | assert(_debugAssertNotDisposed()); |
191 | if (_listeners!.isEmpty) { |
192 | return; |
193 | } |
194 | |
195 | final List<_ListenerEntry> localListeners = List<_ListenerEntry>.of(_listeners!); |
196 | for (final _ListenerEntry entry in localListeners) { |
197 | try { |
198 | if (entry.list != null) { |
199 | entry.listener(notification); |
200 | } |
201 | } catch (exception, stack) { |
202 | FlutterError.reportError(FlutterErrorDetails( |
203 | exception: exception, |
204 | stack: stack, |
205 | library: 'widget library' , |
206 | context: ErrorDescription('while dispatching notifications for $runtimeType' ), |
207 | informationCollector: () => <DiagnosticsNode>[ |
208 | DiagnosticsProperty<ScrollNotificationObserverState>( |
209 | 'The $runtimeType sending notification was' , |
210 | this, |
211 | style: DiagnosticsTreeStyle.errorProperty, |
212 | ), |
213 | ], |
214 | )); |
215 | } |
216 | } |
217 | } |
218 | |
219 | @override |
220 | Widget build(BuildContext context) { |
221 | return NotificationListener<ScrollMetricsNotification>( |
222 | onNotification: (ScrollMetricsNotification notification) { |
223 | // A ScrollMetricsNotification allows listeners to be notified for an |
224 | // initial state, as well as if the content dimensions change without |
225 | // scrolling. |
226 | _notifyListeners(notification.asScrollUpdate()); |
227 | return false; |
228 | }, |
229 | child: NotificationListener<ScrollNotification>( |
230 | onNotification: (ScrollNotification notification) { |
231 | _notifyListeners(notification); |
232 | return false; |
233 | }, |
234 | child: _ScrollNotificationObserverScope( |
235 | scrollNotificationObserverState: this, |
236 | child: widget.child, |
237 | ), |
238 | ), |
239 | ); |
240 | } |
241 | |
242 | @override |
243 | void dispose() { |
244 | assert(_debugAssertNotDisposed()); |
245 | _listeners = null; |
246 | super.dispose(); |
247 | } |
248 | } |
249 | |