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

5 6
import 'dart:async';

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

import 'basic.dart';
import 'framework.dart';
15
import 'notification_listener.dart';
16
import 'page_storage.dart';
17 18
import 'scroll_activity.dart';
import 'scroll_context.dart';
19
import 'scroll_metrics.dart';
20
import 'scroll_notification.dart';
21
import 'scroll_physics.dart';
22

23 24
export 'scroll_activity.dart' show ScrollHoldController;

25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
/// 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,
}

51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
/// 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].
///
66
/// This object is a [Listenable] that notifies its listeners when [pixels]
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
/// 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].
88
///  * [ScrollNotification] and [NotificationListener], which can be used to watch
89
///    the scroll position without using a [ScrollController].
90
abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
91 92
  /// Creates an object that determines which portion of the content is visible
  /// in a scroll view.
93 94
  ///
  /// The [physics], [context], and [keepScrollOffset] parameters must not be null.
95
  ScrollPosition({
96 97
    required this.physics,
    required this.context,
98
    this.keepScrollOffset = true,
99
    ScrollPosition? oldPosition,
100
    this.debugLabel,
101 102
  }) : assert(physics != null),
       assert(context != null),
103 104
       assert(context.vsync != null),
       assert(keepScrollOffset != null) {
105 106
    if (oldPosition != null)
      absorb(oldPosition);
107 108
    if (keepScrollOffset)
      restoreScrollOffset();
109
  }
110

111 112 113 114
  /// 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.
115
  final ScrollPhysics physics;
116 117 118 119

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

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

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

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

142
  @override
143 144
  double get maxScrollExtent => _maxScrollExtent!;
  double? _maxScrollExtent;
145 146

  @override
147
  bool get hasContentDimensions => _minScrollExtent != null && _maxScrollExtent != null;
148

149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166
  /// 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;

167
  @override
168 169 170 171 172
  double get pixels => _pixels!;
  double? _pixels;

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

174
  @override
175 176 177 178 179
  double get viewportDimension => _viewportDimension!;
  double? _viewportDimension;

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

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

188
  /// Take any current applicable state from the given [ScrollPosition].
189
  ///
190
  /// This method is called by the constructor if it is given an `oldPosition`.
191
  /// The `other` argument might not have the same [runtimeType] as this object.
192
  ///
193 194 195 196
  /// 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).
197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214
  ///
  /// 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].
215 216 217 218
  @protected
  @mustCallSuper
  void absorb(ScrollPosition other) {
    assert(other != null);
219
    assert(other.context == context);
220
    assert(_pixels == null);
221 222 223 224 225 226 227 228 229 230
    if (other.hasContentDimensions) {
      _minScrollExtent = other.minScrollExtent;
      _maxScrollExtent = other.maxScrollExtent;
    }
    if (other.hasPixels) {
      _pixels = other.pixels;
    }
    if (other.hasViewportDimension) {
      _viewportDimension = other.viewportDimension;
    }
231 232 233 234 235 236

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

  /// 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).
  ///
254 255 256 257 258
  /// 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) {
259
    assert(hasPixels);
260
    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.");
261
    if (newPixels != pixels) {
262
      final double overscroll = applyBoundaryConditions(newPixels);
263
      assert(() {
264
        final double delta = newPixels - pixels;
265
        if (overscroll.abs() > delta.abs()) {
266 267 268 269
          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'
270
            '$runtimeType.applyBoundaryConditions reported an overscroll of $overscroll units.',
271
          );
272 273
        }
        return true;
274
      }());
275
      final double oldPixels = pixels;
276
      _pixels = newPixels - overscroll;
277 278
      if (_pixels != oldPixels) {
        notifyListeners();
279
        didUpdateScrollPositionBy(pixels - oldPixels);
280
      }
281 282 283
      if (overscroll != 0.0) {
        didOverscrollBy(overscroll);
        return overscroll;
284 285 286 287 288
      }
    }
    return 0.0;
  }

289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312
  /// 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.)
313 314 315
  ///
  /// See also:
  ///
316
  ///  * [correctBy], which is a method of [ViewportOffset] used
317 318
  ///    by viewport render objects to correct the offset during layout
  ///    without notifying its listeners.
319
  ///  * [jumpTo], for making changes to position while not in the
320
  ///    middle of layout and applying the new position immediately.
321
  ///  * [animateTo], which is like [jumpTo] but animating to the
322
  ///    destination offset.
323
  // ignore: use_setters_to_change_properties, (API is intended to discourage setting value)
324 325 326 327
  void correctPixels(double value) {
    _pixels = value;
  }

328 329 330 331 332 333 334 335 336 337 338 339 340 341 342
  /// 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.
343 344
  @override
  void correctBy(double correction) {
345
    assert(
346
      hasPixels,
347
      'An initial pixels value must exist by calling correctPixels on the ScrollPosition',
348
    );
349
    _pixels = _pixels! + correction;
350
    _didChangeViewportDimensionOrReceiveCorrection = true;
351 352
  }

353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371
  /// 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].
  ///
372 373 374
  /// 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.
375
  @protected
376
  void forcePixels(double value) {
377
    assert(hasPixels);
378
    assert(value != null);
379
    _impliedVelocity = value - pixels;
380 381
    _pixels = value;
    notifyListeners();
382
    SchedulerBinding.instance!.addPostFrameCallback((Duration timeStamp) {
383 384
      _impliedVelocity = 0;
    });
385 386
  }

387 388 389 390 391 392 393 394 395 396 397 398
  /// 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.
399
  // TODO(goderbauer): Deprecate this when state restoration supports all features of PageStorage.
400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416
  @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.
417 418 419
  ///
  /// 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.
420
  // TODO(goderbauer): Deprecate this when state restoration supports all features of PageStorage.
421 422
  @protected
  void restoreScrollOffset() {
423 424
    if (!hasPixels) {
      final double? value = PageStorage.of(context.storageContext)?.readState(context.storageContext) as double?;
425 426 427 428 429
      if (value != null)
        correctPixels(value);
    }
  }

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 458 459 460 461
  /// 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() {
462
    assert(hasPixels);
463 464 465
    context.saveOffset(pixels);
  }

466 467 468 469 470 471
  /// 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.
472 473 474
  ///
  /// The default implementation defers to the [physics] object's
  /// [ScrollPhysics.applyBoundaryConditions].
475 476 477 478 479 480
  @protected
  double applyBoundaryConditions(double value) {
    final double result = physics.applyBoundaryConditions(this, value);
    assert(() {
      final double delta = value - pixels;
      if (result.abs() > delta.abs()) {
481 482 483 484 485 486 487 488
        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 '
489
          'viewport dimension is $viewportDimension.',
490
        );
491 492
      }
      return true;
493
    }());
494 495
    return result;
  }
496

497
  bool _didChangeViewportDimensionOrReceiveCorrection = true;
498 499

  @override
500
  bool applyViewportDimension(double viewportDimension) {
501 502
    if (_viewportDimension != viewportDimension) {
      _viewportDimension = viewportDimension;
503
      _didChangeViewportDimensionOrReceiveCorrection = true;
504 505 506 507
      // 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.
    }
508
    return true;
509 510
  }

511
  bool _pendingDimensions = false;
512
  ScrollMetrics? _lastMetrics;
513
  // True indicates that there is a ScrollMetrics update notification pending.
514
  bool _haveScheduledUpdateNotification = false;
515
  Axis? _lastAxis;
516

517 518 519 520 521 522 523 524 525 526 527
  bool _isMetricsChanged() {
    assert(haveDimensions);
    final ScrollMetrics currentMetrics = copyWith();

    return _lastMetrics == null ||
      !(currentMetrics.extentBefore == _lastMetrics!.extentBefore
      && currentMetrics.extentInside == _lastMetrics!.extentInside
      && currentMetrics.extentAfter == _lastMetrics!.extentAfter
      && currentMetrics.axisDirection == _lastMetrics!.axisDirection);
  }

528 529
  @override
  bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
530 531
    assert(minScrollExtent != null);
    assert(maxScrollExtent != null);
532
    assert(haveDimensions == (_lastMetrics != null));
533 534
    if (!nearEqual(_minScrollExtent, minScrollExtent, Tolerance.defaultTolerance.distance) ||
        !nearEqual(_maxScrollExtent, maxScrollExtent, Tolerance.defaultTolerance.distance) ||
535 536
        _didChangeViewportDimensionOrReceiveCorrection ||
        _lastAxis != axis) {
537 538 539
      assert(minScrollExtent != null);
      assert(maxScrollExtent != null);
      assert(minScrollExtent <= maxScrollExtent);
540 541
      _minScrollExtent = minScrollExtent;
      _maxScrollExtent = maxScrollExtent;
542
      _lastAxis = axis;
543
      final ScrollMetrics? currentMetrics = haveDimensions ? copyWith() : null;
544
      _didChangeViewportDimensionOrReceiveCorrection = false;
545
      _pendingDimensions = true;
546
      if (haveDimensions && !correctForNewDimensions(_lastMetrics!, currentMetrics!)) {
547
        return false;
548
      }
549
      _haveDimensions = true;
550 551 552
    }
    assert(haveDimensions);
    if (_pendingDimensions) {
553
      applyNewDimensions();
554
      _pendingDimensions = false;
555 556
    }
    assert(!_didChangeViewportDimensionOrReceiveCorrection, 'Use correctForNewDimensions() (and return true) to change the scroll offset during applyContentDimensions().');
557 558 559 560 561

    if (_isMetricsChanged()) {
      // It isn't safe to trigger the ScrollMetricsNotification if we are in
      // the middle of rendering the frame, the developer is likely to schedule
      // a new frame(build scheduled during frame is illegal).
562
      if (!_haveScheduledUpdateNotification) {
563 564 565 566 567
        scheduleMicrotask(didUpdateScrollMetrics);
        _haveScheduledUpdateNotification = true;
      }
      _lastMetrics = copyWith();
    }
568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588
    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,
589 590
      isScrolling: activity!.isScrolling,
      velocity: activity!.velocity,
591 592 593 594
    );
    if (newPixels != pixels) {
      correctPixels(newPixels);
      return false;
595
    }
596
    return true;
597 598
  }

599 600 601 602 603 604 605 606 607 608
  /// 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
609
  /// calling its [ScrollActivity.applyNewDimensions] method.
610 611 612
  ///
  /// See also:
  ///
613 614 615 616 617
  ///  * [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].
618
  @protected
619 620
  @mustCallSuper
  void applyNewDimensions() {
621
    assert(hasPixels);
622
    assert(_pendingDimensions);
623
    activity!.applyNewDimensions();
624
    _updateSemanticActions(); // will potentially request a semantics update.
625
  }
626

627
  Set<SemanticsAction>? _semanticActions;
628 629 630 631 632 633 634 635 636 637 638 639 640 641

  /// 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() {
642 643
    final SemanticsAction forward;
    final SemanticsAction backward;
644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672
    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;
673
    context.setSemanticsActions(_semanticActions!);
674 675
  }

676 677
  /// Animates the position such that the given object is as visible as possible
  /// by just scrolling this position.
678
  ///
679 680 681 682 683 684
  /// 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.
  ///
685 686
  /// See also:
  ///
687 688
  ///  * [ScrollPositionAlignmentPolicy] for the way in which `alignment` is
  ///    applied, and the way the given `object` is aligned.
689 690
  Future<void> ensureVisible(
    RenderObject object, {
691 692 693
    double alignment = 0.0,
    Duration duration = Duration.zero,
    Curve curve = Curves.ease,
694
    ScrollPositionAlignmentPolicy alignmentPolicy = ScrollPositionAlignmentPolicy.explicit,
695
    RenderObject? targetRenderObject,
696
  }) {
697
    assert(alignmentPolicy != null);
698
    assert(object.attached);
699
    final RenderAbstractViewport viewport = RenderAbstractViewport.of(object)!;
700
    assert(viewport != null);
701

702 703 704 705
    Rect? targetRect;
    if (targetRenderObject != null && targetRenderObject != object) {
      targetRect = MatrixUtils.transformRect(
        targetRenderObject.getTransformTo(object),
706
        object.paintBounds.intersect(targetRenderObject.paintBounds),
707 708 709
      );
    }

710 711 712
    double target;
    switch (alignmentPolicy) {
      case ScrollPositionAlignmentPolicy.explicit:
713
        target = viewport.getOffsetToReveal(object, alignment, rect: targetRect).offset.clamp(minScrollExtent, maxScrollExtent);
714 715
        break;
      case ScrollPositionAlignmentPolicy.keepVisibleAtEnd:
716
        target = viewport.getOffsetToReveal(object, 1.0, rect: targetRect).offset.clamp(minScrollExtent, maxScrollExtent);
717 718 719 720 721
        if (target < pixels) {
          target = pixels;
        }
        break;
      case ScrollPositionAlignmentPolicy.keepVisibleAtStart:
722
        target = viewport.getOffsetToReveal(object, 0.0, rect: targetRect).offset.clamp(minScrollExtent, maxScrollExtent);
723 724 725 726 727
        if (target > pixels) {
          target = pixels;
        }
        break;
    }
728

729
    if (target == pixels)
730
      return Future<void>.value();
731

732
    if (duration == Duration.zero) {
733
      jumpTo(target);
734
      return Future<void>.value();
735
    }
736

737
    return animateTo(target, duration: duration, curve: curve);
738 739
  }

740 741 742
  /// This notifier's value is true if a scroll is underway and false if the scroll
  /// position is idle.
  ///
743
  /// Listeners added by stateful widgets should be removed in the widget's
744
  /// [State.dispose] method.
745
  final ValueNotifier<bool> isScrollingNotifier = ValueNotifier<bool>(false);
746

747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773
  /// 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].
774
  @override
775 776
  Future<void> animateTo(
    double to, {
777 778
    required Duration duration,
    required Curve curve,
779
  });
780

781 782 783 784 785 786 787 788 789
  /// 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.
790
  @override
791
  void jumpTo(double value);
792

793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808
  /// 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);

809 810 811 812 813 814 815 816
  /// 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
817 818
  Future<void> moveTo(
    double to, {
819 820 821
    Duration? duration,
    Curve? curve,
    bool? clamp = true,
822 823 824 825
  }) {
    assert(to != null);
    assert(clamp != null);

826 827
    if (clamp!)
      to = to.clamp(minScrollExtent, maxScrollExtent);
828 829 830 831

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

832 833 834
  @override
  bool get allowImplicitScrolling => physics.allowImplicitScrolling;

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

839
  /// Stop the current activity and start a [HoldScrollActivity].
840
  ScrollHoldController hold(VoidCallback holdCancelCallback);
841

842 843 844 845 846
  /// 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.
847
  Drag drag(DragStartDetails details, VoidCallback dragCancelCallback);
848

849 850 851 852 853 854 855
  /// 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.
856
  @protected
857
  @visibleForTesting
858 859
  ScrollActivity? get activity => _activity;
  ScrollActivity? _activity;
860

861 862 863 864 865 866
  /// 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
867
  /// explicitly null-check the argument.
868
  void beginActivity(ScrollActivity? newActivity) {
869 870 871 872
    if (newActivity == null)
      return;
    bool wasScrolling, oldIgnorePointer;
    if (_activity != null) {
873 874
      oldIgnorePointer = _activity!.shouldIgnorePointer;
      wasScrolling = _activity!.isScrolling;
875
      if (wasScrolling && !newActivity.isScrolling)
876
        didEndScroll(); // notifies and then saves the scroll offset
877
      _activity!.dispose();
878 879 880 881 882
    } else {
      oldIgnorePointer = false;
      wasScrolling = false;
    }
    _activity = newActivity;
883 884 885 886
    if (oldIgnorePointer != activity!.shouldIgnorePointer)
      context.setIgnorePointer(activity!.shouldIgnorePointer);
    isScrollingNotifier.value = activity!.isScrolling;
    if (!wasScrolling && _activity!.isScrolling)
887 888 889 890 891 892 893 894
      didStartScroll();
  }


  // NOTIFICATION DISPATCH

  /// Called by [beginActivity] to report when an activity has started.
  void didStartScroll() {
895
    activity!.dispatchScrollStartNotification(copyWith(), context.notificationContext);
896 897 898 899
  }

  /// Called by [setPixels] to report a change to the [pixels] position.
  void didUpdateScrollPositionBy(double delta) {
900
    activity!.dispatchScrollUpdateNotification(copyWith(), context.notificationContext!, delta);
901 902 903
  }

  /// Called by [beginActivity] to report when an activity has ended.
904 905
  ///
  /// This also saves the scroll offset using [saveScrollOffset].
906
  void didEndScroll() {
907
    activity!.dispatchScrollEndNotification(copyWith(), context.notificationContext!);
908
    saveOffset();
909 910
    if (keepScrollOffset)
      saveScrollOffset();
911 912 913 914 915 916
  }

  /// 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) {
917 918
    assert(activity!.isScrolling);
    activity!.dispatchOverscrollNotification(copyWith(), context.notificationContext!, value);
919 920 921 922 923 924
  }

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

928 929 930 931 932 933 934 935 936
  /// Dispatches a notification that the [ScrollMetrics] have changed.
  void didUpdateScrollMetrics() {
    assert(SchedulerBinding.instance!.schedulerPhase != SchedulerPhase.persistentCallbacks);
    assert(_haveScheduledUpdateNotification);
    _haveScheduledUpdateNotification = false;
    if (context.notificationContext != null)
      ScrollMetricsNotification(metrics: copyWith(), context: context.notificationContext!).dispatch(context.notificationContext);
  }

937 938 939 940
  /// Provides a heuristic to determine if expensive frame-bound tasks should be
  /// deferred.
  ///
  /// The actual work of this is delegated to the [physics] via
941
  /// [ScrollPhysics.recommendDeferredLoading] called with the current
942 943 944 945 946 947 948 949
  /// [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);
950
    assert(activity!.velocity != null);
951 952
    assert(_impliedVelocity != null);
    return physics.recommendDeferredLoading(
953
      activity!.velocity + _impliedVelocity,
954 955 956
      copyWith(),
      context,
    );
957 958
  }

959 960 961 962 963 964
  @override
  void dispose() {
    activity?.dispose(); // it will be null if it got absorbed by another ScrollPosition
    _activity = null;
    super.dispose();
  }
965

966 967
  @override
  void notifyListeners() {
968
    _updateSemanticActions(); // will potentially request a semantics update.
969 970 971
    super.notifyListeners();
  }

972
  @override
973
  void debugFillDescription(List<String> description) {
974
    if (debugLabel != null)
975
      description.add(debugLabel!);
976
    super.debugFillDescription(description);
977 978
    description.add('range: ${_minScrollExtent?.toStringAsFixed(1)}..${_maxScrollExtent?.toStringAsFixed(1)}');
    description.add('viewport: ${_viewportDimension?.toStringAsFixed(1)}');
979 980
  }
}
981 982 983 984 985 986 987 988 989 990 991 992

/// A notification that a scrollable widget's [ScrollMetrics] have changed.
///
/// For example, when the content of a scrollable is altered, making it larger
/// or smaller, this notification will be dispatched. Similarly, if the size
/// of the window or parent changes, the scrollable can notify of these
/// changes in dimensions.
///
/// The above behaviors usually do not trigger [ScrollNotification] events,
/// so this is useful for listening to [ScrollMetrics] changes that are not
/// caused by the user scrolling.
///
993
/// {@tool dartpad}
994 995 996 997
/// This sample shows how a [ScrollMetricsNotification] is dispatched when
/// the `windowSize` is changed. Press the floating action button to increase
/// the scrollable window's size.
///
998
/// ** See code in examples/api/lib/widgets/scroll_position/scroll_metrics_notification.0.dart **
999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022
/// {@end-tool}
class ScrollMetricsNotification extends Notification with ViewportNotificationMixin {
  /// Creates a notification that the scrollable widget's [ScrollMetrics] have
  /// changed.
  ScrollMetricsNotification({
    required this.metrics,
    required this.context,
  });

  /// Description of a scrollable widget's [ScrollMetrics].
  final ScrollMetrics metrics;

  /// The build context of the widget that fired this notification.
  ///
  /// This can be used to find the scrollable widget's render objects to
  /// determine the size of the viewport, for instance.
  final BuildContext context;

  @override
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    description.add('$metrics');
  }
}