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';
15library;
16
17import 'package:flutter/gestures.dart';
18import 'package:flutter/rendering.dart';
19
20import 'framework.dart';
21import 'notification_listener.dart';
22import '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].
28mixin 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.
52mixin 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///
129abstract 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.
159class 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.
190class 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.
233class 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.
305class 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.
346class 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.
374typedef 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.
379bool defaultScrollNotificationPredicate(ScrollNotification notification) {
380 return notification.depth == 0;
381}
382