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