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';
14library;
15
16import 'dart:async';
17
18import 'package:flutter/foundation.dart';
19import 'package:flutter/gestures.dart';
20import 'package:flutter/physics.dart';
21import 'package:flutter/rendering.dart';
22import 'package:flutter/scheduler.dart';
23
24import 'basic.dart';
25import 'framework.dart';
26import 'notification_listener.dart';
27import 'page_storage.dart';
28import 'scroll_activity.dart';
29import 'scroll_context.dart';
30import 'scroll_metrics.dart';
31import 'scroll_notification.dart';
32import 'scroll_physics.dart';
33
34export 'scroll_activity.dart' show ScrollHoldController;
35
36/// The policy to use when applying the `alignment` parameter of
37/// [ScrollPosition.ensureVisible].
38enum 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].
189abstract 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}
1156class 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

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com