scroll_activity.dart 21.5 KB
Newer Older
1 2 3 4 5
// 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';
6
import 'dart:math' as math;
7 8 9 10 11 12 13 14 15 16 17 18 19 20

import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/physics.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';

import 'basic.dart';
import 'framework.dart';
import 'gesture_detector.dart';
import 'scroll_metrics.dart';
import 'scroll_notification.dart';
import 'ticker_provider.dart';

21 22
/// A backend for a [ScrollActivity].
///
23
/// Used by subclasses of [ScrollActivity] to manipulate the scroll view that
24 25 26 27 28
/// they are acting upon.
///
/// See also:
///
///  * [ScrollActivity], which uses this class as its delegate.
29
///  * [ScrollPositionWithSingleContext], the main implementation of this interface.
30
abstract class ScrollActivityDelegate {
31
  /// The direction in which the scroll view scrolls.
32 33
  AxisDirection get axisDirection;

34 35 36 37
  /// Update the scroll position to the given pixel value.
  ///
  /// Returns the overscroll, if any. See [ScrollPosition.setPixels] for more
  /// information.
38
  double setPixels(double pixels);
39 40 41 42 43 44 45

  /// Updates the scroll position by the given amount.
  ///
  /// Appropriate for when the user is directly manipulating the scroll
  /// position, for example by dragging the scroll view. Typically applies
  /// [ScrollPhysics.applyPhysicsToUserOffset] and other transformations that
  /// are appropriate for user-driving scrolling.
46
  void applyUserOffset(double delta);
47

48
  /// Terminate the current activity and start an idle activity.
49
  void goIdle();
50 51 52

  /// Terminate the current activity and start a ballistic activity with the
  /// given velocity.
53 54 55 56 57 58 59
  void goBallistic(double velocity);
}

/// Base class for scrolling activities like dragging and flinging.
///
/// See also:
///
60 61
///  * [ScrollPosition], which uses [ScrollActivity] objects to manage the
///    [ScrollPosition] of a [Scrollable].
62
abstract class ScrollActivity {
63
  /// Initializes [delegate] for subclasses.
64 65
  ScrollActivity(this._delegate);

66
  /// The delegate that this activity will use to actuate the scroll view.
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
  ScrollActivityDelegate get delegate => _delegate;
  ScrollActivityDelegate _delegate;

  /// Updates the activity's link to the [ScrollActivityDelegate].
  ///
  /// This should only be called when an activity is being moved from a defunct
  /// (or about-to-be defunct) [ScrollActivityDelegate] object to a new one.
  void updateDelegate(ScrollActivityDelegate value) {
    assert(_delegate != value);
    _delegate = value;
  }

  /// Called by the [ScrollActivityDelegate] when it has changed type (for
  /// example, when changing from an Android-style scroll position to an
  /// iOS-style scroll position). If this activity can differ between the two
  /// modes, then it should tell the position to restart that activity
  /// appropriately.
  ///
  /// For example, [BallisticScrollActivity]'s implementation calls
  /// [ScrollActivityDelegate.goBallistic].
  void resetActivity() { }

89
  /// Dispatch a [ScrollStartNotification] with the given metrics.
90
  void dispatchScrollStartNotification(ScrollMetrics metrics, BuildContext context) {
91
    ScrollStartNotification(metrics: metrics, context: context).dispatch(context);
92 93
  }

94
  /// Dispatch a [ScrollUpdateNotification] with the given metrics and scroll delta.
95
  void dispatchScrollUpdateNotification(ScrollMetrics metrics, BuildContext context, double scrollDelta) {
96
    ScrollUpdateNotification(metrics: metrics, context: context, scrollDelta: scrollDelta).dispatch(context);
97 98
  }

99
  /// Dispatch an [OverscrollNotification] with the given metrics and overscroll.
100
  void dispatchOverscrollNotification(ScrollMetrics metrics, BuildContext context, double overscroll) {
101
    OverscrollNotification(metrics: metrics, context: context, overscroll: overscroll).dispatch(context);
102 103
  }

104
  /// Dispatch a [ScrollEndNotification] with the given metrics and overscroll.
105
  void dispatchScrollEndNotification(ScrollMetrics metrics, BuildContext context) {
106
    ScrollEndNotification(metrics: metrics, context: context).dispatch(context);
107 108
  }

109
  /// Called when the scroll view that is performing this activity changes its metrics.
110 111
  void applyNewDimensions() { }

112 113
  /// Whether the scroll view should ignore pointer events while performing this
  /// activity.
114 115
  bool get shouldIgnorePointer;

116 117 118 119
  /// Whether performing this activity constitutes scrolling.
  ///
  /// Used, for example, to determine whether the user scroll direction is
  /// [ScrollDirection.idle].
120 121
  bool get isScrolling;

122 123 124 125 126
  /// If applicable, the velocity at which the scroll offset is currently
  /// independently changing (i.e. without external stimuli such as a dragging
  /// gestures) in logical pixels per second for this activity.
  double get velocity;

127
  /// Called when the scroll view stops performing this activity.
128 129 130 131 132 133
  @mustCallSuper
  void dispose() {
    _delegate = null;
  }

  @override
134
  String toString() => describeIdentity(this);
135 136
}

137 138 139
/// A scroll activity that does nothing.
///
/// When a scroll view is not scrolling, it is performing the idle activity.
140 141 142
///
/// If the [Scrollable] changes dimensions, this activity triggers a ballistic
/// activity to restore the view.
143
class IdleScrollActivity extends ScrollActivity {
144
  /// Creates a scroll activity that does nothing.
145 146 147 148 149 150 151 152 153 154 155 156
  IdleScrollActivity(ScrollActivityDelegate delegate) : super(delegate);

  @override
  void applyNewDimensions() {
    delegate.goBallistic(0.0);
  }

  @override
  bool get shouldIgnorePointer => false;

  @override
  bool get isScrolling => false;
157 158 159

  @override
  double get velocity => 0.0;
160 161
}

162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198
/// Interface for holding a [Scrollable] stationary.
///
/// An object that implements this interface is returned by
/// [ScrollPosition.hold]. It holds the scrollable stationary until an activity
/// is started or the [cancel] method is called.
abstract class ScrollHoldController {
  /// Release the [Scrollable], potentially letting it go ballistic if
  /// necessary.
  void cancel();
}

/// A scroll activity that does nothing but can be released to resume
/// normal idle behavior.
///
/// This is used while the user is touching the [Scrollable] but before the
/// touch has become a [Drag].
///
/// For the purposes of [ScrollNotification]s, this activity does not constitute
/// scrolling, and does not prevent the user from interacting with the contents
/// of the [Scrollable] (unlike when a drag has begun or there is a scroll
/// animation underway).
class HoldScrollActivity extends ScrollActivity implements ScrollHoldController {
  /// Creates a scroll activity that does nothing.
  HoldScrollActivity({
    @required ScrollActivityDelegate delegate,
    this.onHoldCanceled,
  }) : super(delegate);

  /// Called when [dispose] is called.
  final VoidCallback onHoldCanceled;

  @override
  bool get shouldIgnorePointer => false;

  @override
  bool get isScrolling => false;

199 200 201
  @override
  double get velocity => 0.0;

202 203 204 205 206 207 208 209 210 211 212 213 214
  @override
  void cancel() {
    delegate.goBallistic(0.0);
  }

  @override
  void dispose() {
    if (onHoldCanceled != null)
      onHoldCanceled();
    super.dispose();
  }
}

215 216 217 218 219 220
/// Scrolls a scroll view as the user drags their finger across the screen.
///
/// See also:
///
///  * [DragScrollActivity], which is the activity the scroll view performs
///    while a drag is underway.
221
class ScrollDragController implements Drag {
222 223 224 225 226 227 228
  /// Creates an object that scrolls a scroll view as the user drags their
  /// finger across the screen.
  ///
  /// The [delegate] and `details` arguments must not be null.
  ScrollDragController({
    @required ScrollActivityDelegate delegate,
    @required DragStartDetails details,
229
    this.onDragCanceled,
230
    this.carriedVelocity,
231
    this.motionStartDistanceThreshold,
232 233
  }) : assert(delegate != null),
       assert(details != null),
234 235 236 237
       assert(
         motionStartDistanceThreshold == null || motionStartDistanceThreshold > 0.0,
         'motionStartDistanceThreshold must be a positive number or null'
       ),
238
       _delegate = delegate,
239 240
       _lastDetails = details,
       _retainMomentum = carriedVelocity != null && carriedVelocity != 0.0,
241 242
       _lastNonStationaryTimestamp = details.sourceTimeStamp,
       _offsetSinceLastStop = motionStartDistanceThreshold == null ? null : 0.0;
243

244
  /// The object that will actuate the scroll view as the user drags.
245 246
  ScrollActivityDelegate get delegate => _delegate;
  ScrollActivityDelegate _delegate;
247

248
  /// Called when [dispose] is called.
249
  final VoidCallback onDragCanceled;
250

251 252 253 254
  /// Velocity that was present from a previous [ScrollActivity] when this drag
  /// began.
  final double carriedVelocity;

255 256 257 258
  /// Amount of pixels in either direction the drag has to move by to start
  /// scroll movement again after each time scrolling came to a stop.
  final double motionStartDistanceThreshold;

259 260
  Duration _lastNonStationaryTimestamp;
  bool _retainMomentum;
261 262
  /// Null if already in motion or has no [motionStartDistanceThreshold].
  double _offsetSinceLastStop;
263 264 265 266

  /// Maximum amount of time interval the drag can have consecutive stationary
  /// pointer update events before losing the momentum carried from a previous
  /// scroll activity.
267
  static const Duration momentumRetainStationaryDurationThreshold =
268
      Duration(milliseconds: 20);
269

270 271 272 273
  /// Maximum amount of time interval the drag can have consecutive stationary
  /// pointer update events before needing to break the
  /// [motionStartDistanceThreshold] to start motion again.
  static const Duration motionStoppedDurationThreshold =
274
      Duration(milliseconds: 50);
275

276 277
  /// The drag distance past which, a [motionStartDistanceThreshold] breaking
  /// drag is considered a deliberate fling.
278
  static const double _bigThresholdBreakDistance = 24.0;
279

280 281
  bool get _reversed => axisDirectionIsReversed(delegate.axisDirection);

282 283 284 285 286 287 288 289 290
  /// Updates the controller's link to the [ScrollActivityDelegate].
  ///
  /// This should only be called when a controller is being moved from a defunct
  /// (or about-to-be defunct) [ScrollActivityDelegate] object to a new one.
  void updateDelegate(ScrollActivityDelegate value) {
    assert(_delegate != value);
    _delegate = value;
  }

291 292 293 294 295 296 297 298 299 300 301 302
  /// Determines whether to lose the existing incoming velocity when starting
  /// the drag.
  void _maybeLoseMomentum(double offset, Duration timestamp) {
    if (_retainMomentum &&
        offset == 0.0 &&
        (timestamp == null || // If drag event has no timestamp, we lose momentum.
         timestamp - _lastNonStationaryTimestamp > momentumRetainStationaryDurationThreshold)) {
      // If pointer is stationary for too long, we lose momentum.
      _retainMomentum = false;
    }
  }

303 304 305
  /// If a motion start threshold exists, determine whether the threshold needs
  /// to be broken to scroll. Also possibly apply an offset adjustment when
  /// threshold is first broken.
306
  ///
307 308 309
  /// Returns `0.0` when stationary or within threshold. Returns `offset`
  /// transparently when already in motion.
  double _adjustForScrollStartThreshold(double offset, Duration timestamp) {
310 311 312
    if (timestamp == null) {
      // If we can't track time, we can't apply thresholds.
      // May be null for proxied drags like via accessibility.
313
      return offset;
314 315 316 317 318 319 320 321 322
    }
    if (offset == 0.0) {
      if (motionStartDistanceThreshold != null &&
          _offsetSinceLastStop == null &&
          timestamp - _lastNonStationaryTimestamp > motionStoppedDurationThreshold) {
        // Enforce a new threshold.
        _offsetSinceLastStop = 0.0;
      }
      // Not moving can't break threshold.
323
      return 0.0;
324 325
    } else {
      if (_offsetSinceLastStop == null) {
326 327 328
        // Already in motion or no threshold behavior configured such as for
        // Android. Allow transparent offset transmission.
        return offset;
329 330 331 332 333
      } else {
        _offsetSinceLastStop += offset;
        if (_offsetSinceLastStop.abs() > motionStartDistanceThreshold) {
          // Threshold broken.
          _offsetSinceLastStop = null;
334
          if (offset.abs() > _bigThresholdBreakDistance) {
335 336 337 338 339 340 341 342 343
            // This is heuristically a very deliberate fling. Leave the motion
            // unaffected.
            return offset;
          } else {
            // This is a normal speed threshold break.
            return math.min(
              // Ease into the motion when the threshold is initially broken
              // to avoid a visible jump.
              motionStartDistanceThreshold / 3.0,
344
              offset.abs(),
345 346
            ) * offset.sign;
          }
347
        } else {
348
          return 0.0;
349 350 351 352 353
        }
      }
    }
  }

354 355 356 357 358
  @override
  void update(DragUpdateDetails details) {
    assert(details.primaryDelta != null);
    _lastDetails = details;
    double offset = details.primaryDelta;
359
    if (offset != 0.0) {
360 361
      _lastNonStationaryTimestamp = details.sourceTimeStamp;
    }
362 363 364
    // By default, iOS platforms carries momentum and has a start threshold
    // (configured in [BouncingScrollPhysics]). The 2 operations below are
    // no-ops on Android.
365
    _maybeLoseMomentum(offset, details.sourceTimeStamp);
366 367
    offset = _adjustForScrollStartThreshold(offset, details.sourceTimeStamp);
    if (offset == 0.0) {
368 369
      return;
    }
370 371 372 373 374 375 376 377 378 379 380
    if (_reversed) // e.g. an AxisDirection.up scrollable
      offset = -offset;
    delegate.applyUserOffset(offset);
  }

  @override
  void end(DragEndDetails details) {
    assert(details.primaryVelocity != null);
    // We negate the velocity here because if the touch is moving downwards,
    // the scroll has to move upwards. It's the same reason that update()
    // above negates the delta before applying it to the scroll offset.
381 382 383 384 385 386 387 388 389
    double velocity = -details.primaryVelocity;
    if (_reversed) // e.g. an AxisDirection.up scrollable
      velocity = -velocity;
    _lastDetails = details;

    // Build momentum only if dragging in the same direction.
    if (_retainMomentum && velocity.sign == carriedVelocity.sign)
      velocity += carriedVelocity;
    delegate.goBallistic(velocity);
390 391 392 393 394 395 396
  }

  @override
  void cancel() {
    delegate.goBallistic(0.0);
  }

397
  /// Called by the delegate when it is no longer sending events to this object.
398
  @mustCallSuper
399 400 401 402 403 404
  void dispose() {
    _lastDetails = null;
    if (onDragCanceled != null)
      onDragCanceled();
  }

405 406
  /// The most recently observed [DragStartDetails], [DragUpdateDetails], or
  /// [DragEndDetails] object.
407
  dynamic get lastDetails => _lastDetails;
408
  dynamic _lastDetails;
409 410

  @override
411
  String toString() => describeIdentity(this);
412 413
}

414 415 416 417 418 419 420
/// The activity a scroll view performs when a the user drags their finger
/// across the screen.
///
/// See also:
///
///  * [ScrollDragController], which listens to the [Drag] and actually scrolls
///    the scroll view.
421
class DragScrollActivity extends ScrollActivity {
422 423
  /// Creates an activity for when the user drags their finger across the
  /// screen.
424 425 426
  DragScrollActivity(
    ScrollActivityDelegate delegate,
    ScrollDragController controller,
427 428
  ) : _controller = controller,
      super(delegate);
429 430 431

  ScrollDragController _controller;

432 433
  @override
  void dispatchScrollStartNotification(ScrollMetrics metrics, BuildContext context) {
434 435
    final dynamic lastDetails = _controller.lastDetails;
    assert(lastDetails is DragStartDetails);
436
    ScrollStartNotification(metrics: metrics, context: context, dragDetails: lastDetails).dispatch(context);
437 438 439 440
  }

  @override
  void dispatchScrollUpdateNotification(ScrollMetrics metrics, BuildContext context, double scrollDelta) {
441 442
    final dynamic lastDetails = _controller.lastDetails;
    assert(lastDetails is DragUpdateDetails);
443
    ScrollUpdateNotification(metrics: metrics, context: context, scrollDelta: scrollDelta, dragDetails: lastDetails).dispatch(context);
444 445 446 447
  }

  @override
  void dispatchOverscrollNotification(ScrollMetrics metrics, BuildContext context, double overscroll) {
448 449
    final dynamic lastDetails = _controller.lastDetails;
    assert(lastDetails is DragUpdateDetails);
450
    OverscrollNotification(metrics: metrics, context: context, overscroll: overscroll, dragDetails: lastDetails).dispatch(context);
451 452 453 454 455
  }

  @override
  void dispatchScrollEndNotification(ScrollMetrics metrics, BuildContext context) {
    // We might not have DragEndDetails yet if we're being called from beginActivity.
456
    final dynamic lastDetails = _controller.lastDetails;
457
    ScrollEndNotification(
458 459
      metrics: metrics,
      context: context,
460
      dragDetails: lastDetails is DragEndDetails ? lastDetails : null,
461 462 463 464 465 466 467 468
    ).dispatch(context);
  }

  @override
  bool get shouldIgnorePointer => true;

  @override
  bool get isScrolling => true;
469

470 471 472 473 474
  // DragScrollActivity is not independently changing velocity yet
  // until the drag is ended.
  @override
  double get velocity => 0.0;

475 476 477 478 479
  @override
  void dispose() {
    _controller = null;
    super.dispose();
  }
480 481 482

  @override
  String toString() {
483
    return '${describeIdentity(this)}($_controller)';
484
  }
485 486
}

487
/// An activity that animates a scroll view based on a physics [Simulation].
488 489 490 491 492 493
///
/// A [BallisticScrollActivity] is typically used when the user lifts their
/// finger off the screen to continue the scrolling gesture with the current velocity.
///
/// [BallisticScrollActivity] is also used to restore a scroll view to a valid
/// scroll offset when the geometry of the scroll view changes. In these
Ian Hickson's avatar
Ian Hickson committed
494
/// situations, the [Simulation] typically starts with a zero velocity.
495 496 497 498 499
///
/// See also:
///
///  * [DrivenScrollActivity], which animates a scroll view based on a set of
///    animation parameters.
500
class BallisticScrollActivity extends ScrollActivity {
501 502 503
  /// Creates an activity that animates a scroll view based on a [simulation].
  ///
  /// The [delegate], [simulation], and [vsync] arguments must not be null.
504 505 506 507 508
  BallisticScrollActivity(
    ScrollActivityDelegate delegate,
    Simulation simulation,
    TickerProvider vsync,
  ) : super(delegate) {
509
    _controller = AnimationController.unbounded(
510
      debugLabel: kDebugMode ? '$runtimeType' : null,
511 512 513 514 515 516 517
      vsync: vsync,
    )
      ..addListener(_tick)
      ..animateWith(simulation)
       .whenComplete(_end); // won't trigger if we dispose _controller first
  }

518
  @override
519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539
  double get velocity => _controller.velocity;

  AnimationController _controller;

  @override
  void resetActivity() {
    delegate.goBallistic(velocity);
  }

  @override
  void applyNewDimensions() {
    delegate.goBallistic(velocity);
  }

  void _tick() {
    if (!applyMoveTo(_controller.value))
      delegate.goIdle();
  }

  /// Move the position to the given location.
  ///
540 541
  /// If the new position was fully applied, returns true. If there was any
  /// overflow, returns false.
542 543 544 545 546 547 548 549 550 551 552 553 554 555
  ///
  /// The default implementation calls [ScrollActivityDelegate.setPixels]
  /// and returns true if the overflow was zero.
  @protected
  bool applyMoveTo(double value) {
    return delegate.setPixels(value) == 0.0;
  }

  void _end() {
    delegate?.goBallistic(0.0);
  }

  @override
  void dispatchOverscrollNotification(ScrollMetrics metrics, BuildContext context, double overscroll) {
556
    OverscrollNotification(metrics: metrics, context: context, overscroll: overscroll, velocity: velocity).dispatch(context);
557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572
  }

  @override
  bool get shouldIgnorePointer => true;

  @override
  bool get isScrolling => true;

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  String toString() {
573
    return '${describeIdentity(this)}($_controller)';
574 575 576
  }
}

577 578 579 580 581 582 583 584 585
/// An activity that animates a scroll view based on animation parameters.
///
/// For example, a [DrivenScrollActivity] is used to implement
/// [ScrollController.animateTo].
///
/// See also:
///
///  * [BallisticScrollActivity], which animates a scroll view based on a
///    physics [Simulation].
586
class DrivenScrollActivity extends ScrollActivity {
587 588 589 590
  /// Creates an activity that animates a scroll view based on animation
  /// parameters.
  ///
  /// All of the parameters must be non-null.
591 592 593 594 595 596 597
  DrivenScrollActivity(
    ScrollActivityDelegate delegate, {
    @required double from,
    @required double to,
    @required Duration duration,
    @required Curve curve,
    @required TickerProvider vsync,
598 599 600
  }) : assert(from != null),
       assert(to != null),
       assert(duration != null),
601
       assert(duration > Duration.zero),
602 603
       assert(curve != null),
       super(delegate) {
604
    _completer = Completer<void>();
605
    _controller = AnimationController.unbounded(
606 607 608 609 610 611 612 613 614
      value: from,
      debugLabel: '$runtimeType',
      vsync: vsync,
    )
      ..addListener(_tick)
      ..animateTo(to, duration: duration, curve: curve)
       .whenComplete(_end); // won't trigger if we dispose _controller first
  }

615
  Completer<void> _completer;
616 617
  AnimationController _controller;

618 619 620 621 622
  /// A [Future] that completes when the activity stops.
  ///
  /// For example, this [Future] will complete if the animation reaches the end
  /// or if the user interacts with the scroll view in way that causes the
  /// animation to stop before it reaches the end.
623
  Future<void> get done => _completer.future;
624

625
  @override
626 627 628 629 630 631 632 633 634 635 636 637 638
  double get velocity => _controller.velocity;

  void _tick() {
    if (delegate.setPixels(_controller.value) != 0.0)
      delegate.goIdle();
  }

  void _end() {
    delegate?.goBallistic(velocity);
  }

  @override
  void dispatchOverscrollNotification(ScrollMetrics metrics, BuildContext context, double overscroll) {
639
    OverscrollNotification(metrics: metrics, context: context, overscroll: overscroll, velocity: velocity).dispatch(context);
640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656
  }

  @override
  bool get shouldIgnorePointer => true;

  @override
  bool get isScrolling => true;

  @override
  void dispose() {
    _completer.complete();
    _controller.dispose();
    super.dispose();
  }

  @override
  String toString() {
657
    return '${describeIdentity(this)}($_controller)';
658 659
  }
}