1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:async';
6
7import 'package:flutter/foundation.dart';
8import 'package:flutter/gestures.dart';
9import 'package:flutter/physics.dart';
10import 'package:flutter/rendering.dart';
11import 'package:flutter/scheduler.dart';
12
13import 'basic.dart';
14import 'framework.dart';
15import 'notification_listener.dart';
16import 'page_storage.dart';
17import 'scroll_activity.dart';
18import 'scroll_context.dart';
19import 'scroll_metrics.dart';
20import 'scroll_notification.dart';
21import 'scroll_physics.dart';
22
23export 'scroll_activity.dart' show ScrollHoldController;
24
25/// The policy to use when applying the `alignment` parameter of
26/// [ScrollPosition.ensureVisible].
27enum 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].
178abstract 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}
1124class 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