scroll_position.dart 37.4 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
7
import 'package:flutter/physics.dart';
8 9 10 11 12
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';

import 'basic.dart';
import 'framework.dart';
13
import 'page_storage.dart';
14 15
import 'scroll_activity.dart';
import 'scroll_context.dart';
16
import 'scroll_metrics.dart';
17
import 'scroll_notification.dart';
18
import 'scroll_physics.dart';
19

20 21
export 'scroll_activity.dart' show ScrollHoldController;

22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
/// The policy to use when applying the `alignment` parameter of
/// [ScrollPosition.ensureVisible].
enum ScrollPositionAlignmentPolicy {
  /// Use the `alignment` property of [ScrollPosition.ensureVisible] to decide
  /// where to align the visible object.
  explicit,

  /// Find the bottom edge of the scroll container, and scroll the container, if
  /// necessary, to show the bottom of the object.
  ///
  /// For example, find the bottom edge of the scroll container. If the bottom
  /// edge of the item is below the bottom edge of the scroll container, scroll
  /// the item so that the bottom of the item is just visible. If the entire
  /// item is already visible, then do nothing.
  keepVisibleAtEnd,

  /// Find the top edge of the scroll container, and scroll the container if
  /// necessary to show the top of the object.
  ///
  /// For example, find the top edge of the scroll container. If the top edge of
  /// the item is above the top edge of the scroll container, scroll the item so
  /// that the top of the item is just visible. If the entire item is already
  /// visible, then do nothing.
  keepVisibleAtStart,
}

48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
/// Determines which portion of the content is visible in a scroll view.
///
/// The [pixels] value determines the scroll offset that the scroll view uses to
/// select which part of its content to display. As the user scrolls the
/// viewport, this value changes, which changes the content that is displayed.
///
/// The [ScrollPosition] applies [physics] to scrolling, and stores the
/// [minScrollExtent] and [maxScrollExtent].
///
/// Scrolling is controlled by the current [activity], which is set by
/// [beginActivity]. [ScrollPosition] itself does not start any activities.
/// Instead, concrete subclasses, such as [ScrollPositionWithSingleContext],
/// typically start activities in response to user input or instructions from a
/// [ScrollController].
///
63
/// This object is a [Listenable] that notifies its listeners when [pixels]
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
/// changes.
///
/// ## Subclassing ScrollPosition
///
/// Over time, a [Scrollable] might have many different [ScrollPosition]
/// objects. For example, if [Scrollable.physics] changes type, [Scrollable]
/// creates a new [ScrollPosition] with the new physics. To transfer state from
/// the old instance to the new instance, subclasses implement [absorb]. See
/// [absorb] for more details.
///
/// Subclasses also need to call [didUpdateScrollDirection] whenever
/// [userScrollDirection] changes values.
///
/// See also:
///
///  * [Scrollable], which uses a [ScrollPosition] to determine which portion of
///    its content to display.
///  * [ScrollController], which can be used with [ListView], [GridView] and
///    other scrollable widgets to control a [ScrollPosition].
///  * [ScrollPositionWithSingleContext], which is the most commonly used
///    concrete subclass of [ScrollPosition].
85
///  * [ScrollNotification] and [NotificationListener], which can be used to watch
86
///    the scroll position without using a [ScrollController].
87
abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
88 89
  /// Creates an object that determines which portion of the content is visible
  /// in a scroll view.
90 91
  ///
  /// The [physics], [context], and [keepScrollOffset] parameters must not be null.
92
  ScrollPosition({
93 94
    required this.physics,
    required this.context,
95
    this.keepScrollOffset = true,
96
    ScrollPosition? oldPosition,
97
    this.debugLabel,
98 99
  }) : assert(physics != null),
       assert(context != null),
100 101
       assert(context.vsync != null),
       assert(keepScrollOffset != null) {
102 103
    if (oldPosition != null)
      absorb(oldPosition);
104 105
    if (keepScrollOffset)
      restoreScrollOffset();
106
  }
107

108 109 110 111
  /// How the scroll position should respond to user input.
  ///
  /// For example, determines how the widget continues to animate after the
  /// user stops dragging the scroll view.
112
  final ScrollPhysics physics;
113 114 115 116

  /// Where the scrolling is taking place.
  ///
  /// Typically implemented by [ScrollableState].
117
  final ScrollContext context;
118

119
  /// Save the current scroll offset with [PageStorage] and restore it if
120 121 122 123 124 125
  /// this scroll position's scrollable is recreated.
  ///
  /// See also:
  ///
  ///  * [ScrollController.keepScrollOffset] and [PageController.keepPage], which
  ///    create scroll positions and initialize this property.
126
  // TODO(goderbauer): Deprecate this when state restoration supports all features of PageStorage.
127 128
  final bool keepScrollOffset;

129 130 131 132
  /// A label that is used in the [toString] output.
  ///
  /// Intended to aid with identifying animation controller instances in debug
  /// output.
133 134 135 136 137
  final String? debugLabel;

  @override
  double get minScrollExtent => _minScrollExtent!;
  double? _minScrollExtent;
138

139
  @override
140 141
  double get maxScrollExtent => _maxScrollExtent!;
  double? _maxScrollExtent;
142 143

  @override
144
  bool get hasContentDimensions => _minScrollExtent != null && _maxScrollExtent != null;
145

146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163
  /// The additional velocity added for a [forcePixels] change in a single
  /// frame.
  ///
  /// This value is used by [recommendDeferredLoading] in addition to the
  /// [activity]'s [ScrollActivity.velocity] to ask the [physics] whether or
  /// not to defer loading. It accounts for the fact that a [forcePixels] call
  /// may involve a [ScrollActivity] with 0 velocity, but the scrollable is
  /// still instantaneously moving from its current position to a potentially
  /// very far position, and which is of interest to callers of
  /// [recommendDeferredLoading].
  ///
  /// For example, if a scrollable is currently at 5000 pixels, and we [jumpTo]
  /// 0 to get back to the top of the list, we would have an implied velocity of
  /// -5000 and an `activity.velocity` of 0. The jump may be going past a
  /// number of resource intensive widgets which should avoid doing work if the
  /// position jumps past them.
  double _impliedVelocity = 0;

164
  @override
165 166 167 168 169
  double get pixels => _pixels!;
  double? _pixels;

  @override
  bool get hasPixels => _pixels != null;
170

171
  @override
172 173 174 175 176
  double get viewportDimension => _viewportDimension!;
  double? _viewportDimension;

  @override
  bool get hasViewportDimension => _viewportDimension != null;
177

178
  /// Whether [viewportDimension], [minScrollExtent], [maxScrollExtent],
179
  /// [outOfRange], and [atEdge] are available.
180
  ///
181
  /// Set to true just before the first time [applyNewDimensions] is called.
182 183
  bool get haveDimensions => _haveDimensions;
  bool _haveDimensions = false;
184

185
  /// Take any current applicable state from the given [ScrollPosition].
186
  ///
187
  /// This method is called by the constructor if it is given an `oldPosition`.
188
  /// The `other` argument might not have the same [runtimeType] as this object.
189
  ///
190 191 192 193
  /// This method can be destructive to the other [ScrollPosition]. The other
  /// object must be disposed immediately after this call (in the same call
  /// stack, before microtask resolution, by whomever called this object's
  /// constructor).
194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211
  ///
  /// If the old [ScrollPosition] object is a different [runtimeType] than this
  /// one, the [ScrollActivity.resetActivity] method is invoked on the newly
  /// adopted [ScrollActivity].
  ///
  /// ## Overriding
  ///
  /// Overrides of this method must call `super.absorb` after setting any
  /// metrics-related or activity-related state, since this method may restart
  /// the activity and scroll activities tend to use those metrics when being
  /// restarted.
  ///
  /// Overrides of this method might need to start an [IdleScrollActivity] if
  /// they are unable to absorb the activity from the other [ScrollPosition].
  ///
  /// Overrides of this method might also need to update the delegates of
  /// absorbed scroll activities if they use themselves as a
  /// [ScrollActivityDelegate].
212 213 214 215
  @protected
  @mustCallSuper
  void absorb(ScrollPosition other) {
    assert(other != null);
216
    assert(other.context == context);
217
    assert(_pixels == null);
218 219 220 221 222 223 224 225 226 227
    if (other.hasContentDimensions) {
      _minScrollExtent = other.minScrollExtent;
      _maxScrollExtent = other.maxScrollExtent;
    }
    if (other.hasPixels) {
      _pixels = other.pixels;
    }
    if (other.hasViewportDimension) {
      _viewportDimension = other.viewportDimension;
    }
228 229 230 231 232 233

    assert(activity == null);
    assert(other.activity != null);
    _activity = other.activity;
    other._activity = null;
    if (other.runtimeType != runtimeType)
234 235 236
      activity!.resetActivity();
    context.setIgnorePointer(activity!.shouldIgnorePointer);
    isScrollingNotifier.value = activity!.isScrolling;
237 238 239 240 241 242 243 244 245 246 247 248 249 250
  }

  /// Update the scroll position ([pixels]) to a given pixel value.
  ///
  /// This should only be called by the current [ScrollActivity], either during
  /// the transient callback phase or in response to user input.
  ///
  /// Returns the overscroll, if any. If the return value is 0.0, that means
  /// that [pixels] now returns the given `value`. If the return value is
  /// positive, then [pixels] is less than the requested `value` by the given
  /// amount (overscroll past the max extent), and if it is negative, it is
  /// greater than the requested `value` by the given amount (underscroll past
  /// the min extent).
  ///
251 252 253 254 255
  /// The amount of overscroll is computed by [applyBoundaryConditions].
  ///
  /// The amount of the change that is applied is reported using [didUpdateScrollPositionBy].
  /// If there is any overscroll, it is reported using [didOverscrollBy].
  double setPixels(double newPixels) {
256 257
    assert(hasPixels);
    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.');
258
    if (newPixels != pixels) {
259
      final double overscroll = applyBoundaryConditions(newPixels);
260
      assert(() {
261
        final double delta = newPixels - pixels;
262
        if (overscroll.abs() > delta.abs()) {
263 264 265 266
          throw FlutterError(
            '$runtimeType.applyBoundaryConditions returned invalid overscroll value.\n'
            'setPixels() was called to change the scroll offset from $pixels to $newPixels.\n'
            'That is a delta of $delta units.\n'
267
            '$runtimeType.applyBoundaryConditions reported an overscroll of $overscroll units.',
268
          );
269 270
        }
        return true;
271
      }());
272
      final double oldPixels = pixels;
273
      _pixels = newPixels - overscroll;
274 275
      if (_pixels != oldPixels) {
        notifyListeners();
276
        didUpdateScrollPositionBy(pixels - oldPixels);
277
      }
278 279 280
      if (overscroll != 0.0) {
        didOverscrollBy(overscroll);
        return overscroll;
281 282 283 284 285
      }
    }
    return 0.0;
  }

286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309
  /// Change the value of [pixels] to the new value, without notifying any
  /// customers.
  ///
  /// This is used to adjust the position while doing layout. In particular,
  /// this is typically called as a response to [applyViewportDimension] or
  /// [applyContentDimensions] (in both cases, if this method is called, those
  /// methods should then return false to indicate that the position has been
  /// adjusted).
  ///
  /// Calling this is rarely correct in other contexts. It will not immediately
  /// cause the rendering to change, since it does not notify the widgets or
  /// render objects that might be listening to this object: they will only
  /// change when they next read the value, which could be arbitrarily later. It
  /// is generally only appropriate in the very specific case of the value being
  /// corrected during layout (since then the value is immediately read), in the
  /// specific case of a [ScrollPosition] with a single viewport customer.
  ///
  /// To cause the position to jump or animate to a new value, consider [jumpTo]
  /// or [animateTo], which will honor the normal conventions for changing the
  /// scroll offset.
  ///
  /// To force the [pixels] to a particular value without honoring the normal
  /// conventions for changing the scroll offset, consider [forcePixels]. (But
  /// see the discussion there for why that might still be a bad idea.)
310 311 312
  ///
  /// See also:
  ///
313
  ///  * [correctBy], which is a method of [ViewportOffset] used
314 315
  ///    by viewport render objects to correct the offset during layout
  ///    without notifying its listeners.
316
  ///  * [jumpTo], for making changes to position while not in the
317
  ///    middle of layout and applying the new position immediately.
318
  ///  * [animateTo], which is like [jumpTo] but animating to the
319
  ///    destination offset.
320 321 322 323
  void correctPixels(double value) {
    _pixels = value;
  }

324 325 326 327 328 329 330 331 332 333 334 335 336 337 338
  /// Apply a layout-time correction to the scroll offset.
  ///
  /// This method should change the [pixels] value by `correction`, but without
  /// calling [notifyListeners]. It is called during layout by the
  /// [RenderViewport], before [applyContentDimensions]. After this method is
  /// called, the layout will be recomputed and that may result in this method
  /// being called again, though this should be very rare.
  ///
  /// See also:
  ///
  ///  * [jumpTo], for also changing the scroll position when not in layout.
  ///    [jumpTo] applies the change immediately and notifies its listeners.
  ///  * [correctPixels], which is used by the [ScrollPosition] itself to
  ///    set the offset initially during construction or after
  ///    [applyViewportDimension] or [applyContentDimensions] is called.
339 340
  @override
  void correctBy(double correction) {
341
    assert(
342
      hasPixels,
343
      'An initial pixels value must exist by calling correctPixels on the ScrollPosition',
344
    );
345
    _pixels = _pixels! + correction;
346
    _didChangeViewportDimensionOrReceiveCorrection = true;
347 348
  }

349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367
  /// Change the value of [pixels] to the new value, and notify any customers,
  /// but without honoring normal conventions for changing the scroll offset.
  ///
  /// This is used to implement [jumpTo]. It can also be used adjust the
  /// position when the dimensions of the viewport change. It should only be
  /// used when manually implementing the logic for honoring the relevant
  /// conventions of the class. For example, [ScrollPositionWithSingleContext]
  /// introduces [ScrollActivity] objects and uses [forcePixels] in conjunction
  /// with adjusting the activity, e.g. by calling
  /// [ScrollPositionWithSingleContext.goIdle], so that the activity does
  /// not immediately set the value back. (Consider, for instance, a case where
  /// one is using a [DrivenScrollActivity]. That object will ignore any calls
  /// to [forcePixels], which would result in the rendering stuttering: changing
  /// in response to [forcePixels], and then changing back to the next value
  /// derived from the animation.)
  ///
  /// To cause the position to jump or animate to a new value, consider [jumpTo]
  /// or [animateTo].
  ///
368 369 370
  /// This should not be called during layout (e.g. when setting the initial
  /// scroll offset). Consider [correctPixels] if you find you need to adjust
  /// the position during layout.
371
  @protected
372
  void forcePixels(double value) {
373
    assert(hasPixels);
374
    assert(value != null);
375
    _impliedVelocity = value - pixels;
376 377
    _pixels = value;
    notifyListeners();
378
    SchedulerBinding.instance!.addPostFrameCallback((Duration timeStamp) {
379 380
      _impliedVelocity = 0;
    });
381 382
  }

383 384 385 386 387 388 389 390 391 392 393 394
  /// Called whenever scrolling ends, to store the current scroll offset in a
  /// storage mechanism with a lifetime that matches the app's lifetime.
  ///
  /// The stored value will be used by [restoreScrollOffset] when the
  /// [ScrollPosition] is recreated, in the case of the [Scrollable] being
  /// disposed then recreated in the same session. This might happen, for
  /// instance, if a [ListView] is on one of the pages inside a [TabBarView],
  /// and that page is displayed, then hidden, then displayed again.
  ///
  /// The default implementation writes the [pixels] using the nearest
  /// [PageStorage] found from the [context]'s [ScrollContext.storageContext]
  /// property.
395
  // TODO(goderbauer): Deprecate this when state restoration supports all features of PageStorage.
396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412
  @protected
  void saveScrollOffset() {
    PageStorage.of(context.storageContext)?.writeState(context.storageContext, pixels);
  }

  /// Called whenever the [ScrollPosition] is created, to restore the scroll
  /// offset if possible.
  ///
  /// The value is stored by [saveScrollOffset] when the scroll position
  /// changes, so that it can be restored in the case of the [Scrollable] being
  /// disposed then recreated in the same session. This might happen, for
  /// instance, if a [ListView] is on one of the pages inside a [TabBarView],
  /// and that page is displayed, then hidden, then displayed again.
  ///
  /// The default implementation reads the value from the nearest [PageStorage]
  /// found from the [context]'s [ScrollContext.storageContext] property, and
  /// sets it using [correctPixels], if [pixels] is still null.
413 414 415
  ///
  /// This method is called from the constructor, so layout has not yet
  /// occurred, and the viewport dimensions aren't yet known when it is called.
416
  // TODO(goderbauer): Deprecate this when state restoration supports all features of PageStorage.
417 418
  @protected
  void restoreScrollOffset() {
419 420
    if (!hasPixels) {
      final double? value = PageStorage.of(context.storageContext)?.readState(context.storageContext) as double?;
421 422 423 424 425
      if (value != null)
        correctPixels(value);
    }
  }

426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457
  /// Called by [context] to restore the scroll offset to the provided value.
  ///
  /// The provided value has previously been provided to the [context] by
  /// calling [ScrollContext.saveOffset], e.g. from [saveOffset].
  ///
  /// This method may be called right after the scroll position is created
  /// before layout has occurred. In that case, `initialRestore` is set to true
  /// and the viewport dimensions will not be known yet. If the [context]
  /// doesn't have any information to restore the scroll offset this method is
  /// not called.
  ///
  /// The method may be called multiple times in the lifecycle of a
  /// [ScrollPosition] to restore it to different scroll offsets.
  void restoreOffset(double offset, {bool initialRestore = false}) {
    assert(initialRestore != null);
    assert(offset != null);
    if (initialRestore) {
      correctPixels(offset);
    } else {
      jumpTo(offset);
    }
  }

  /// Called whenever scrolling ends, to persist the current scroll offset for
  /// state restoration purposes.
  ///
  /// The default implementation stores the current value of [pixels] on the
  /// [context] by calling [ScrollContext.saveOffset]. At a later point in time
  /// or after the application restarts, the [context] may restore the scroll
  /// position to the persisted offset by calling [restoreOffset].
  @protected
  void saveOffset() {
458
    assert(hasPixels);
459 460 461
    context.saveOffset(pixels);
  }

462 463 464 465 466 467
  /// Returns the overscroll by applying the boundary conditions.
  ///
  /// If the given value is in bounds, returns 0.0. Otherwise, returns the
  /// amount of value that cannot be applied to [pixels] as a result of the
  /// boundary conditions. If the [physics] allow out-of-bounds scrolling, this
  /// method always returns 0.0.
468 469 470
  ///
  /// The default implementation defers to the [physics] object's
  /// [ScrollPhysics.applyBoundaryConditions].
471 472 473 474 475 476
  @protected
  double applyBoundaryConditions(double value) {
    final double result = physics.applyBoundaryConditions(this, value);
    assert(() {
      final double delta = value - pixels;
      if (result.abs() > delta.abs()) {
477 478 479 480 481 482 483 484
        throw FlutterError(
          '${physics.runtimeType}.applyBoundaryConditions returned invalid overscroll value.\n'
          'The method was called to consider a change from $pixels to $value, which is a '
          'delta of ${delta.toStringAsFixed(1)} units. However, it returned an overscroll of '
          '${result.toStringAsFixed(1)} units, which has a greater magnitude than the delta. '
          'The applyBoundaryConditions method is only supposed to reduce the possible range '
          'of movement, not increase it.\n'
          'The scroll extents are $minScrollExtent .. $maxScrollExtent, and the '
485
          'viewport dimension is $viewportDimension.',
486
        );
487 488
      }
      return true;
489
    }());
490 491
    return result;
  }
492

493
  bool _didChangeViewportDimensionOrReceiveCorrection = true;
494 495

  @override
496
  bool applyViewportDimension(double viewportDimension) {
497 498
    if (_viewportDimension != viewportDimension) {
      _viewportDimension = viewportDimension;
499
      _didChangeViewportDimensionOrReceiveCorrection = true;
500 501 502 503
      // If this is called, you can rely on applyContentDimensions being called
      // soon afterwards in the same layout phase. So we put all the logic that
      // relies on both values being computed into applyContentDimensions.
    }
504
    return true;
505 506
  }

507
  bool _pendingDimensions = false;
508
  ScrollMetrics? _lastMetrics;
509

510 511
  @override
  bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
512 513
    assert(minScrollExtent != null);
    assert(maxScrollExtent != null);
514
    assert(haveDimensions == (_lastMetrics != null));
515 516
    if (!nearEqual(_minScrollExtent, minScrollExtent, Tolerance.defaultTolerance.distance) ||
        !nearEqual(_maxScrollExtent, maxScrollExtent, Tolerance.defaultTolerance.distance) ||
517
        _didChangeViewportDimensionOrReceiveCorrection) {
518 519 520
      assert(minScrollExtent != null);
      assert(maxScrollExtent != null);
      assert(minScrollExtent <= maxScrollExtent);
521 522
      _minScrollExtent = minScrollExtent;
      _maxScrollExtent = maxScrollExtent;
523
      final ScrollMetrics? currentMetrics = haveDimensions ? copyWith() : null;
524
      _didChangeViewportDimensionOrReceiveCorrection = false;
525
      _pendingDimensions = true;
526
      if (haveDimensions && !correctForNewDimensions(_lastMetrics!, currentMetrics!)) {
527
        return false;
528
      }
529
      _haveDimensions = true;
530 531 532
    }
    assert(haveDimensions);
    if (_pendingDimensions) {
533
      applyNewDimensions();
534
      _pendingDimensions = false;
535 536
    }
    assert(!_didChangeViewportDimensionOrReceiveCorrection, 'Use correctForNewDimensions() (and return true) to change the scroll offset during applyContentDimensions().');
537
    _lastMetrics = copyWith();
538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558
    return true;
  }

  /// Verifies that the new content and viewport dimensions are acceptable.
  ///
  /// Called by [applyContentDimensions] to determine its return value.
  ///
  /// Should return true if the current scroll offset is correct given
  /// the new content and viewport dimensions.
  ///
  /// Otherwise, should call [correctPixels] to correct the scroll
  /// offset given the new dimensions, and then return false.
  ///
  /// This is only called when [haveDimensions] is true.
  ///
  /// The default implementation defers to [ScrollPhysics.adjustPositionForNewDimensions].
  @protected
  bool correctForNewDimensions(ScrollMetrics oldPosition, ScrollMetrics newPosition) {
    final double newPixels = physics.adjustPositionForNewDimensions(
      oldPosition: oldPosition,
      newPosition: newPosition,
559 560
      isScrolling: activity!.isScrolling,
      velocity: activity!.velocity,
561 562 563 564
    );
    if (newPixels != pixels) {
      correctPixels(newPixels);
      return false;
565
    }
566
    return true;
567 568
  }

569 570 571 572 573 574 575 576 577 578
  /// Notifies the activity that the dimensions of the underlying viewport or
  /// contents have changed.
  ///
  /// Called after [applyViewportDimension] or [applyContentDimensions] have
  /// changed the [minScrollExtent], the [maxScrollExtent], or the
  /// [viewportDimension]. When this method is called, it should be called
  /// _after_ any corrections are applied to [pixels] using [correctPixels], not
  /// before.
  ///
  /// The default implementation informs the [activity] of the new dimensions by
579
  /// calling its [ScrollActivity.applyNewDimensions] method.
580 581 582
  ///
  /// See also:
  ///
583 584 585 586 587
  ///  * [applyViewportDimension], which is called when new
  ///    viewport dimensions are established.
  ///  * [applyContentDimensions], which is called after new
  ///    viewport dimensions are established, and also if new content dimensions
  ///    are established, and which calls [ScrollPosition.applyNewDimensions].
588
  @protected
589 590
  @mustCallSuper
  void applyNewDimensions() {
591
    assert(hasPixels);
592
    assert(_pendingDimensions);
593
    activity!.applyNewDimensions();
594
    _updateSemanticActions(); // will potentially request a semantics update.
595
  }
596

597
  Set<SemanticsAction>? _semanticActions;
598 599 600 601 602 603 604 605 606 607 608 609 610 611

  /// Called whenever the scroll position or the dimensions of the scroll view
  /// change to schedule an update of the available semantics actions. The
  /// actual update will be performed in the next frame. If non is pending
  /// a frame will be scheduled.
  ///
  /// For example: If the scroll view has been scrolled all the way to the top,
  /// the action to scroll further up needs to be removed as the scroll view
  /// cannot be scrolled in that direction anymore.
  ///
  /// This method is potentially called twice per frame (if scroll position and
  /// scroll view dimensions both change) and therefore shouldn't do anything
  /// expensive.
  void _updateSemanticActions() {
612 613
    final SemanticsAction forward;
    final SemanticsAction backward;
614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642
    switch (axisDirection) {
      case AxisDirection.up:
        forward = SemanticsAction.scrollDown;
        backward = SemanticsAction.scrollUp;
        break;
      case AxisDirection.right:
        forward = SemanticsAction.scrollLeft;
        backward = SemanticsAction.scrollRight;
        break;
      case AxisDirection.down:
        forward = SemanticsAction.scrollUp;
        backward = SemanticsAction.scrollDown;
        break;
      case AxisDirection.left:
        forward = SemanticsAction.scrollRight;
        backward = SemanticsAction.scrollLeft;
        break;
    }

    final Set<SemanticsAction> actions = <SemanticsAction>{};
    if (pixels > minScrollExtent)
      actions.add(backward);
    if (pixels < maxScrollExtent)
      actions.add(forward);

    if (setEquals<SemanticsAction>(actions, _semanticActions))
      return;

    _semanticActions = actions;
643
    context.setSemanticsActions(_semanticActions!);
644 645
  }

646 647
  /// Animates the position such that the given object is as visible as possible
  /// by just scrolling this position.
648
  ///
649 650 651 652 653 654
  /// The optional `targetRenderObject` parameter is used to determine which area
  /// of that object should be as visible as possible. If `targetRenderObject`
  /// is null, the entire [RenderObject] (as defined by its
  /// [RenderObject.paintBounds]) will be as visible as possible. If
  /// `targetRenderObject` is provided, it must be a descendant of the object.
  ///
655 656
  /// See also:
  ///
657 658
  ///  * [ScrollPositionAlignmentPolicy] for the way in which `alignment` is
  ///    applied, and the way the given `object` is aligned.
659 660
  Future<void> ensureVisible(
    RenderObject object, {
661 662 663
    double alignment = 0.0,
    Duration duration = Duration.zero,
    Curve curve = Curves.ease,
664
    ScrollPositionAlignmentPolicy alignmentPolicy = ScrollPositionAlignmentPolicy.explicit,
665
    RenderObject? targetRenderObject,
666
  }) {
667
    assert(alignmentPolicy != null);
668
    assert(object.attached);
669
    final RenderAbstractViewport viewport = RenderAbstractViewport.of(object)!;
670
    assert(viewport != null);
671

672 673 674 675
    Rect? targetRect;
    if (targetRenderObject != null && targetRenderObject != object) {
      targetRect = MatrixUtils.transformRect(
        targetRenderObject.getTransformTo(object),
676
        object.paintBounds.intersect(targetRenderObject.paintBounds),
677 678 679
      );
    }

680 681 682
    double target;
    switch (alignmentPolicy) {
      case ScrollPositionAlignmentPolicy.explicit:
683
        target = viewport.getOffsetToReveal(object, alignment, rect: targetRect).offset.clamp(minScrollExtent, maxScrollExtent);
684 685
        break;
      case ScrollPositionAlignmentPolicy.keepVisibleAtEnd:
686
        target = viewport.getOffsetToReveal(object, 1.0, rect: targetRect).offset.clamp(minScrollExtent, maxScrollExtent);
687 688 689 690 691
        if (target < pixels) {
          target = pixels;
        }
        break;
      case ScrollPositionAlignmentPolicy.keepVisibleAtStart:
692
        target = viewport.getOffsetToReveal(object, 0.0, rect: targetRect).offset.clamp(minScrollExtent, maxScrollExtent);
693 694 695 696 697
        if (target > pixels) {
          target = pixels;
        }
        break;
    }
698

699
    if (target == pixels)
700
      return Future<void>.value();
701

702
    if (duration == Duration.zero) {
703
      jumpTo(target);
704
      return Future<void>.value();
705
    }
706

707
    return animateTo(target, duration: duration, curve: curve);
708 709
  }

710 711 712
  /// This notifier's value is true if a scroll is underway and false if the scroll
  /// position is idle.
  ///
713
  /// Listeners added by stateful widgets should be removed in the widget's
714
  /// [State.dispose] method.
715
  final ValueNotifier<bool> isScrollingNotifier = ValueNotifier<bool>(false);
716

717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743
  /// Animates the position from its current value to the given value.
  ///
  /// Any active animation is canceled. If the user is currently scrolling, that
  /// action is canceled.
  ///
  /// The returned [Future] will complete when the animation ends, whether it
  /// completed successfully or whether it was interrupted prematurely.
  ///
  /// An animation will be interrupted whenever the user attempts to scroll
  /// manually, or whenever another activity is started, or whenever the
  /// animation reaches the edge of the viewport and attempts to overscroll. (If
  /// the [ScrollPosition] does not overscroll but instead allows scrolling
  /// beyond the extents, then going beyond the extents will not interrupt the
  /// animation.)
  ///
  /// The animation is indifferent to changes to the viewport or content
  /// dimensions.
  ///
  /// Once the animation has completed, the scroll position will attempt to
  /// begin a ballistic activity in case its value is not stable (for example,
  /// if it is scrolled beyond the extents and in that situation the scroll
  /// position would normally bounce back).
  ///
  /// The duration must not be zero. To jump to a particular value without an
  /// animation, use [jumpTo].
  ///
  /// The animation is typically handled by an [DrivenScrollActivity].
744
  @override
745 746
  Future<void> animateTo(
    double to, {
747 748
    required Duration duration,
    required Curve curve,
749
  });
750

751 752 753 754 755 756 757 758 759
  /// Jumps the scroll position from its current value to the given value,
  /// without animation, and without checking if the new value is in range.
  ///
  /// Any active animation is canceled. If the user is currently scrolling, that
  /// action is canceled.
  ///
  /// If this method changes the scroll position, a sequence of start/update/end
  /// scroll notifications will be dispatched. No overscroll notifications can
  /// be generated by this method.
760
  @override
761
  void jumpTo(double value);
762

763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778
  /// Changes the scrolling position based on a pointer signal from current
  /// value to delta without animation and without checking if new value is in
  /// range, taking min/max scroll extent into account.
  ///
  /// Any active animation is canceled. If the user is currently scrolling, that
  /// action is canceled.
  ///
  /// This method dispatches the start/update/end sequence of scrolling
  /// notifications.
  ///
  /// This method is very similar to [jumpTo], but [pointerScroll] will
  /// update the [ScrollDirection].
  ///
  // TODO(YeungKC): Support trackpad scroll, https://github.com/flutter/flutter/issues/23604.
  void pointerScroll(double delta);

779 780 781 782 783 784 785 786
  /// Calls [jumpTo] if duration is null or [Duration.zero], otherwise
  /// [animateTo] is called.
  ///
  /// If [clamp] is true (the default) then [to] is adjusted to prevent over or
  /// underscroll.
  ///
  /// If [animateTo] is called then [curve] defaults to [Curves.ease].
  @override
787 788
  Future<void> moveTo(
    double to, {
789 790 791
    Duration? duration,
    Curve? curve,
    bool? clamp = true,
792 793 794 795
  }) {
    assert(to != null);
    assert(clamp != null);

796 797
    if (clamp!)
      to = to.clamp(minScrollExtent, maxScrollExtent);
798 799 800 801

    return super.moveTo(to, duration: duration, curve: curve);
  }

802 803 804
  @override
  bool get allowImplicitScrolling => physics.allowImplicitScrolling;

805
  /// Deprecated. Use [jumpTo] or a custom [ScrollPosition] instead.
806
  @Deprecated('This will lead to bugs.') // flutter_ignore: deprecation_syntax, https://github.com/flutter/flutter/issues/44609
807
  void jumpToWithoutSettling(double value);
808

809
  /// Stop the current activity and start a [HoldScrollActivity].
810
  ScrollHoldController hold(VoidCallback holdCancelCallback);
811

812 813 814 815 816
  /// Start a drag activity corresponding to the given [DragStartDetails].
  ///
  /// The `onDragCanceled` argument will be invoked if the drag is ended
  /// prematurely (e.g. from another activity taking over). See
  /// [ScrollDragController.onDragCanceled] for details.
817
  Drag drag(DragStartDetails details, VoidCallback dragCancelCallback);
818

819 820 821 822 823 824 825
  /// The currently operative [ScrollActivity].
  ///
  /// If the scroll position is not performing any more specific activity, the
  /// activity will be an [IdleScrollActivity]. To determine whether the scroll
  /// position is idle, check the [isScrollingNotifier].
  ///
  /// Call [beginActivity] to change the current activity.
826
  @protected
827
  @visibleForTesting
828 829
  ScrollActivity? get activity => _activity;
  ScrollActivity? _activity;
830

831 832 833 834 835 836
  /// Change the current [activity], disposing of the old one and
  /// sending scroll notifications as necessary.
  ///
  /// If the argument is null, this method has no effect. This is convenient for
  /// cases where the new activity is obtained from another method, and that
  /// method might return null, since it means the caller does not have to
837
  /// explicitly null-check the argument.
838
  void beginActivity(ScrollActivity? newActivity) {
839 840 841 842
    if (newActivity == null)
      return;
    bool wasScrolling, oldIgnorePointer;
    if (_activity != null) {
843 844
      oldIgnorePointer = _activity!.shouldIgnorePointer;
      wasScrolling = _activity!.isScrolling;
845
      if (wasScrolling && !newActivity.isScrolling)
846
        didEndScroll(); // notifies and then saves the scroll offset
847
      _activity!.dispose();
848 849 850 851 852
    } else {
      oldIgnorePointer = false;
      wasScrolling = false;
    }
    _activity = newActivity;
853 854 855 856
    if (oldIgnorePointer != activity!.shouldIgnorePointer)
      context.setIgnorePointer(activity!.shouldIgnorePointer);
    isScrollingNotifier.value = activity!.isScrolling;
    if (!wasScrolling && _activity!.isScrolling)
857 858 859 860 861 862 863 864
      didStartScroll();
  }


  // NOTIFICATION DISPATCH

  /// Called by [beginActivity] to report when an activity has started.
  void didStartScroll() {
865
    activity!.dispatchScrollStartNotification(copyWith(), context.notificationContext);
866 867 868 869
  }

  /// Called by [setPixels] to report a change to the [pixels] position.
  void didUpdateScrollPositionBy(double delta) {
870
    activity!.dispatchScrollUpdateNotification(copyWith(), context.notificationContext!, delta);
871 872 873
  }

  /// Called by [beginActivity] to report when an activity has ended.
874 875
  ///
  /// This also saves the scroll offset using [saveScrollOffset].
876
  void didEndScroll() {
877
    activity!.dispatchScrollEndNotification(copyWith(), context.notificationContext!);
878
    saveOffset();
879 880
    if (keepScrollOffset)
      saveScrollOffset();
881 882 883 884 885 886
  }

  /// Called by [setPixels] to report overscroll when an attempt is made to
  /// change the [pixels] position. Overscroll is the amount of change that was
  /// not applied to the [pixels] value.
  void didOverscrollBy(double value) {
887 888
    assert(activity!.isScrolling);
    activity!.dispatchOverscrollNotification(copyWith(), context.notificationContext!, value);
889 890 891 892 893 894
  }

  /// Dispatches a notification that the [userScrollDirection] has changed.
  ///
  /// Subclasses should call this function when they change [userScrollDirection].
  void didUpdateScrollDirection(ScrollDirection direction) {
895
    UserScrollNotification(metrics: copyWith(), context: context.notificationContext!, direction: direction).dispatch(context.notificationContext);
896 897
  }

898 899 900 901
  /// Provides a heuristic to determine if expensive frame-bound tasks should be
  /// deferred.
  ///
  /// The actual work of this is delegated to the [physics] via
902
  /// [ScrollPhysics.recommendDeferredLoading] called with the current
903 904 905 906 907 908 909 910
  /// [activity]'s [ScrollActivity.velocity].
  ///
  /// Returning true from this method indicates that the [ScrollPhysics]
  /// evaluate the current scroll velocity to be great enough that expensive
  /// operations impacting the UI should be deferred.
  bool recommendDeferredLoading(BuildContext context) {
    assert(context != null);
    assert(activity != null);
911
    assert(activity!.velocity != null);
912 913
    assert(_impliedVelocity != null);
    return physics.recommendDeferredLoading(
914
      activity!.velocity + _impliedVelocity,
915 916 917
      copyWith(),
      context,
    );
918 919
  }

920 921 922 923 924 925
  @override
  void dispose() {
    activity?.dispose(); // it will be null if it got absorbed by another ScrollPosition
    _activity = null;
    super.dispose();
  }
926

927 928
  @override
  void notifyListeners() {
929
    _updateSemanticActions(); // will potentially request a semantics update.
930 931 932
    super.notifyListeners();
  }

933
  @override
934
  void debugFillDescription(List<String> description) {
935
    if (debugLabel != null)
936
      description.add(debugLabel!);
937
    super.debugFillDescription(description);
938 939
    description.add('range: ${_minScrollExtent?.toStringAsFixed(1)}..${_maxScrollExtent?.toStringAsFixed(1)}');
    description.add('viewport: ${_viewportDimension?.toStringAsFixed(1)}');
940 941
  }
}