scroll_position.dart 27.8 KB
Newer Older
1 2 3 4 5 6 7 8
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

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

import 'basic.dart';
import 'framework.dart';
import 'gesture_detector.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
/// 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].
///
40
/// This object is a [Listenable] that notifies its listeners when [pixels]
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
/// 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].
62
///  * [ScrollNotification] and [NotificationListener], which can be used to watch
63
///    the scroll position without using a [ScrollController].
64
abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
65 66
  /// Creates an object that determines which portion of the content is visible
  /// in a scroll view.
67 68
  ///
  /// The [physics], [context], and [keepScrollOffset] parameters must not be null.
69 70
  ScrollPosition({
    @required this.physics,
71
    @required this.context,
72
    this.keepScrollOffset = true,
73
    ScrollPosition oldPosition,
74
    this.debugLabel,
75 76
  }) : assert(physics != null),
       assert(context != null),
77 78
       assert(context.vsync != null),
       assert(keepScrollOffset != null) {
79 80
    if (oldPosition != null)
      absorb(oldPosition);
81 82
    if (keepScrollOffset)
      restoreScrollOffset();
83
  }
84

85 86 87 88
  /// 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.
89
  final ScrollPhysics physics;
90 91 92 93

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

96
  /// Save the current scroll offset with [PageStorage] and restore it if
97 98 99 100 101 102 103 104
  /// this scroll position's scrollable is recreated.
  ///
  /// See also:
  ///
  ///  * [ScrollController.keepScrollOffset] and [PageController.keepPage], which
  ///    create scroll positions and initialize this property.
  final bool keepScrollOffset;

105 106 107 108
  /// A label that is used in the [toString] output.
  ///
  /// Intended to aid with identifying animation controller instances in debug
  /// output.
109
  final String debugLabel;
110

111 112 113 114 115 116 117
  @override
  double get minScrollExtent => _minScrollExtent;
  double _minScrollExtent;

  @override
  double get maxScrollExtent => _maxScrollExtent;
  double _maxScrollExtent;
118

119 120
  @override
  double get pixels => _pixels;
121
  double _pixels;
122

123 124 125
  @override
  double get viewportDimension => _viewportDimension;
  double _viewportDimension;
126

127
  /// Whether [viewportDimension], [minScrollExtent], [maxScrollExtent],
128
  /// [outOfRange], and [atEdge] are available.
129
  ///
130
  /// Set to true just before the first time [applyNewDimensions] is called.
131 132
  bool get haveDimensions => _haveDimensions;
  bool _haveDimensions = false;
133

134
  /// Take any current applicable state from the given [ScrollPosition].
135
  ///
136
  /// This method is called by the constructor if it is given an `oldPosition`.
137
  /// The `other` argument might not have the same [runtimeType] as this object.
138
  ///
139 140 141 142
  /// 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).
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
  ///
  /// 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].
161 162 163 164
  @protected
  @mustCallSuper
  void absorb(ScrollPosition other) {
    assert(other != null);
165
    assert(other.context == context);
166 167 168 169 170
    assert(_pixels == null);
    _minScrollExtent = other.minScrollExtent;
    _maxScrollExtent = other.maxScrollExtent;
    _pixels = other._pixels;
    _viewportDimension = other.viewportDimension;
171 172 173 174 175 176 177 178 179

    assert(activity == null);
    assert(other.activity != null);
    _activity = other.activity;
    other._activity = null;
    if (other.runtimeType != runtimeType)
      activity.resetActivity();
    context.setIgnorePointer(activity.shouldIgnorePointer);
    isScrollingNotifier.value = activity.isScrolling;
180 181 182 183 184 185 186 187 188 189 190 191 192 193
  }

  /// 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).
  ///
194 195 196 197 198 199
  /// 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) {
    assert(_pixels != null);
200
    assert(SchedulerBinding.instance.schedulerPhase.index <= SchedulerPhase.transientCallbacks.index);
201
    if (newPixels != pixels) {
202
      final double overscroll = applyBoundaryConditions(newPixels);
203
      assert(() {
204
        final double delta = newPixels - pixels;
205
        if (overscroll.abs() > delta.abs()) {
206
          throw FlutterError(
207 208
            '$runtimeType.applyBoundaryConditions returned invalid overscroll value.\n'
            'setPixels() was called to change the scroll offset from $pixels to $newPixels.\n'
209
            'That is a delta of $delta units.\n'
210
            '$runtimeType.applyBoundaryConditions reported an overscroll of $overscroll units.'
211 212 213
          );
        }
        return true;
214
      }());
215
      final double oldPixels = _pixels;
216
      _pixels = newPixels - overscroll;
217 218
      if (_pixels != oldPixels) {
        notifyListeners();
219
        didUpdateScrollPositionBy(_pixels - oldPixels);
220
      }
221 222 223
      if (overscroll != 0.0) {
        didOverscrollBy(overscroll);
        return overscroll;
224 225 226 227 228
      }
    }
    return 0.0;
  }

229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252
  /// 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.)
253 254 255
  ///
  /// See also:
  ///
256
  ///  * [correctBy], which is a method of [ViewportOffset] used
257 258
  ///    by viewport render objects to correct the offset during layout
  ///    without notifying its listeners.
259
  ///  * [jumpTo], for making changes to position while not in the
260
  ///    middle of layout and applying the new position immediately.
261
  ///  * [animateTo], which is like [jumpTo] but animating to the
262
  ///    destination offset.
263 264 265 266
  void correctPixels(double value) {
    _pixels = value;
  }

267 268 269 270 271 272 273 274 275 276 277 278 279 280 281
  /// 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.
282 283
  @override
  void correctBy(double correction) {
284 285 286 287
    assert(
      _pixels != null,
      'An initial pixels value must exist by caling correctPixels on the ScrollPosition',
    );
288
    _pixels += correction;
289
    _didChangeViewportDimensionOrReceiveCorrection = true;
290 291
  }

292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310
  /// 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].
  ///
311 312 313
  /// 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.
314
  @protected
315
  void forcePixels(double value) {
316
    assert(pixels != null);
317 318
    _pixels = value;
    notifyListeners();
319 320
  }

321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349
  /// 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.
  @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.
350 351 352
  ///
  /// 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.
353 354 355 356 357 358 359 360 361
  @protected
  void restoreScrollOffset() {
    if (pixels == null) {
      final double value = PageStorage.of(context.storageContext)?.readState(context.storageContext);
      if (value != null)
        correctPixels(value);
    }
  }

362 363 364 365 366 367
  /// 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.
368 369 370
  ///
  /// The default implementation defers to the [physics] object's
  /// [ScrollPhysics.applyBoundaryConditions].
371 372 373 374 375 376
  @protected
  double applyBoundaryConditions(double value) {
    final double result = physics.applyBoundaryConditions(this, value);
    assert(() {
      final double delta = value - pixels;
      if (result.abs() > delta.abs()) {
377
        throw FlutterError(
378 379 380 381 382 383 384 385 386 387 388
          '${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 '
          'viewport dimension is $viewportDimension.'
        );
      }
      return true;
389
    }());
390 391
    return result;
  }
392

393
  bool _didChangeViewportDimensionOrReceiveCorrection = true;
394 395

  @override
396
  bool applyViewportDimension(double viewportDimension) {
397 398
    if (_viewportDimension != viewportDimension) {
      _viewportDimension = viewportDimension;
399
      _didChangeViewportDimensionOrReceiveCorrection = true;
400 401 402 403
      // 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.
    }
404
    return true;
405 406
  }

407 408
  Set<SemanticsAction> _semanticActions;

409 410
  /// Called whenever the scroll position or the dimensions of the scroll view
  /// change to schedule an update of the available semantics actions. The
411
  /// actual update will be performed in the next frame. If non is pending
412 413 414 415 416 417 418 419 420
  /// 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.
421 422 423 424 425 426 427 428 429 430 431 432 433 434
  void _updateSemanticActions() {
    SemanticsAction forward;
    SemanticsAction backward;
    switch (axis) {
      case Axis.vertical:
        forward = SemanticsAction.scrollUp;
        backward = SemanticsAction.scrollDown;
        break;
      case Axis.horizontal:
        forward = SemanticsAction.scrollLeft;
        backward = SemanticsAction.scrollRight;
        break;
    }

435
    final Set<SemanticsAction> actions = <SemanticsAction>{};
436 437 438 439 440
    if (pixels > minScrollExtent)
      actions.add(backward);
    if (pixels < maxScrollExtent)
      actions.add(forward);

441
    if (setEquals<SemanticsAction>(actions, _semanticActions))
442 443 444 445 446 447
      return;

    _semanticActions = actions;
    context.setSemanticsActions(_semanticActions);
  }

448 449
  @override
  bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
450 451
    if (!nearEqual(_minScrollExtent, minScrollExtent, Tolerance.defaultTolerance.distance) ||
        !nearEqual(_maxScrollExtent, maxScrollExtent, Tolerance.defaultTolerance.distance) ||
452
        _didChangeViewportDimensionOrReceiveCorrection) {
453 454
      _minScrollExtent = minScrollExtent;
      _maxScrollExtent = maxScrollExtent;
455 456
      _haveDimensions = true;
      applyNewDimensions();
457
      _didChangeViewportDimensionOrReceiveCorrection = false;
458
    }
459
    return true;
460 461
  }

462 463 464 465 466 467 468 469 470 471
  /// 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
472
  /// calling its [ScrollActivity.applyNewDimensions] method.
473 474 475
  ///
  /// See also:
  ///
476 477 478 479 480
  ///  * [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].
481
  @protected
482 483 484 485
  @mustCallSuper
  void applyNewDimensions() {
    assert(pixels != null);
    activity.applyNewDimensions();
486
    _updateSemanticActions(); // will potentially request a semantics update.
487
  }
488

489 490
  /// Animates the position such that the given object is as visible as possible
  /// by just scrolling this position.
491 492
  Future<void> ensureVisible(
    RenderObject object, {
493 494 495
    double alignment = 0.0,
    Duration duration = Duration.zero,
    Curve curve = Curves.ease,
496 497 498 499
  }) {
    assert(object.attached);
    final RenderAbstractViewport viewport = RenderAbstractViewport.of(object);
    assert(viewport != null);
500

501
    final double target = viewport.getOffsetToReveal(object, alignment).offset.clamp(minScrollExtent, maxScrollExtent);
502

503
    if (target == pixels)
504
      return Future<void>.value();
505

506
    if (duration == Duration.zero) {
507
      jumpTo(target);
508
      return Future<void>.value();
509
    }
510

511
    return animateTo(target, duration: duration, curve: curve);
512 513
  }

514 515 516
  /// This notifier's value is true if a scroll is underway and false if the scroll
  /// position is idle.
  ///
517
  /// Listeners added by stateful widgets should be removed in the widget's
518
  /// [State.dispose] method.
519
  final ValueNotifier<bool> isScrollingNotifier = ValueNotifier<bool>(false);
520

521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547
  /// 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].
548
  @override
549 550
  Future<void> animateTo(
    double to, {
551 552
    @required Duration duration,
    @required Curve curve,
553
  });
554

555 556 557 558 559 560 561 562 563
  /// 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.
564
  @override
565
  void jumpTo(double value);
566

567 568 569 570 571 572 573 574
  /// 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
575 576
  Future<void> moveTo(
    double to, {
577 578 579 580 581 582 583 584 585 586 587 588 589
    Duration duration,
    Curve curve,
    bool clamp = true,
  }) {
    assert(to != null);
    assert(clamp != null);

    if (clamp)
      to = to.clamp(minScrollExtent, maxScrollExtent);

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

590 591 592
  @override
  bool get allowImplicitScrolling => physics.allowImplicitScrolling;

593 594 595
  /// Deprecated. Use [jumpTo] or a custom [ScrollPosition] instead.
  @Deprecated('This will lead to bugs.')
  void jumpToWithoutSettling(double value);
596

597
  /// Stop the current activity and start a [HoldScrollActivity].
598
  ScrollHoldController hold(VoidCallback holdCancelCallback);
599

600 601 602 603 604
  /// 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.
605
  Drag drag(DragStartDetails details, VoidCallback dragCancelCallback);
606

607 608 609 610 611 612 613
  /// 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.
614
  @protected
615
  @visibleForTesting
616 617
  ScrollActivity get activity => _activity;
  ScrollActivity _activity;
618

619 620 621 622 623 624
  /// 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
625
  /// explicitly null-check the argument.
626 627 628 629 630 631 632 633
  void beginActivity(ScrollActivity newActivity) {
    if (newActivity == null)
      return;
    bool wasScrolling, oldIgnorePointer;
    if (_activity != null) {
      oldIgnorePointer = _activity.shouldIgnorePointer;
      wasScrolling = _activity.isScrolling;
      if (wasScrolling && !newActivity.isScrolling)
634
        didEndScroll(); // notifies and then saves the scroll offset
635 636 637 638 639 640 641 642
      _activity.dispose();
    } else {
      oldIgnorePointer = false;
      wasScrolling = false;
    }
    _activity = newActivity;
    if (oldIgnorePointer != activity.shouldIgnorePointer)
      context.setIgnorePointer(activity.shouldIgnorePointer);
643
    isScrollingNotifier.value = activity.isScrolling;
644 645 646 647 648 649 650 651 652
    if (!wasScrolling && _activity.isScrolling)
      didStartScroll();
  }


  // NOTIFICATION DISPATCH

  /// Called by [beginActivity] to report when an activity has started.
  void didStartScroll() {
653
    activity.dispatchScrollStartNotification(copyWith(), context.notificationContext);
654 655 656 657
  }

  /// Called by [setPixels] to report a change to the [pixels] position.
  void didUpdateScrollPositionBy(double delta) {
658
    activity.dispatchScrollUpdateNotification(copyWith(), context.notificationContext, delta);
659 660 661
  }

  /// Called by [beginActivity] to report when an activity has ended.
662 663
  ///
  /// This also saves the scroll offset using [saveScrollOffset].
664
  void didEndScroll() {
665
    activity.dispatchScrollEndNotification(copyWith(), context.notificationContext);
666 667
    if (keepScrollOffset)
      saveScrollOffset();
668 669 670 671 672 673 674
  }

  /// 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) {
    assert(activity.isScrolling);
675
    activity.dispatchOverscrollNotification(copyWith(), context.notificationContext, value);
676 677 678 679 680 681
  }

  /// Dispatches a notification that the [userScrollDirection] has changed.
  ///
  /// Subclasses should call this function when they change [userScrollDirection].
  void didUpdateScrollDirection(ScrollDirection direction) {
682
    UserScrollNotification(metrics: copyWith(), context: context.notificationContext, direction: direction).dispatch(context.notificationContext);
683 684 685 686 687 688 689 690 691
  }

  @override
  void dispose() {
    assert(pixels != null);
    activity?.dispose(); // it will be null if it got absorbed by another ScrollPosition
    _activity = null;
    super.dispose();
  }
692

693 694
  @override
  void notifyListeners() {
695
    _updateSemanticActions(); // will potentially request a semantics update.
696 697 698
    super.notifyListeners();
  }

699
  @override
700
  void debugFillDescription(List<String> description) {
701 702
    if (debugLabel != null)
      description.add(debugLabel);
703 704 705
    super.debugFillDescription(description);
    description.add('range: ${minScrollExtent?.toStringAsFixed(1)}..${maxScrollExtent?.toStringAsFixed(1)}');
    description.add('viewport: ${viewportDimension?.toStringAsFixed(1)}');
706 707
  }
}