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:async'; |
6 | |
7 | import 'package:flutter/foundation.dart'; |
8 | import 'package:flutter/gestures.dart'; |
9 | import 'package:flutter/physics.dart'; |
10 | import 'package:flutter/rendering.dart'; |
11 | import 'package:flutter/scheduler.dart'; |
12 | |
13 | import 'basic.dart'; |
14 | import 'framework.dart'; |
15 | import 'notification_listener.dart'; |
16 | import 'page_storage.dart'; |
17 | import 'scroll_activity.dart'; |
18 | import 'scroll_context.dart'; |
19 | import 'scroll_metrics.dart'; |
20 | import 'scroll_notification.dart'; |
21 | import 'scroll_physics.dart'; |
22 | |
23 | export 'scroll_activity.dart' show ScrollHoldController; |
24 | |
25 | /// The policy to use when applying the `alignment` parameter of |
26 | /// [ScrollPosition.ensureVisible]. |
27 | enum ScrollPositionAlignmentPolicy { |
28 | /// Use the `alignment` property of [ScrollPosition.ensureVisible] to decide |
29 | /// where to align the visible object. |
30 | explicit, |
31 | |
32 | /// Find the bottom edge of the scroll container, and scroll the container, if |
33 | /// necessary, to show the bottom of the object. |
34 | /// |
35 | /// For example, find the bottom edge of the scroll container. If the bottom |
36 | /// edge of the item is below the bottom edge of the scroll container, scroll |
37 | /// the item so that the bottom of the item is just visible. If the entire |
38 | /// item is already visible, then do nothing. |
39 | keepVisibleAtEnd, |
40 | |
41 | /// Find the top edge of the scroll container, and scroll the container if |
42 | /// necessary to show the top of the object. |
43 | /// |
44 | /// For example, find the top edge of the scroll container. If the top edge of |
45 | /// the item is above the top edge of the scroll container, scroll the item so |
46 | /// that the top of the item is just visible. If the entire item is already |
47 | /// visible, then do nothing. |
48 | keepVisibleAtStart, |
49 | } |
50 | |
51 | /// Determines which portion of the content is visible in a scroll view. |
52 | /// |
53 | /// The [pixels] value determines the scroll offset that the scroll view uses to |
54 | /// select which part of its content to display. As the user scrolls the |
55 | /// viewport, this value changes, which changes the content that is displayed. |
56 | /// |
57 | /// The [ScrollPosition] applies [physics] to scrolling, and stores the |
58 | /// [minScrollExtent] and [maxScrollExtent]. |
59 | /// |
60 | /// Scrolling is controlled by the current [activity], which is set by |
61 | /// [beginActivity]. [ScrollPosition] itself does not start any activities. |
62 | /// Instead, concrete subclasses, such as [ScrollPositionWithSingleContext], |
63 | /// typically start activities in response to user input or instructions from a |
64 | /// [ScrollController]. |
65 | /// |
66 | /// This object is a [Listenable] that notifies its listeners when [pixels] |
67 | /// changes. |
68 | /// |
69 | /// {@template flutter.widgets.scrollPosition.listening} |
70 | /// ### Accessing Scrolling Information |
71 | /// |
72 | /// There are several ways to acquire information about scrolling and |
73 | /// scrollable widgets, but each provides different types of information about |
74 | /// the scrolling activity, the position, and the dimensions of the [Viewport]. |
75 | /// |
76 | /// A [ScrollController] is a [Listenable]. It notifies its listeners whenever |
77 | /// any of the attached [ScrollPosition]s notify _their_ listeners, such as when |
78 | /// scrolling occurs. This is very similar to using a [NotificationListener] of |
79 | /// type [ScrollNotification] to listen to changes in the scroll position, with |
80 | /// the difference being that a notification listener will provide information |
81 | /// about the scrolling activity. A notification listener can further listen to |
82 | /// specific subclasses of [ScrollNotification], like [UserScrollNotification]. |
83 | /// |
84 | /// {@tool dartpad} |
85 | /// This sample shows the difference between using a [ScrollController] or a |
86 | /// [NotificationListener] of type [ScrollNotification] to listen to scrolling |
87 | /// activities. Toggling the [Radio] button switches between the two. |
88 | /// Using a [ScrollNotification] will provide details about the scrolling |
89 | /// activity, along with the metrics of the [ScrollPosition], but not the scroll |
90 | /// position object itself. By listening with a [ScrollController], the position |
91 | /// object is directly accessible. |
92 | /// Both of these types of notifications are only triggered by scrolling. |
93 | /// |
94 | /// ** See code in examples/api/lib/widgets/scroll_position/scroll_controller_notification.0.dart ** |
95 | /// {@end-tool} |
96 | /// |
97 | /// [ScrollController] does not notify its listeners when the list of |
98 | /// [ScrollPosition]s attached to the scroll controller changes. To listen to |
99 | /// the attaching and detaching of scroll positions to the controller, use the |
100 | /// [ScrollController.onAttach] and [ScrollController.onDetach] methods. This is |
101 | /// also useful for adding a listener to the |
102 | /// [ScrollPosition.isScrollingNotifier] when the position is created during the |
103 | /// build method of the [Scrollable]. |
104 | /// |
105 | /// At the time that a scroll position is attached, the [ScrollMetrics], such as |
106 | /// the [ScrollMetrics.maxScrollExtent], are not yet available. These are not |
107 | /// determined until the [Scrollable] has finished laying out its contents and |
108 | /// computing things like the full extent of that content. |
109 | /// [ScrollPosition.hasContentDimensions] can be used to know when the |
110 | /// metrics are available, or a [ScrollMetricsNotification] can be used, |
111 | /// discussed further below. |
112 | /// |
113 | /// {@tool dartpad} |
114 | /// This sample shows how to apply a listener to the |
115 | /// [ScrollPosition.isScrollingNotifier] using [ScrollController.onAttach]. |
116 | /// This is used to change the [AppBar]'s color when scrolling is occurring. |
117 | /// |
118 | /// ** See code in examples/api/lib/widgets/scroll_position/scroll_controller_on_attach.0.dart ** |
119 | /// {@end-tool} |
120 | /// |
121 | /// #### From a different context |
122 | /// |
123 | /// When needing to access scrolling information from a context that is within |
124 | /// the scrolling widget itself, use [Scrollable.of] to access the |
125 | /// [ScrollableState] and the [ScrollableState.position]. This would be the same |
126 | /// [ScrollPosition] attached to a [ScrollController]. |
127 | /// |
128 | /// When needing to access scrolling information from a context that is not an |
129 | /// ancestor of the scrolling widget, use [ScrollNotificationObserver]. This is |
130 | /// used by [AppBar] to create the scrolled under effect. Since [Scaffold.appBar] |
131 | /// is a separate subtree from the [Scaffold.body], scroll notifications would |
132 | /// not bubble up to the app bar. Use |
133 | /// [ScrollNotificationObserverState.addListener] to listen to scroll |
134 | /// notifications happening outside of the current context. |
135 | /// |
136 | /// #### Dimension changes |
137 | /// |
138 | /// Lastly, listening to a [ScrollController] or a [ScrollPosition] will |
139 | /// _not_ notify when the [ScrollMetrics] of a given scroll position changes, |
140 | /// such as when the window is resized, changing the dimensions of the |
141 | /// [Viewport] and the previously mentioned extents of the scrollable. In order |
142 | /// to listen to changes in scroll metrics, use a [NotificationListener] of type |
143 | /// [ScrollMetricsNotification]. This type of notification differs from |
144 | /// [ScrollNotification], as it is not associated with the activity of |
145 | /// scrolling, but rather the dimensions of the scrollable area, such as the |
146 | /// window size. |
147 | /// |
148 | /// {@tool dartpad} |
149 | /// This sample shows how a [ScrollMetricsNotification] is dispatched when |
150 | /// the `windowSize` is changed. Press the floating action button to increase |
151 | /// the scrollable window's size. |
152 | /// |
153 | /// ** See code in examples/api/lib/widgets/scroll_position/scroll_metrics_notification.0.dart ** |
154 | /// {@end-tool} |
155 | /// {@endtemplate} |
156 | /// |
157 | /// ## Subclassing ScrollPosition |
158 | /// |
159 | /// Over time, a [Scrollable] might have many different [ScrollPosition] |
160 | /// objects. For example, if [Scrollable.physics] changes type, [Scrollable] |
161 | /// creates a new [ScrollPosition] with the new physics. To transfer state from |
162 | /// the old instance to the new instance, subclasses implement [absorb]. See |
163 | /// [absorb] for more details. |
164 | /// |
165 | /// Subclasses also need to call [didUpdateScrollDirection] whenever |
166 | /// [userScrollDirection] changes values. |
167 | /// |
168 | /// See also: |
169 | /// |
170 | /// * [Scrollable], which uses a [ScrollPosition] to determine which portion of |
171 | /// its content to display. |
172 | /// * [ScrollController], which can be used with [ListView], [GridView] and |
173 | /// other scrollable widgets to control a [ScrollPosition]. |
174 | /// * [ScrollPositionWithSingleContext], which is the most commonly used |
175 | /// concrete subclass of [ScrollPosition]. |
176 | /// * [ScrollNotification] and [NotificationListener], which can be used to watch |
177 | /// the scroll position without using a [ScrollController]. |
178 | abstract class ScrollPosition extends ViewportOffset with ScrollMetrics { |
179 | /// Creates an object that determines which portion of the content is visible |
180 | /// in a scroll view. |
181 | ScrollPosition({ |
182 | required this.physics, |
183 | required this.context, |
184 | this.keepScrollOffset = true, |
185 | ScrollPosition? oldPosition, |
186 | this.debugLabel, |
187 | }) { |
188 | if (oldPosition != null) { |
189 | absorb(oldPosition); |
190 | } |
191 | if (keepScrollOffset) { |
192 | restoreScrollOffset(); |
193 | } |
194 | } |
195 | |
196 | /// How the scroll position should respond to user input. |
197 | /// |
198 | /// For example, determines how the widget continues to animate after the |
199 | /// user stops dragging the scroll view. |
200 | final ScrollPhysics physics; |
201 | |
202 | /// Where the scrolling is taking place. |
203 | /// |
204 | /// Typically implemented by [ScrollableState]. |
205 | final ScrollContext context; |
206 | |
207 | /// Save the current scroll offset with [PageStorage] and restore it if |
208 | /// this scroll position's scrollable is recreated. |
209 | /// |
210 | /// See also: |
211 | /// |
212 | /// * [ScrollController.keepScrollOffset] and [PageController.keepPage], which |
213 | /// create scroll positions and initialize this property. |
214 | // TODO(goderbauer): Deprecate this when state restoration supports all features of PageStorage. |
215 | final bool keepScrollOffset; |
216 | |
217 | /// A label that is used in the [toString] output. |
218 | /// |
219 | /// Intended to aid with identifying animation controller instances in debug |
220 | /// output. |
221 | final String? debugLabel; |
222 | |
223 | @override |
224 | double get minScrollExtent => _minScrollExtent!; |
225 | double? _minScrollExtent; |
226 | |
227 | @override |
228 | double get maxScrollExtent => _maxScrollExtent!; |
229 | double? _maxScrollExtent; |
230 | |
231 | @override |
232 | bool get hasContentDimensions => _minScrollExtent != null && _maxScrollExtent != null; |
233 | |
234 | /// The additional velocity added for a [forcePixels] change in a single |
235 | /// frame. |
236 | /// |
237 | /// This value is used by [recommendDeferredLoading] in addition to the |
238 | /// [activity]'s [ScrollActivity.velocity] to ask the [physics] whether or |
239 | /// not to defer loading. It accounts for the fact that a [forcePixels] call |
240 | /// may involve a [ScrollActivity] with 0 velocity, but the scrollable is |
241 | /// still instantaneously moving from its current position to a potentially |
242 | /// very far position, and which is of interest to callers of |
243 | /// [recommendDeferredLoading]. |
244 | /// |
245 | /// For example, if a scrollable is currently at 5000 pixels, and we [jumpTo] |
246 | /// 0 to get back to the top of the list, we would have an implied velocity of |
247 | /// -5000 and an `activity.velocity` of 0. The jump may be going past a |
248 | /// number of resource intensive widgets which should avoid doing work if the |
249 | /// position jumps past them. |
250 | double _impliedVelocity = 0; |
251 | |
252 | @override |
253 | double get pixels => _pixels!; |
254 | double? _pixels; |
255 | |
256 | @override |
257 | bool get hasPixels => _pixels != null; |
258 | |
259 | @override |
260 | double get viewportDimension => _viewportDimension!; |
261 | double? _viewportDimension; |
262 | |
263 | @override |
264 | bool get hasViewportDimension => _viewportDimension != null; |
265 | |
266 | /// Whether [viewportDimension], [minScrollExtent], [maxScrollExtent], |
267 | /// [outOfRange], and [atEdge] are available. |
268 | /// |
269 | /// Set to true just before the first time [applyNewDimensions] is called. |
270 | bool get haveDimensions => _haveDimensions; |
271 | bool _haveDimensions = false; |
272 | |
273 | /// Take any current applicable state from the given [ScrollPosition]. |
274 | /// |
275 | /// This method is called by the constructor if it is given an `oldPosition`. |
276 | /// The `other` argument might not have the same [runtimeType] as this object. |
277 | /// |
278 | /// This method can be destructive to the other [ScrollPosition]. The other |
279 | /// object must be disposed immediately after this call (in the same call |
280 | /// stack, before microtask resolution, by whomever called this object's |
281 | /// constructor). |
282 | /// |
283 | /// If the old [ScrollPosition] object is a different [runtimeType] than this |
284 | /// one, the [ScrollActivity.resetActivity] method is invoked on the newly |
285 | /// adopted [ScrollActivity]. |
286 | /// |
287 | /// ## Overriding |
288 | /// |
289 | /// Overrides of this method must call `super.absorb` after setting any |
290 | /// metrics-related or activity-related state, since this method may restart |
291 | /// the activity and scroll activities tend to use those metrics when being |
292 | /// restarted. |
293 | /// |
294 | /// Overrides of this method might need to start an [IdleScrollActivity] if |
295 | /// they are unable to absorb the activity from the other [ScrollPosition]. |
296 | /// |
297 | /// Overrides of this method might also need to update the delegates of |
298 | /// absorbed scroll activities if they use themselves as a |
299 | /// [ScrollActivityDelegate]. |
300 | @protected |
301 | @mustCallSuper |
302 | void absorb(ScrollPosition other) { |
303 | assert(other.context == context); |
304 | assert(_pixels == null); |
305 | if (other.hasContentDimensions) { |
306 | _minScrollExtent = other.minScrollExtent; |
307 | _maxScrollExtent = other.maxScrollExtent; |
308 | } |
309 | if (other.hasPixels) { |
310 | _pixels = other.pixels; |
311 | } |
312 | if (other.hasViewportDimension) { |
313 | _viewportDimension = other.viewportDimension; |
314 | } |
315 | |
316 | assert(activity == null); |
317 | assert(other.activity != null); |
318 | _activity = other.activity; |
319 | other._activity = null; |
320 | if (other.runtimeType != runtimeType) { |
321 | activity!.resetActivity(); |
322 | } |
323 | context.setIgnorePointer(activity!.shouldIgnorePointer); |
324 | isScrollingNotifier.value = activity!.isScrolling; |
325 | } |
326 | |
327 | @override |
328 | double get devicePixelRatio => context.devicePixelRatio; |
329 | |
330 | /// Update the scroll position ([pixels]) to a given pixel value. |
331 | /// |
332 | /// This should only be called by the current [ScrollActivity], either during |
333 | /// the transient callback phase or in response to user input. |
334 | /// |
335 | /// Returns the overscroll, if any. If the return value is 0.0, that means |
336 | /// that [pixels] now returns the given `value`. If the return value is |
337 | /// positive, then [pixels] is less than the requested `value` by the given |
338 | /// amount (overscroll past the max extent), and if it is negative, it is |
339 | /// greater than the requested `value` by the given amount (underscroll past |
340 | /// the min extent). |
341 | /// |
342 | /// The amount of overscroll is computed by [applyBoundaryConditions]. |
343 | /// |
344 | /// The amount of the change that is applied is reported using [didUpdateScrollPositionBy]. |
345 | /// If there is any overscroll, it is reported using [didOverscrollBy]. |
346 | double setPixels(double newPixels) { |
347 | assert(hasPixels); |
348 | assert(SchedulerBinding.instance.schedulerPhase != SchedulerPhase.persistentCallbacks, "A scrollable's position should not change during the build, layout, and paint phases, otherwise the rendering will be confused." ); |
349 | if (newPixels != pixels) { |
350 | final double overscroll = applyBoundaryConditions(newPixels); |
351 | assert(() { |
352 | final double delta = newPixels - pixels; |
353 | if (overscroll.abs() > delta.abs()) { |
354 | throw FlutterError( |
355 | ' $runtimeType.applyBoundaryConditions returned invalid overscroll value.\n' |
356 | 'setPixels() was called to change the scroll offset from $pixels to $newPixels.\n' |
357 | 'That is a delta of $delta units.\n' |
358 | ' $runtimeType.applyBoundaryConditions reported an overscroll of $overscroll units.' , |
359 | ); |
360 | } |
361 | return true; |
362 | }()); |
363 | final double oldPixels = pixels; |
364 | _pixels = newPixels - overscroll; |
365 | if (_pixels != oldPixels) { |
366 | notifyListeners(); |
367 | didUpdateScrollPositionBy(pixels - oldPixels); |
368 | } |
369 | if (overscroll.abs() > precisionErrorTolerance) { |
370 | didOverscrollBy(overscroll); |
371 | return overscroll; |
372 | } |
373 | } |
374 | return 0.0; |
375 | } |
376 | |
377 | /// Change the value of [pixels] to the new value, without notifying any |
378 | /// customers. |
379 | /// |
380 | /// This is used to adjust the position while doing layout. In particular, |
381 | /// this is typically called as a response to [applyViewportDimension] or |
382 | /// [applyContentDimensions] (in both cases, if this method is called, those |
383 | /// methods should then return false to indicate that the position has been |
384 | /// adjusted). |
385 | /// |
386 | /// Calling this is rarely correct in other contexts. It will not immediately |
387 | /// cause the rendering to change, since it does not notify the widgets or |
388 | /// render objects that might be listening to this object: they will only |
389 | /// change when they next read the value, which could be arbitrarily later. It |
390 | /// is generally only appropriate in the very specific case of the value being |
391 | /// corrected during layout (since then the value is immediately read), in the |
392 | /// specific case of a [ScrollPosition] with a single viewport customer. |
393 | /// |
394 | /// To cause the position to jump or animate to a new value, consider [jumpTo] |
395 | /// or [animateTo], which will honor the normal conventions for changing the |
396 | /// scroll offset. |
397 | /// |
398 | /// To force the [pixels] to a particular value without honoring the normal |
399 | /// conventions for changing the scroll offset, consider [forcePixels]. (But |
400 | /// see the discussion there for why that might still be a bad idea.) |
401 | /// |
402 | /// See also: |
403 | /// |
404 | /// * [correctBy], which is a method of [ViewportOffset] used |
405 | /// by viewport render objects to correct the offset during layout |
406 | /// without notifying its listeners. |
407 | /// * [jumpTo], for making changes to position while not in the |
408 | /// middle of layout and applying the new position immediately. |
409 | /// * [animateTo], which is like [jumpTo] but animating to the |
410 | /// destination offset. |
411 | // ignore: use_setters_to_change_properties, (API is intended to discourage setting value) |
412 | void correctPixels(double value) { |
413 | _pixels = value; |
414 | } |
415 | |
416 | /// Apply a layout-time correction to the scroll offset. |
417 | /// |
418 | /// This method should change the [pixels] value by `correction`, but without |
419 | /// calling [notifyListeners]. It is called during layout by the |
420 | /// [RenderViewport], before [applyContentDimensions]. After this method is |
421 | /// called, the layout will be recomputed and that may result in this method |
422 | /// being called again, though this should be very rare. |
423 | /// |
424 | /// See also: |
425 | /// |
426 | /// * [jumpTo], for also changing the scroll position when not in layout. |
427 | /// [jumpTo] applies the change immediately and notifies its listeners. |
428 | /// * [correctPixels], which is used by the [ScrollPosition] itself to |
429 | /// set the offset initially during construction or after |
430 | /// [applyViewportDimension] or [applyContentDimensions] is called. |
431 | @override |
432 | void correctBy(double correction) { |
433 | assert( |
434 | hasPixels, |
435 | 'An initial pixels value must exist by calling correctPixels on the ScrollPosition' , |
436 | ); |
437 | _pixels = _pixels! + correction; |
438 | _didChangeViewportDimensionOrReceiveCorrection = true; |
439 | } |
440 | |
441 | /// Change the value of [pixels] to the new value, and notify any customers, |
442 | /// but without honoring normal conventions for changing the scroll offset. |
443 | /// |
444 | /// This is used to implement [jumpTo]. It can also be used adjust the |
445 | /// position when the dimensions of the viewport change. It should only be |
446 | /// used when manually implementing the logic for honoring the relevant |
447 | /// conventions of the class. For example, [ScrollPositionWithSingleContext] |
448 | /// introduces [ScrollActivity] objects and uses [forcePixels] in conjunction |
449 | /// with adjusting the activity, e.g. by calling |
450 | /// [ScrollPositionWithSingleContext.goIdle], so that the activity does |
451 | /// not immediately set the value back. (Consider, for instance, a case where |
452 | /// one is using a [DrivenScrollActivity]. That object will ignore any calls |
453 | /// to [forcePixels], which would result in the rendering stuttering: changing |
454 | /// in response to [forcePixels], and then changing back to the next value |
455 | /// derived from the animation.) |
456 | /// |
457 | /// To cause the position to jump or animate to a new value, consider [jumpTo] |
458 | /// or [animateTo]. |
459 | /// |
460 | /// This should not be called during layout (e.g. when setting the initial |
461 | /// scroll offset). Consider [correctPixels] if you find you need to adjust |
462 | /// the position during layout. |
463 | @protected |
464 | void forcePixels(double value) { |
465 | assert(hasPixels); |
466 | _impliedVelocity = value - pixels; |
467 | _pixels = value; |
468 | notifyListeners(); |
469 | SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) { |
470 | _impliedVelocity = 0; |
471 | }, debugLabel: 'ScrollPosition.resetVelocity' ); |
472 | } |
473 | |
474 | /// Called whenever scrolling ends, to store the current scroll offset in a |
475 | /// storage mechanism with a lifetime that matches the app's lifetime. |
476 | /// |
477 | /// The stored value will be used by [restoreScrollOffset] when the |
478 | /// [ScrollPosition] is recreated, in the case of the [Scrollable] being |
479 | /// disposed then recreated in the same session. This might happen, for |
480 | /// instance, if a [ListView] is on one of the pages inside a [TabBarView], |
481 | /// and that page is displayed, then hidden, then displayed again. |
482 | /// |
483 | /// The default implementation writes the [pixels] using the nearest |
484 | /// [PageStorage] found from the [context]'s [ScrollContext.storageContext] |
485 | /// property. |
486 | // TODO(goderbauer): Deprecate this when state restoration supports all features of PageStorage. |
487 | @protected |
488 | void saveScrollOffset() { |
489 | PageStorage.maybeOf(context.storageContext)?.writeState(context.storageContext, pixels); |
490 | } |
491 | |
492 | /// Called whenever the [ScrollPosition] is created, to restore the scroll |
493 | /// offset if possible. |
494 | /// |
495 | /// The value is stored by [saveScrollOffset] when the scroll position |
496 | /// changes, so that it can be restored in the case of the [Scrollable] being |
497 | /// disposed then recreated in the same session. This might happen, for |
498 | /// instance, if a [ListView] is on one of the pages inside a [TabBarView], |
499 | /// and that page is displayed, then hidden, then displayed again. |
500 | /// |
501 | /// The default implementation reads the value from the nearest [PageStorage] |
502 | /// found from the [context]'s [ScrollContext.storageContext] property, and |
503 | /// sets it using [correctPixels], if [pixels] is still null. |
504 | /// |
505 | /// This method is called from the constructor, so layout has not yet |
506 | /// occurred, and the viewport dimensions aren't yet known when it is called. |
507 | // TODO(goderbauer): Deprecate this when state restoration supports all features of PageStorage. |
508 | @protected |
509 | void restoreScrollOffset() { |
510 | if (!hasPixels) { |
511 | final double? value = PageStorage.maybeOf(context.storageContext)?.readState(context.storageContext) as double?; |
512 | if (value != null) { |
513 | correctPixels(value); |
514 | } |
515 | } |
516 | } |
517 | |
518 | /// Called by [context] to restore the scroll offset to the provided value. |
519 | /// |
520 | /// The provided value has previously been provided to the [context] by |
521 | /// calling [ScrollContext.saveOffset], e.g. from [saveOffset]. |
522 | /// |
523 | /// This method may be called right after the scroll position is created |
524 | /// before layout has occurred. In that case, `initialRestore` is set to true |
525 | /// and the viewport dimensions will not be known yet. If the [context] |
526 | /// doesn't have any information to restore the scroll offset this method is |
527 | /// not called. |
528 | /// |
529 | /// The method may be called multiple times in the lifecycle of a |
530 | /// [ScrollPosition] to restore it to different scroll offsets. |
531 | void restoreOffset(double offset, {bool initialRestore = false}) { |
532 | if (initialRestore) { |
533 | correctPixels(offset); |
534 | } else { |
535 | jumpTo(offset); |
536 | } |
537 | } |
538 | |
539 | /// Called whenever scrolling ends, to persist the current scroll offset for |
540 | /// state restoration purposes. |
541 | /// |
542 | /// The default implementation stores the current value of [pixels] on the |
543 | /// [context] by calling [ScrollContext.saveOffset]. At a later point in time |
544 | /// or after the application restarts, the [context] may restore the scroll |
545 | /// position to the persisted offset by calling [restoreOffset]. |
546 | @protected |
547 | void saveOffset() { |
548 | assert(hasPixels); |
549 | context.saveOffset(pixels); |
550 | } |
551 | |
552 | /// Returns the overscroll by applying the boundary conditions. |
553 | /// |
554 | /// If the given value is in bounds, returns 0.0. Otherwise, returns the |
555 | /// amount of value that cannot be applied to [pixels] as a result of the |
556 | /// boundary conditions. If the [physics] allow out-of-bounds scrolling, this |
557 | /// method always returns 0.0. |
558 | /// |
559 | /// The default implementation defers to the [physics] object's |
560 | /// [ScrollPhysics.applyBoundaryConditions]. |
561 | @protected |
562 | double applyBoundaryConditions(double value) { |
563 | final double result = physics.applyBoundaryConditions(this, value); |
564 | assert(() { |
565 | final double delta = value - pixels; |
566 | if (result.abs() > delta.abs()) { |
567 | throw FlutterError( |
568 | ' ${physics.runtimeType}.applyBoundaryConditions returned invalid overscroll value.\n' |
569 | 'The method was called to consider a change from $pixels to $value, which is a ' |
570 | 'delta of ${delta.toStringAsFixed(1)} units. However, it returned an overscroll of ' |
571 | ' ${result.toStringAsFixed(1)} units, which has a greater magnitude than the delta. ' |
572 | 'The applyBoundaryConditions method is only supposed to reduce the possible range ' |
573 | 'of movement, not increase it.\n' |
574 | 'The scroll extents are $minScrollExtent .. $maxScrollExtent, and the ' |
575 | 'viewport dimension is $viewportDimension.' , |
576 | ); |
577 | } |
578 | return true; |
579 | }()); |
580 | return result; |
581 | } |
582 | |
583 | bool _didChangeViewportDimensionOrReceiveCorrection = true; |
584 | |
585 | @override |
586 | bool applyViewportDimension(double viewportDimension) { |
587 | if (_viewportDimension != viewportDimension) { |
588 | _viewportDimension = viewportDimension; |
589 | _didChangeViewportDimensionOrReceiveCorrection = true; |
590 | // If this is called, you can rely on applyContentDimensions being called |
591 | // soon afterwards in the same layout phase. So we put all the logic that |
592 | // relies on both values being computed into applyContentDimensions. |
593 | } |
594 | return true; |
595 | } |
596 | |
597 | bool _pendingDimensions = false; |
598 | ScrollMetrics? _lastMetrics; |
599 | // True indicates that there is a ScrollMetrics update notification pending. |
600 | bool _haveScheduledUpdateNotification = false; |
601 | Axis? _lastAxis; |
602 | |
603 | bool _isMetricsChanged() { |
604 | assert(haveDimensions); |
605 | final ScrollMetrics currentMetrics = copyWith(); |
606 | |
607 | return _lastMetrics == null || |
608 | !(currentMetrics.extentBefore == _lastMetrics!.extentBefore && |
609 | currentMetrics.extentInside == _lastMetrics!.extentInside && |
610 | currentMetrics.extentAfter == _lastMetrics!.extentAfter && |
611 | currentMetrics.axisDirection == _lastMetrics!.axisDirection); |
612 | } |
613 | |
614 | @override |
615 | bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) { |
616 | assert(haveDimensions == (_lastMetrics != null)); |
617 | if (!nearEqual(_minScrollExtent, minScrollExtent, Tolerance.defaultTolerance.distance) || |
618 | !nearEqual(_maxScrollExtent, maxScrollExtent, Tolerance.defaultTolerance.distance) || |
619 | _didChangeViewportDimensionOrReceiveCorrection || |
620 | _lastAxis != axis) { |
621 | assert(minScrollExtent <= maxScrollExtent); |
622 | _minScrollExtent = minScrollExtent; |
623 | _maxScrollExtent = maxScrollExtent; |
624 | _lastAxis = axis; |
625 | final ScrollMetrics? currentMetrics = haveDimensions ? copyWith() : null; |
626 | _didChangeViewportDimensionOrReceiveCorrection = false; |
627 | _pendingDimensions = true; |
628 | if (haveDimensions && !correctForNewDimensions(_lastMetrics!, currentMetrics!)) { |
629 | return false; |
630 | } |
631 | _haveDimensions = true; |
632 | } |
633 | assert(haveDimensions); |
634 | if (_pendingDimensions) { |
635 | applyNewDimensions(); |
636 | _pendingDimensions = false; |
637 | } |
638 | assert(!_didChangeViewportDimensionOrReceiveCorrection, 'Use correctForNewDimensions() (and return true) to change the scroll offset during applyContentDimensions().' ); |
639 | |
640 | if (_isMetricsChanged()) { |
641 | // It is too late to send useful notifications, because the potential |
642 | // listeners have, by definition, already been built this frame. To make |
643 | // sure the notification is sent at all, we delay it until after the frame |
644 | // is complete. |
645 | if (!_haveScheduledUpdateNotification) { |
646 | scheduleMicrotask(didUpdateScrollMetrics); |
647 | _haveScheduledUpdateNotification = true; |
648 | } |
649 | _lastMetrics = copyWith(); |
650 | } |
651 | return true; |
652 | } |
653 | |
654 | /// Verifies that the new content and viewport dimensions are acceptable. |
655 | /// |
656 | /// Called by [applyContentDimensions] to determine its return value. |
657 | /// |
658 | /// Should return true if the current scroll offset is correct given |
659 | /// the new content and viewport dimensions. |
660 | /// |
661 | /// Otherwise, should call [correctPixels] to correct the scroll |
662 | /// offset given the new dimensions, and then return false. |
663 | /// |
664 | /// This is only called when [haveDimensions] is true. |
665 | /// |
666 | /// The default implementation defers to [ScrollPhysics.adjustPositionForNewDimensions]. |
667 | @protected |
668 | bool correctForNewDimensions(ScrollMetrics oldPosition, ScrollMetrics newPosition) { |
669 | final double newPixels = physics.adjustPositionForNewDimensions( |
670 | oldPosition: oldPosition, |
671 | newPosition: newPosition, |
672 | isScrolling: activity!.isScrolling, |
673 | velocity: activity!.velocity, |
674 | ); |
675 | if (newPixels != pixels) { |
676 | correctPixels(newPixels); |
677 | return false; |
678 | } |
679 | return true; |
680 | } |
681 | |
682 | /// Notifies the activity that the dimensions of the underlying viewport or |
683 | /// contents have changed. |
684 | /// |
685 | /// Called after [applyViewportDimension] or [applyContentDimensions] have |
686 | /// changed the [minScrollExtent], the [maxScrollExtent], or the |
687 | /// [viewportDimension]. When this method is called, it should be called |
688 | /// _after_ any corrections are applied to [pixels] using [correctPixels], not |
689 | /// before. |
690 | /// |
691 | /// The default implementation informs the [activity] of the new dimensions by |
692 | /// calling its [ScrollActivity.applyNewDimensions] method. |
693 | /// |
694 | /// See also: |
695 | /// |
696 | /// * [applyViewportDimension], which is called when new |
697 | /// viewport dimensions are established. |
698 | /// * [applyContentDimensions], which is called after new |
699 | /// viewport dimensions are established, and also if new content dimensions |
700 | /// are established, and which calls [ScrollPosition.applyNewDimensions]. |
701 | @protected |
702 | @mustCallSuper |
703 | void applyNewDimensions() { |
704 | assert(hasPixels); |
705 | assert(_pendingDimensions); |
706 | activity!.applyNewDimensions(); |
707 | _updateSemanticActions(); // will potentially request a semantics update. |
708 | } |
709 | |
710 | Set<SemanticsAction>? _semanticActions; |
711 | |
712 | /// Called whenever the scroll position or the dimensions of the scroll view |
713 | /// change to schedule an update of the available semantics actions. The |
714 | /// actual update will be performed in the next frame. If non is pending |
715 | /// a frame will be scheduled. |
716 | /// |
717 | /// For example: If the scroll view has been scrolled all the way to the top, |
718 | /// the action to scroll further up needs to be removed as the scroll view |
719 | /// cannot be scrolled in that direction anymore. |
720 | /// |
721 | /// This method is potentially called twice per frame (if scroll position and |
722 | /// scroll view dimensions both change) and therefore shouldn't do anything |
723 | /// expensive. |
724 | void _updateSemanticActions() { |
725 | final SemanticsAction forward; |
726 | final SemanticsAction backward; |
727 | switch (axisDirection) { |
728 | case AxisDirection.up: |
729 | forward = SemanticsAction.scrollDown; |
730 | backward = SemanticsAction.scrollUp; |
731 | case AxisDirection.right: |
732 | forward = SemanticsAction.scrollLeft; |
733 | backward = SemanticsAction.scrollRight; |
734 | case AxisDirection.down: |
735 | forward = SemanticsAction.scrollUp; |
736 | backward = SemanticsAction.scrollDown; |
737 | case AxisDirection.left: |
738 | forward = SemanticsAction.scrollRight; |
739 | backward = SemanticsAction.scrollLeft; |
740 | } |
741 | |
742 | final Set<SemanticsAction> actions = <SemanticsAction>{}; |
743 | if (pixels > minScrollExtent) { |
744 | actions.add(backward); |
745 | } |
746 | if (pixels < maxScrollExtent) { |
747 | actions.add(forward); |
748 | } |
749 | |
750 | if (setEquals<SemanticsAction>(actions, _semanticActions)) { |
751 | return; |
752 | } |
753 | |
754 | _semanticActions = actions; |
755 | context.setSemanticsActions(_semanticActions!); |
756 | } |
757 | |
758 | ScrollPositionAlignmentPolicy _maybeFlipAlignment(ScrollPositionAlignmentPolicy alignmentPolicy) { |
759 | return switch (alignmentPolicy) { |
760 | // Don't flip when explicit. |
761 | ScrollPositionAlignmentPolicy.explicit => alignmentPolicy, |
762 | ScrollPositionAlignmentPolicy.keepVisibleAtEnd => ScrollPositionAlignmentPolicy.keepVisibleAtStart, |
763 | ScrollPositionAlignmentPolicy.keepVisibleAtStart => ScrollPositionAlignmentPolicy.keepVisibleAtEnd, |
764 | }; |
765 | } |
766 | |
767 | ScrollPositionAlignmentPolicy _applyAxisDirectionToAlignmentPolicy(ScrollPositionAlignmentPolicy alignmentPolicy) { |
768 | return switch (axisDirection) { |
769 | // Start and end alignments must account for axis direction. |
770 | // When focus is requested for example, it knows the directionality of the |
771 | // keyboard keys initiating traversal, but not the direction of the |
772 | // Scrollable. |
773 | AxisDirection.up || AxisDirection.left => _maybeFlipAlignment(alignmentPolicy), |
774 | AxisDirection.down || AxisDirection.right => alignmentPolicy, |
775 | }; |
776 | } |
777 | |
778 | /// Animates the position such that the given object is as visible as possible |
779 | /// by just scrolling this position. |
780 | /// |
781 | /// The optional `targetRenderObject` parameter is used to determine which area |
782 | /// of that object should be as visible as possible. If `targetRenderObject` |
783 | /// is null, the entire [RenderObject] (as defined by its |
784 | /// [RenderObject.paintBounds]) will be as visible as possible. If |
785 | /// `targetRenderObject` is provided, it must be a descendant of the object. |
786 | /// |
787 | /// See also: |
788 | /// |
789 | /// * [ScrollPositionAlignmentPolicy] for the way in which `alignment` is |
790 | /// applied, and the way the given `object` is aligned. |
791 | Future<void> ensureVisible( |
792 | RenderObject object, { |
793 | double alignment = 0.0, |
794 | Duration duration = Duration.zero, |
795 | Curve curve = Curves.ease, |
796 | ScrollPositionAlignmentPolicy alignmentPolicy = ScrollPositionAlignmentPolicy.explicit, |
797 | RenderObject? targetRenderObject, |
798 | }) async { |
799 | assert(object.attached); |
800 | final RenderAbstractViewport? viewport = RenderAbstractViewport.maybeOf(object); |
801 | // If no viewport is found, return. |
802 | if (viewport == null) { |
803 | return; |
804 | } |
805 | |
806 | Rect? targetRect; |
807 | if (targetRenderObject != null && targetRenderObject != object) { |
808 | targetRect = MatrixUtils.transformRect( |
809 | targetRenderObject.getTransformTo(object), |
810 | object.paintBounds.intersect(targetRenderObject.paintBounds), |
811 | ); |
812 | } |
813 | |
814 | double target; |
815 | switch (_applyAxisDirectionToAlignmentPolicy(alignmentPolicy)) { |
816 | case ScrollPositionAlignmentPolicy.explicit: |
817 | target = viewport.getOffsetToReveal( |
818 | object, |
819 | alignment, |
820 | rect: targetRect, |
821 | axis: axis, |
822 | ).offset; |
823 | target = clampDouble(target, minScrollExtent, maxScrollExtent); |
824 | case ScrollPositionAlignmentPolicy.keepVisibleAtEnd: |
825 | target = viewport.getOffsetToReveal( |
826 | object, |
827 | 1.0, // Aligns to end |
828 | rect: targetRect, |
829 | axis: axis, |
830 | ).offset; |
831 | target = clampDouble(target, minScrollExtent, maxScrollExtent); |
832 | if (target < pixels) { |
833 | target = pixels; |
834 | } |
835 | case ScrollPositionAlignmentPolicy.keepVisibleAtStart: |
836 | target = viewport.getOffsetToReveal( |
837 | object, |
838 | 0.0, // Aligns to start |
839 | rect: targetRect, |
840 | axis: axis, |
841 | ).offset; |
842 | target = clampDouble(target, minScrollExtent, maxScrollExtent); |
843 | if (target > pixels) { |
844 | target = pixels; |
845 | } |
846 | } |
847 | |
848 | if (target == pixels) { |
849 | return; |
850 | } |
851 | |
852 | if (duration == Duration.zero) { |
853 | jumpTo(target); |
854 | return; |
855 | } |
856 | |
857 | return animateTo(target, duration: duration, curve: curve); |
858 | } |
859 | |
860 | /// This notifier's value is true if a scroll is underway and false if the scroll |
861 | /// position is idle. |
862 | /// |
863 | /// Listeners added by stateful widgets should be removed in the widget's |
864 | /// [State.dispose] method. |
865 | final ValueNotifier<bool> isScrollingNotifier = ValueNotifier<bool>(false); |
866 | |
867 | /// Animates the position from its current value to the given value. |
868 | /// |
869 | /// Any active animation is canceled. If the user is currently scrolling, that |
870 | /// action is canceled. |
871 | /// |
872 | /// The returned [Future] will complete when the animation ends, whether it |
873 | /// completed successfully or whether it was interrupted prematurely. |
874 | /// |
875 | /// An animation will be interrupted whenever the user attempts to scroll |
876 | /// manually, or whenever another activity is started, or whenever the |
877 | /// animation reaches the edge of the viewport and attempts to overscroll. (If |
878 | /// the [ScrollPosition] does not overscroll but instead allows scrolling |
879 | /// beyond the extents, then going beyond the extents will not interrupt the |
880 | /// animation.) |
881 | /// |
882 | /// The animation is indifferent to changes to the viewport or content |
883 | /// dimensions. |
884 | /// |
885 | /// Once the animation has completed, the scroll position will attempt to |
886 | /// begin a ballistic activity in case its value is not stable (for example, |
887 | /// if it is scrolled beyond the extents and in that situation the scroll |
888 | /// position would normally bounce back). |
889 | /// |
890 | /// The duration must not be zero. To jump to a particular value without an |
891 | /// animation, use [jumpTo]. |
892 | /// |
893 | /// The animation is typically handled by an [DrivenScrollActivity]. |
894 | @override |
895 | Future<void> animateTo( |
896 | double to, { |
897 | required Duration duration, |
898 | required Curve curve, |
899 | }); |
900 | |
901 | /// Jumps the scroll position from its current value to the given value, |
902 | /// without animation, and without checking if the new value is in range. |
903 | /// |
904 | /// Any active animation is canceled. If the user is currently scrolling, that |
905 | /// action is canceled. |
906 | /// |
907 | /// If this method changes the scroll position, a sequence of start/update/end |
908 | /// scroll notifications will be dispatched. No overscroll notifications can |
909 | /// be generated by this method. |
910 | @override |
911 | void jumpTo(double value); |
912 | |
913 | /// Changes the scrolling position based on a pointer signal from current |
914 | /// value to delta without animation and without checking if new value is in |
915 | /// range, taking min/max scroll extent into account. |
916 | /// |
917 | /// Any active animation is canceled. If the user is currently scrolling, that |
918 | /// action is canceled. |
919 | /// |
920 | /// This method dispatches the start/update/end sequence of scrolling |
921 | /// notifications. |
922 | /// |
923 | /// This method is very similar to [jumpTo], but [pointerScroll] will |
924 | /// update the [ScrollDirection]. |
925 | void pointerScroll(double delta); |
926 | |
927 | /// Calls [jumpTo] if duration is null or [Duration.zero], otherwise |
928 | /// [animateTo] is called. |
929 | /// |
930 | /// If [clamp] is true (the default) then [to] is adjusted to prevent over or |
931 | /// underscroll. |
932 | /// |
933 | /// If [animateTo] is called then [curve] defaults to [Curves.ease]. |
934 | @override |
935 | Future<void> moveTo( |
936 | double to, { |
937 | Duration? duration, |
938 | Curve? curve, |
939 | bool? clamp = true, |
940 | }) { |
941 | assert(clamp != null); |
942 | |
943 | if (clamp!) { |
944 | to = clampDouble(to, minScrollExtent, maxScrollExtent); |
945 | } |
946 | |
947 | return super.moveTo(to, duration: duration, curve: curve); |
948 | } |
949 | |
950 | @override |
951 | bool get allowImplicitScrolling => physics.allowImplicitScrolling; |
952 | |
953 | /// Deprecated. Use [jumpTo] or a custom [ScrollPosition] instead. |
954 | @Deprecated('This will lead to bugs.' ) // flutter_ignore: deprecation_syntax, https://github.com/flutter/flutter/issues/44609 |
955 | void jumpToWithoutSettling(double value); |
956 | |
957 | /// Stop the current activity and start a [HoldScrollActivity]. |
958 | ScrollHoldController hold(VoidCallback holdCancelCallback); |
959 | |
960 | /// Start a drag activity corresponding to the given [DragStartDetails]. |
961 | /// |
962 | /// The `onDragCanceled` argument will be invoked if the drag is ended |
963 | /// prematurely (e.g. from another activity taking over). See |
964 | /// [ScrollDragController.onDragCanceled] for details. |
965 | Drag drag(DragStartDetails details, VoidCallback dragCancelCallback); |
966 | |
967 | /// The currently operative [ScrollActivity]. |
968 | /// |
969 | /// If the scroll position is not performing any more specific activity, the |
970 | /// activity will be an [IdleScrollActivity]. To determine whether the scroll |
971 | /// position is idle, check the [isScrollingNotifier]. |
972 | /// |
973 | /// Call [beginActivity] to change the current activity. |
974 | @protected |
975 | @visibleForTesting |
976 | ScrollActivity? get activity => _activity; |
977 | ScrollActivity? _activity; |
978 | |
979 | /// Change the current [activity], disposing of the old one and |
980 | /// sending scroll notifications as necessary. |
981 | /// |
982 | /// If the argument is null, this method has no effect. This is convenient for |
983 | /// cases where the new activity is obtained from another method, and that |
984 | /// method might return null, since it means the caller does not have to |
985 | /// explicitly null-check the argument. |
986 | void beginActivity(ScrollActivity? newActivity) { |
987 | if (newActivity == null) { |
988 | return; |
989 | } |
990 | bool wasScrolling, oldIgnorePointer; |
991 | if (_activity != null) { |
992 | oldIgnorePointer = _activity!.shouldIgnorePointer; |
993 | wasScrolling = _activity!.isScrolling; |
994 | if (wasScrolling && !newActivity.isScrolling) { |
995 | // Notifies and then saves the scroll offset. |
996 | didEndScroll(); |
997 | } |
998 | _activity!.dispose(); |
999 | } else { |
1000 | oldIgnorePointer = false; |
1001 | wasScrolling = false; |
1002 | } |
1003 | _activity = newActivity; |
1004 | if (oldIgnorePointer != activity!.shouldIgnorePointer) { |
1005 | context.setIgnorePointer(activity!.shouldIgnorePointer); |
1006 | } |
1007 | isScrollingNotifier.value = activity!.isScrolling; |
1008 | if (!wasScrolling && _activity!.isScrolling) { |
1009 | didStartScroll(); |
1010 | } |
1011 | } |
1012 | |
1013 | |
1014 | // NOTIFICATION DISPATCH |
1015 | |
1016 | /// Called by [beginActivity] to report when an activity has started. |
1017 | void didStartScroll() { |
1018 | activity!.dispatchScrollStartNotification(copyWith(), context.notificationContext); |
1019 | } |
1020 | |
1021 | /// Called by [setPixels] to report a change to the [pixels] position. |
1022 | void didUpdateScrollPositionBy(double delta) { |
1023 | activity!.dispatchScrollUpdateNotification(copyWith(), context.notificationContext!, delta); |
1024 | } |
1025 | |
1026 | /// Called by [beginActivity] to report when an activity has ended. |
1027 | /// |
1028 | /// This also saves the scroll offset using [saveScrollOffset]. |
1029 | void didEndScroll() { |
1030 | activity!.dispatchScrollEndNotification(copyWith(), context.notificationContext!); |
1031 | saveOffset(); |
1032 | if (keepScrollOffset) { |
1033 | saveScrollOffset(); |
1034 | } |
1035 | } |
1036 | |
1037 | /// Called by [setPixels] to report overscroll when an attempt is made to |
1038 | /// change the [pixels] position. Overscroll is the amount of change that was |
1039 | /// not applied to the [pixels] value. |
1040 | void didOverscrollBy(double value) { |
1041 | assert(activity!.isScrolling); |
1042 | activity!.dispatchOverscrollNotification(copyWith(), context.notificationContext!, value); |
1043 | } |
1044 | |
1045 | /// Dispatches a notification that the [userScrollDirection] has changed. |
1046 | /// |
1047 | /// Subclasses should call this function when they change [userScrollDirection]. |
1048 | void didUpdateScrollDirection(ScrollDirection direction) { |
1049 | UserScrollNotification(metrics: copyWith(), context: context.notificationContext!, direction: direction).dispatch(context.notificationContext); |
1050 | } |
1051 | |
1052 | /// Dispatches a notification that the [ScrollMetrics] have changed. |
1053 | void didUpdateScrollMetrics() { |
1054 | assert(SchedulerBinding.instance.schedulerPhase != SchedulerPhase.persistentCallbacks); |
1055 | assert(_haveScheduledUpdateNotification); |
1056 | _haveScheduledUpdateNotification = false; |
1057 | if (context.notificationContext != null) { |
1058 | ScrollMetricsNotification(metrics: copyWith(), context: context.notificationContext!).dispatch(context.notificationContext); |
1059 | } |
1060 | } |
1061 | |
1062 | /// Provides a heuristic to determine if expensive frame-bound tasks should be |
1063 | /// deferred. |
1064 | /// |
1065 | /// The actual work of this is delegated to the [physics] via |
1066 | /// [ScrollPhysics.recommendDeferredLoading] called with the current |
1067 | /// [activity]'s [ScrollActivity.velocity]. |
1068 | /// |
1069 | /// Returning true from this method indicates that the [ScrollPhysics] |
1070 | /// evaluate the current scroll velocity to be great enough that expensive |
1071 | /// operations impacting the UI should be deferred. |
1072 | bool recommendDeferredLoading(BuildContext context) { |
1073 | assert(activity != null); |
1074 | return physics.recommendDeferredLoading( |
1075 | activity!.velocity + _impliedVelocity, |
1076 | copyWith(), |
1077 | context, |
1078 | ); |
1079 | } |
1080 | |
1081 | @override |
1082 | void dispose() { |
1083 | activity?.dispose(); // it will be null if it got absorbed by another ScrollPosition |
1084 | _activity = null; |
1085 | isScrollingNotifier.dispose(); |
1086 | super.dispose(); |
1087 | } |
1088 | |
1089 | @override |
1090 | void notifyListeners() { |
1091 | _updateSemanticActions(); // will potentially request a semantics update. |
1092 | super.notifyListeners(); |
1093 | } |
1094 | |
1095 | @override |
1096 | void debugFillDescription(List<String> description) { |
1097 | if (debugLabel != null) { |
1098 | description.add(debugLabel!); |
1099 | } |
1100 | super.debugFillDescription(description); |
1101 | description.add('range: ${_minScrollExtent?.toStringAsFixed(1)}.. ${_maxScrollExtent?.toStringAsFixed(1)}' ); |
1102 | description.add('viewport: ${_viewportDimension?.toStringAsFixed(1)}' ); |
1103 | } |
1104 | } |
1105 | |
1106 | /// A notification that a scrollable widget's [ScrollMetrics] have changed. |
1107 | /// |
1108 | /// For example, when the content of a scrollable is altered, making it larger |
1109 | /// or smaller, this notification will be dispatched. Similarly, if the size |
1110 | /// of the window or parent changes, the scrollable can notify of these |
1111 | /// changes in dimensions. |
1112 | /// |
1113 | /// The above behaviors usually do not trigger [ScrollNotification] events, |
1114 | /// so this is useful for listening to [ScrollMetrics] changes that are not |
1115 | /// caused by the user scrolling. |
1116 | /// |
1117 | /// {@tool dartpad} |
1118 | /// This sample shows how a [ScrollMetricsNotification] is dispatched when |
1119 | /// the `windowSize` is changed. Press the floating action button to increase |
1120 | /// the scrollable window's size. |
1121 | /// |
1122 | /// ** See code in examples/api/lib/widgets/scroll_position/scroll_metrics_notification.0.dart ** |
1123 | /// {@end-tool} |
1124 | class ScrollMetricsNotification extends Notification with ViewportNotificationMixin { |
1125 | /// Creates a notification that the scrollable widget's [ScrollMetrics] have |
1126 | /// changed. |
1127 | ScrollMetricsNotification({ |
1128 | required this.metrics, |
1129 | required this.context, |
1130 | }); |
1131 | |
1132 | /// Description of a scrollable widget's [ScrollMetrics]. |
1133 | final ScrollMetrics metrics; |
1134 | |
1135 | /// The build context of the widget that fired this notification. |
1136 | /// |
1137 | /// This can be used to find the scrollable widget's render objects to |
1138 | /// determine the size of the viewport, for instance. |
1139 | final BuildContext context; |
1140 | |
1141 | /// Convert this notification to a [ScrollNotification]. |
1142 | /// |
1143 | /// This allows it to be used with [ScrollNotificationPredicate]s. |
1144 | ScrollUpdateNotification asScrollUpdate() { |
1145 | return ScrollUpdateNotification( |
1146 | metrics: metrics, |
1147 | context: context, |
1148 | depth: depth, |
1149 | ); |
1150 | } |
1151 | |
1152 | @override |
1153 | void debugFillDescription(List<String> description) { |
1154 | super.debugFillDescription(description); |
1155 | description.add(' $metrics' ); |
1156 | } |
1157 | } |
1158 | |