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