scroll_position.dart 40.5 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 324 325 326
  void correctPixels(double value) {
    _pixels = value;
  }

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

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

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

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

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

496
  bool _didChangeViewportDimensionOrReceiveCorrection = true;
497 498

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

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

516 517 518 519 520 521 522 523 524 525 526
  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);
  }

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

    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).
561
      if (!_haveScheduledUpdateNotification) {
562 563 564 565 566
        scheduleMicrotask(didUpdateScrollMetrics);
        _haveScheduledUpdateNotification = true;
      }
      _lastMetrics = copyWith();
    }
567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587
    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,
588 589
      isScrolling: activity!.isScrolling,
      velocity: activity!.velocity,
590 591 592 593
    );
    if (newPixels != pixels) {
      correctPixels(newPixels);
      return false;
594
    }
595
    return true;
596 597
  }

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

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

  /// 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() {
641 642
    final SemanticsAction forward;
    final SemanticsAction backward;
643 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
    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;
672
    context.setSemanticsActions(_semanticActions!);
673 674
  }

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

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

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

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

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

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

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

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
  /// 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].
773
  @override
774 775
  Future<void> animateTo(
    double to, {
776 777
    required Duration duration,
    required Curve curve,
778
  });
779

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

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

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

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

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

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

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

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

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

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

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


  // NOTIFICATION DISPATCH

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

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

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

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

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

927 928 929 930 931 932 933 934 935
  /// 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);
  }

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

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

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

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

/// 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.
///
/// {@tool dartpad --template=freeform}
/// 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.
///
997
/// ** See code in examples/api/lib/widgets/scroll_position/scroll_metrics_notification.0.dart **
998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021
/// {@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');
  }
}