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 'package:flutter/gestures.dart'; |
6 | import 'package:flutter/rendering.dart'; |
7 | |
8 | import 'framework.dart'; |
9 | import 'notification_listener.dart'; |
10 | import '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]. |
16 | mixin 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. |
40 | mixin 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 | /// |
117 | abstract 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. |
147 | class 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. |
178 | class 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. |
221 | class 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. |
270 | class 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. |
311 | class 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. |
339 | typedef 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. |
344 | bool defaultScrollNotificationPredicate(ScrollNotification notification) { |
345 | return notification.depth == 0; |
346 | } |
347 | |