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 'package:flutter/material.dart'; |
6 | /// |
7 | /// @docImport 'overscroll_indicator.dart'; |
8 | /// @docImport 'scroll_activity.dart'; |
9 | /// @docImport 'scroll_controller.dart'; |
10 | /// @docImport 'scroll_physics.dart'; |
11 | /// @docImport 'scroll_position.dart'; |
12 | /// @docImport 'scroll_view.dart'; |
13 | /// @docImport 'scrollable.dart'; |
14 | /// @docImport 'viewport.dart'; |
15 | library; |
16 | |
17 | import 'package:flutter/gestures.dart'; |
18 | import 'package:flutter/rendering.dart'; |
19 | |
20 | import 'framework.dart'; |
21 | import 'notification_listener.dart'; |
22 | import 'scroll_metrics.dart'; |
23 | |
24 | /// Mixin for [Notification]s that track how many [RenderAbstractViewport] they |
25 | /// have bubbled through. |
26 | /// |
27 | /// This is used by [ScrollNotification] and [OverscrollIndicatorNotification]. |
28 | mixin ViewportNotificationMixin on Notification { |
29 | /// The number of viewports that this notification has bubbled through. |
30 | /// |
31 | /// Typically listeners only respond to notifications with a [depth] of zero. |
32 | /// |
33 | /// Specifically, this is the number of [Widget]s representing |
34 | /// [RenderAbstractViewport] render objects through which this notification |
35 | /// has bubbled. |
36 | int get depth => _depth; |
37 | int _depth = 0; |
38 | |
39 | @override |
40 | void debugFillDescription(List<String> description) { |
41 | super.debugFillDescription(description); |
42 | description.add('depth: $depth ( ${ depth == 0 ? "local" : "remote" })' ); |
43 | } |
44 | } |
45 | |
46 | /// A mixin that allows [Element]s containing [Viewport] like widgets to correctly |
47 | /// modify the notification depth of a [ViewportNotificationMixin]. |
48 | /// |
49 | /// See also: |
50 | /// * [Viewport], which creates a custom [MultiChildRenderObjectElement] that mixes |
51 | /// this in. |
52 | mixin ViewportElementMixin on NotifiableElementMixin { |
53 | @override |
54 | bool onNotification(Notification notification) { |
55 | if (notification is ViewportNotificationMixin) { |
56 | notification._depth += 1; |
57 | } |
58 | return false; |
59 | } |
60 | } |
61 | |
62 | /// A [Notification] related to scrolling. |
63 | /// |
64 | /// [Scrollable] widgets notify their ancestors about scrolling-related changes. |
65 | /// The notifications have the following lifecycle: |
66 | /// |
67 | /// * A [ScrollStartNotification], which indicates that the widget has started |
68 | /// scrolling. |
69 | /// * Zero or more [ScrollUpdateNotification]s, which indicate that the widget |
70 | /// has changed its scroll position, mixed with zero or more |
71 | /// [OverscrollNotification]s, which indicate that the widget has not changed |
72 | /// its scroll position because the change would have caused its scroll |
73 | /// position to go outside its scroll bounds. |
74 | /// * Interspersed with the [ScrollUpdateNotification]s and |
75 | /// [OverscrollNotification]s are zero or more [UserScrollNotification]s, |
76 | /// which indicate that the user has changed the direction in which they are |
77 | /// scrolling. |
78 | /// * A [ScrollEndNotification], which indicates that the widget has stopped |
79 | /// scrolling. |
80 | /// * A [UserScrollNotification], with a [UserScrollNotification.direction] of |
81 | /// [ScrollDirection.idle]. |
82 | /// |
83 | /// Notifications bubble up through the tree, which means a given |
84 | /// [NotificationListener] will receive notifications for all descendant |
85 | /// [Scrollable] widgets. To focus on notifications from the nearest |
86 | /// [Scrollable] descendant, check that the [depth] property of the notification |
87 | /// is zero. |
88 | /// |
89 | /// When a scroll notification is received by a [NotificationListener], the |
90 | /// listener will have already completed build and layout, and it is therefore |
91 | /// too late for that widget to call [State.setState]. Any attempt to adjust the |
92 | /// build or layout based on a scroll notification would result in a layout that |
93 | /// lagged one frame behind, which is a poor user experience. Scroll |
94 | /// notifications are therefore primarily useful for paint effects (since paint |
95 | /// happens after layout). The [GlowingOverscrollIndicator] and [Scrollbar] |
96 | /// widgets are examples of paint effects that use scroll notifications. |
97 | /// |
98 | /// {@tool dartpad} |
99 | /// This sample shows the difference between using a [ScrollController] or a |
100 | /// [NotificationListener] of type [ScrollNotification] to listen to scrolling |
101 | /// activities. Toggling the [Radio] button switches between the two. |
102 | /// Using a [ScrollNotification] will provide details about the scrolling |
103 | /// activity, along with the metrics of the [ScrollPosition], but not the scroll |
104 | /// position object itself. By listening with a [ScrollController], the position |
105 | /// object is directly accessible. |
106 | /// Both of these types of notifications are only triggered by scrolling. |
107 | /// |
108 | /// ** See code in examples/api/lib/widgets/scroll_position/scroll_controller_notification.0.dart ** |
109 | /// {@end-tool} |
110 | /// |
111 | /// To drive layout based on the scroll position, consider listening to the |
112 | /// [ScrollPosition] directly (or indirectly via a [ScrollController]). This |
113 | /// will not notify when the [ScrollMetrics] of a given scroll position changes, |
114 | /// such as when the window is resized, changing the dimensions of the |
115 | /// [Viewport]. In order to listen to changes in scroll metrics, use a |
116 | /// [NotificationListener] of type [ScrollMetricsNotification]. |
117 | /// This type of notification differs from [ScrollNotification], as it is not |
118 | /// associated with the activity of scrolling, but rather the dimensions of |
119 | /// the scrollable area. |
120 | /// |
121 | /// {@tool dartpad} |
122 | /// This sample shows how a [ScrollMetricsNotification] is dispatched when |
123 | /// the `windowSize` is changed. Press the floating action button to increase |
124 | /// the scrollable window's size. |
125 | /// |
126 | /// ** See code in examples/api/lib/widgets/scroll_position/scroll_metrics_notification.0.dart ** |
127 | /// {@end-tool} |
128 | /// |
129 | abstract class ScrollNotification extends LayoutChangedNotification with ViewportNotificationMixin { |
130 | /// Initializes fields for subclasses. |
131 | ScrollNotification({ |
132 | required this.metrics, |
133 | required this.context, |
134 | }); |
135 | |
136 | /// A description of a [Scrollable]'s contents, useful for modeling the state |
137 | /// of its viewport. |
138 | final ScrollMetrics metrics; |
139 | |
140 | /// The build context of the widget that fired this notification. |
141 | /// |
142 | /// This can be used to find the scrollable's render objects to determine the |
143 | /// size of the viewport, for instance. |
144 | final BuildContext? context; |
145 | |
146 | @override |
147 | void debugFillDescription(List<String> description) { |
148 | super.debugFillDescription(description); |
149 | description.add(' $metrics' ); |
150 | } |
151 | } |
152 | |
153 | /// A notification that a [Scrollable] widget has started scrolling. |
154 | /// |
155 | /// See also: |
156 | /// |
157 | /// * [ScrollEndNotification], which indicates that scrolling has stopped. |
158 | /// * [ScrollNotification], which describes the notification lifecycle. |
159 | class ScrollStartNotification extends ScrollNotification { |
160 | /// Creates a notification that a [Scrollable] widget has started scrolling. |
161 | ScrollStartNotification({ |
162 | required super.metrics, |
163 | required super.context, |
164 | this.dragDetails, |
165 | }); |
166 | |
167 | /// If the [Scrollable] started scrolling because of a drag, the details about |
168 | /// that drag start. |
169 | /// |
170 | /// Otherwise, null. |
171 | final DragStartDetails? dragDetails; |
172 | |
173 | @override |
174 | void debugFillDescription(List<String> description) { |
175 | super.debugFillDescription(description); |
176 | if (dragDetails != null) { |
177 | description.add(' $dragDetails' ); |
178 | } |
179 | } |
180 | } |
181 | |
182 | /// A notification that a [Scrollable] widget has changed its scroll position. |
183 | /// |
184 | /// See also: |
185 | /// |
186 | /// * [OverscrollNotification], which indicates that a [Scrollable] widget |
187 | /// has not changed its scroll position because the change would have caused |
188 | /// its scroll position to go outside its scroll bounds. |
189 | /// * [ScrollNotification], which describes the notification lifecycle. |
190 | class ScrollUpdateNotification extends ScrollNotification { |
191 | /// Creates a notification that a [Scrollable] widget has changed its scroll |
192 | /// position. |
193 | ScrollUpdateNotification({ |
194 | required super.metrics, |
195 | required BuildContext super.context, |
196 | this.dragDetails, |
197 | this.scrollDelta, |
198 | int? depth, |
199 | }) { |
200 | if (depth != null) { |
201 | _depth = depth; |
202 | } |
203 | } |
204 | |
205 | /// If the [Scrollable] changed its scroll position because of a drag, the |
206 | /// details about that drag update. |
207 | /// |
208 | /// Otherwise, null. |
209 | final DragUpdateDetails? dragDetails; |
210 | |
211 | /// The distance by which the [Scrollable] was scrolled, in logical pixels. |
212 | final double? scrollDelta; |
213 | |
214 | @override |
215 | void debugFillDescription(List<String> description) { |
216 | super.debugFillDescription(description); |
217 | description.add('scrollDelta: $scrollDelta' ); |
218 | if (dragDetails != null) { |
219 | description.add(' $dragDetails' ); |
220 | } |
221 | } |
222 | } |
223 | |
224 | /// A notification that a [Scrollable] widget has not changed its scroll position |
225 | /// because the change would have caused its scroll position to go outside of |
226 | /// its scroll bounds. |
227 | /// |
228 | /// See also: |
229 | /// |
230 | /// * [ScrollUpdateNotification], which indicates that a [Scrollable] widget |
231 | /// has changed its scroll position. |
232 | /// * [ScrollNotification], which describes the notification lifecycle. |
233 | class OverscrollNotification extends ScrollNotification { |
234 | /// Creates a notification that a [Scrollable] widget has changed its scroll |
235 | /// position outside of its scroll bounds. |
236 | OverscrollNotification({ |
237 | required super.metrics, |
238 | required BuildContext super.context, |
239 | this.dragDetails, |
240 | required this.overscroll, |
241 | this.velocity = 0.0, |
242 | }) : assert(overscroll.isFinite), |
243 | assert(overscroll != 0.0); |
244 | |
245 | /// If the [Scrollable] overscrolled because of a drag, the details about that |
246 | /// drag update. |
247 | /// |
248 | /// Otherwise, null. |
249 | final DragUpdateDetails? dragDetails; |
250 | |
251 | /// The number of logical pixels that the [Scrollable] avoided scrolling. |
252 | /// |
253 | /// This will be negative for overscroll on the "start" side and positive for |
254 | /// overscroll on the "end" side. |
255 | final double overscroll; |
256 | |
257 | /// The velocity at which the [ScrollPosition] was changing when this |
258 | /// overscroll happened. |
259 | /// |
260 | /// This will typically be 0.0 for touch-driven overscrolls, and positive |
261 | /// for overscrolls that happened from a [BallisticScrollActivity] or |
262 | /// [DrivenScrollActivity]. |
263 | final double velocity; |
264 | |
265 | @override |
266 | void debugFillDescription(List<String> description) { |
267 | super.debugFillDescription(description); |
268 | description.add('overscroll: ${overscroll.toStringAsFixed(1)}' ); |
269 | description.add('velocity: ${velocity.toStringAsFixed(1)}' ); |
270 | if (dragDetails != null) { |
271 | description.add(' $dragDetails' ); |
272 | } |
273 | } |
274 | } |
275 | |
276 | /// A notification that a [Scrollable] widget has stopped scrolling. |
277 | /// |
278 | /// {@tool dartpad} |
279 | /// This sample shows how you can trigger an auto-scroll, which aligns the last |
280 | /// partially visible fixed-height list item, by listening for this |
281 | /// notification with a [NotificationListener]. This sort of thing can also |
282 | /// be done by listening to the [ScrollController]'s |
283 | /// [ScrollPosition.isScrollingNotifier]. An alternative example is provided |
284 | /// with [ScrollPosition.isScrollingNotifier]. |
285 | /// |
286 | /// ** See code in examples/api/lib/widgets/scroll_end_notification/scroll_end_notification.0.dart ** |
287 | /// {@end-tool} |
288 | /// |
289 | /// |
290 | /// {@tool dartpad} |
291 | /// This example auto-scrolls one special "aligned item" sliver to |
292 | /// the top or bottom of the viewport, whenever it's partially visible |
293 | /// (because it overlaps the top or bottom of the viewport). This |
294 | /// example differs from the previous one in that the layout of an |
295 | /// individual sliver is retrieved from its [RenderSliver] via a |
296 | /// [GlobalKey]. The example does not rely on all of the list items |
297 | /// having the same extent. |
298 | /// |
299 | /// ** See code in examples/api/lib/widgets/scroll_end_notification/scroll_end_notification.1.dart ** |
300 | /// {@end-tool} |
301 | /// See also: |
302 | /// |
303 | /// * [ScrollStartNotification], which indicates that scrolling has started. |
304 | /// * [ScrollNotification], which describes the notification lifecycle. |
305 | class ScrollEndNotification extends ScrollNotification { |
306 | /// Creates a notification that a [Scrollable] widget has stopped scrolling. |
307 | ScrollEndNotification({ |
308 | required super.metrics, |
309 | required BuildContext super.context, |
310 | this.dragDetails, |
311 | }); |
312 | |
313 | /// If the [Scrollable] stopped scrolling because of a drag, the details about |
314 | /// that drag end. |
315 | /// |
316 | /// Otherwise, null. |
317 | /// |
318 | /// If a drag ends with some residual velocity, a typical [ScrollPhysics] will |
319 | /// start a ballistic scroll, which delays the [ScrollEndNotification] until |
320 | /// the ballistic simulation completes, at which time [dragDetails] will |
321 | /// be null. If the residual velocity is too small to trigger ballistic |
322 | /// scrolling, then the [ScrollEndNotification] will be dispatched immediately |
323 | /// and [dragDetails] will be non-null. |
324 | final DragEndDetails? dragDetails; |
325 | |
326 | @override |
327 | void debugFillDescription(List<String> description) { |
328 | super.debugFillDescription(description); |
329 | if (dragDetails != null) { |
330 | description.add(' $dragDetails' ); |
331 | } |
332 | } |
333 | } |
334 | |
335 | /// A notification that the user has changed the [ScrollDirection] in which they |
336 | /// are scrolling, or have stopped scrolling. |
337 | /// |
338 | /// For the direction that the [ScrollView] is oriented to, and the direction |
339 | /// contents are being laid out in, see [AxisDirection] & [GrowthDirection]. |
340 | /// |
341 | /// {@macro flutter.rendering.ScrollDirection.sample} |
342 | /// |
343 | /// See also: |
344 | /// |
345 | /// * [ScrollNotification], which describes the notification lifecycle. |
346 | class UserScrollNotification extends ScrollNotification { |
347 | /// Creates a notification that the user has changed the direction in which |
348 | /// they are scrolling. |
349 | UserScrollNotification({ |
350 | required super.metrics, |
351 | required BuildContext super.context, |
352 | required this.direction, |
353 | }); |
354 | |
355 | /// The direction in which the user is scrolling. |
356 | /// |
357 | /// This does not represent the current [AxisDirection] or [GrowthDirection] |
358 | /// of the [Viewport], which respectively represent the direction that the |
359 | /// scroll offset is increasing in, and the direction that contents are being |
360 | /// laid out in. |
361 | /// |
362 | /// {@macro flutter.rendering.ScrollDirection.sample} |
363 | final ScrollDirection direction; |
364 | |
365 | @override |
366 | void debugFillDescription(List<String> description) { |
367 | super.debugFillDescription(description); |
368 | description.add('direction: $direction' ); |
369 | } |
370 | } |
371 | |
372 | /// A predicate for [ScrollNotification], used to customize widgets that |
373 | /// listen to notifications from their children. |
374 | typedef ScrollNotificationPredicate = bool Function(ScrollNotification notification); |
375 | |
376 | /// A [ScrollNotificationPredicate] that checks whether |
377 | /// `notification.depth == 0`, which means that the notification did not bubble |
378 | /// through any intervening scrolling widgets. |
379 | bool defaultScrollNotificationPredicate(ScrollNotification notification) { |
380 | return notification.depth == 0; |
381 | } |
382 | |