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