1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/// @docImport 'package:flutter/rendering.dart';
6///
7/// @docImport 'scroll_controller.dart';
8/// @docImport 'scroll_physics.dart';
9/// @docImport 'scroll_position.dart';
10/// @docImport 'scroll_position_with_single_context.dart';
11/// @docImport 'scrollable.dart';
12library;
13
14import 'dart:async';
15import 'dart:math' as math;
16
17import 'package:flutter/foundation.dart';
18import 'package:flutter/gestures.dart';
19import 'package:flutter/rendering.dart';
20
21import 'basic.dart';
22import 'framework.dart';
23import 'scroll_metrics.dart';
24import 'scroll_notification.dart';
25
26/// A backend for a [ScrollActivity].
27///
28/// Used by subclasses of [ScrollActivity] to manipulate the scroll view that
29/// they are acting upon.
30///
31/// See also:
32///
33/// * [ScrollActivity], which uses this class as its delegate.
34/// * [ScrollPositionWithSingleContext], the main implementation of this interface.
35abstract class ScrollActivityDelegate {
36 /// The direction in which the scroll view scrolls.
37 AxisDirection get axisDirection;
38
39 /// Update the scroll position to the given pixel value.
40 ///
41 /// Returns the overscroll, if any. See [ScrollPosition.setPixels] for more
42 /// information.
43 double setPixels(double pixels);
44
45 /// Updates the scroll position by the given amount.
46 ///
47 /// Appropriate for when the user is directly manipulating the scroll
48 /// position, for example by dragging the scroll view. Typically applies
49 /// [ScrollPhysics.applyPhysicsToUserOffset] and other transformations that
50 /// are appropriate for user-driving scrolling.
51 void applyUserOffset(double delta);
52
53 /// Terminate the current activity and start an idle activity.
54 void goIdle();
55
56 /// Terminate the current activity and start a ballistic activity with the
57 /// given velocity.
58 void goBallistic(double velocity);
59}
60
61/// Base class for scrolling activities like dragging and flinging.
62///
63/// See also:
64///
65/// * [ScrollPosition], which uses [ScrollActivity] objects to manage the
66/// [ScrollPosition] of a [Scrollable].
67abstract class ScrollActivity {
68 /// Initializes [delegate] for subclasses.
69 ScrollActivity(this._delegate) {
70 assert(debugMaybeDispatchCreated('widgets', 'ScrollActivity', this));
71 }
72
73 /// The delegate that this activity will use to actuate the scroll view.
74 ScrollActivityDelegate get delegate => _delegate;
75 ScrollActivityDelegate _delegate;
76
77 bool _isDisposed = false;
78
79 /// Updates the activity's link to the [ScrollActivityDelegate].
80 ///
81 /// This should only be called when an activity is being moved from a defunct
82 /// (or about-to-be defunct) [ScrollActivityDelegate] object to a new one.
83 void updateDelegate(ScrollActivityDelegate value) {
84 assert(_delegate != value);
85 _delegate = value;
86 }
87
88 /// Called by the [ScrollActivityDelegate] when it has changed type (for
89 /// example, when changing from an Android-style scroll position to an
90 /// iOS-style scroll position). If this activity can differ between the two
91 /// modes, then it should tell the position to restart that activity
92 /// appropriately.
93 ///
94 /// For example, [BallisticScrollActivity]'s implementation calls
95 /// [ScrollActivityDelegate.goBallistic].
96 void resetActivity() {}
97
98 /// Dispatch a [ScrollStartNotification] with the given metrics.
99 void dispatchScrollStartNotification(ScrollMetrics metrics, BuildContext? context) {
100 ScrollStartNotification(metrics: metrics, context: context).dispatch(context);
101 }
102
103 /// Dispatch a [ScrollUpdateNotification] with the given metrics and scroll delta.
104 void dispatchScrollUpdateNotification(
105 ScrollMetrics metrics,
106 BuildContext context,
107 double scrollDelta,
108 ) {
109 ScrollUpdateNotification(
110 metrics: metrics,
111 context: context,
112 scrollDelta: scrollDelta,
113 ).dispatch(context);
114 }
115
116 /// Dispatch an [OverscrollNotification] with the given metrics and overscroll.
117 void dispatchOverscrollNotification(
118 ScrollMetrics metrics,
119 BuildContext context,
120 double overscroll,
121 ) {
122 OverscrollNotification(
123 metrics: metrics,
124 context: context,
125 overscroll: overscroll,
126 ).dispatch(context);
127 }
128
129 /// Dispatch a [ScrollEndNotification] with the given metrics and overscroll.
130 void dispatchScrollEndNotification(ScrollMetrics metrics, BuildContext context) {
131 ScrollEndNotification(metrics: metrics, context: context).dispatch(context);
132 }
133
134 /// Called when the scroll view that is performing this activity changes its metrics.
135 void applyNewDimensions() {}
136
137 /// Whether the scroll view should ignore pointer events while performing this
138 /// activity.
139 ///
140 /// See also:
141 ///
142 /// * [isScrolling], which describes whether the activity is considered
143 /// to represent user interaction or not.
144 bool get shouldIgnorePointer;
145
146 /// Whether performing this activity constitutes scrolling.
147 ///
148 /// Used, for example, to determine whether the user scroll
149 /// direction (see [ScrollPosition.userScrollDirection]) is
150 /// [ScrollDirection.idle].
151 ///
152 /// See also:
153 ///
154 /// * [shouldIgnorePointer], which controls whether pointer events
155 /// are allowed while the activity is live.
156 /// * [UserScrollNotification], which exposes this status.
157 bool get isScrolling;
158
159 /// If applicable, the velocity at which the scroll offset is currently
160 /// independently changing (i.e. without external stimuli such as a dragging
161 /// gestures) in logical pixels per second for this activity.
162 double get velocity;
163
164 /// Called when the scroll view stops performing this activity.
165 @mustCallSuper
166 void dispose() {
167 assert(debugMaybeDispatchDisposed(this));
168 _isDisposed = true;
169 }
170
171 @override
172 String toString() => describeIdentity(this);
173}
174
175/// A scroll activity that does nothing.
176///
177/// When a scroll view is not scrolling, it is performing the idle activity.
178///
179/// If the [Scrollable] changes dimensions, this activity triggers a ballistic
180/// activity to restore the view.
181class IdleScrollActivity extends ScrollActivity {
182 /// Creates a scroll activity that does nothing.
183 IdleScrollActivity(super.delegate);
184
185 @override
186 void applyNewDimensions() {
187 delegate.goBallistic(0.0);
188 }
189
190 @override
191 bool get shouldIgnorePointer => false;
192
193 @override
194 bool get isScrolling => false;
195
196 @override
197 double get velocity => 0.0;
198}
199
200/// Interface for holding a [Scrollable] stationary.
201///
202/// An object that implements this interface is returned by
203/// [ScrollPosition.hold]. It holds the scrollable stationary until an activity
204/// is started or the [cancel] method is called.
205abstract class ScrollHoldController {
206 /// Release the [Scrollable], potentially letting it go ballistic if
207 /// necessary.
208 void cancel();
209}
210
211/// A scroll activity that does nothing but can be released to resume
212/// normal idle behavior.
213///
214/// This is used while the user is touching the [Scrollable] but before the
215/// touch has become a [Drag].
216///
217/// For the purposes of [ScrollNotification]s, this activity does not constitute
218/// scrolling, and does not prevent the user from interacting with the contents
219/// of the [Scrollable] (unlike when a drag has begun or there is a scroll
220/// animation underway).
221class HoldScrollActivity extends ScrollActivity implements ScrollHoldController {
222 /// Creates a scroll activity that does nothing.
223 HoldScrollActivity({required ScrollActivityDelegate delegate, this.onHoldCanceled})
224 : super(delegate);
225
226 /// Called when [dispose] is called.
227 final VoidCallback? onHoldCanceled;
228
229 @override
230 bool get shouldIgnorePointer => false;
231
232 @override
233 bool get isScrolling => false;
234
235 @override
236 double get velocity => 0.0;
237
238 @override
239 void cancel() {
240 delegate.goBallistic(0.0);
241 }
242
243 @override
244 void dispose() {
245 onHoldCanceled?.call();
246 super.dispose();
247 }
248}
249
250/// Scrolls a scroll view as the user drags their finger across the screen.
251///
252/// See also:
253///
254/// * [DragScrollActivity], which is the activity the scroll view performs
255/// while a drag is underway.
256class ScrollDragController implements Drag {
257 /// Creates an object that scrolls a scroll view as the user drags their
258 /// finger across the screen.
259 ScrollDragController({
260 required ScrollActivityDelegate delegate,
261 required DragStartDetails details,
262 this.onDragCanceled,
263 this.carriedVelocity,
264 this.motionStartDistanceThreshold,
265 }) : assert(
266 motionStartDistanceThreshold == null || motionStartDistanceThreshold > 0.0,
267 'motionStartDistanceThreshold must be a positive number or null',
268 ),
269 _delegate = delegate,
270 _lastDetails = details,
271 _retainMomentum = carriedVelocity != null && carriedVelocity != 0.0,
272 _lastNonStationaryTimestamp = details.sourceTimeStamp,
273 _kind = details.kind,
274 _offsetSinceLastStop = motionStartDistanceThreshold == null ? null : 0.0 {
275 assert(debugMaybeDispatchCreated('widgets', 'ScrollDragController', this));
276 }
277
278 /// The object that will actuate the scroll view as the user drags.
279 ScrollActivityDelegate get delegate => _delegate;
280 ScrollActivityDelegate _delegate;
281
282 /// Called when [dispose] is called.
283 final VoidCallback? onDragCanceled;
284
285 /// Velocity that was present from a previous [ScrollActivity] when this drag
286 /// began.
287 final double? carriedVelocity;
288
289 /// Amount of pixels in either direction the drag has to move by to start
290 /// scroll movement again after each time scrolling came to a stop.
291 final double? motionStartDistanceThreshold;
292
293 Duration? _lastNonStationaryTimestamp;
294 bool _retainMomentum;
295
296 /// Null if already in motion or has no [motionStartDistanceThreshold].
297 double? _offsetSinceLastStop;
298
299 /// Maximum amount of time interval the drag can have consecutive stationary
300 /// pointer update events before losing the momentum carried from a previous
301 /// scroll activity.
302 static const Duration momentumRetainStationaryDurationThreshold = Duration(milliseconds: 20);
303
304 /// The minimum amount of velocity needed to apply the [carriedVelocity] at
305 /// the end of a drag. Expressed as a factor. For example with a
306 /// [carriedVelocity] of 2000, we will need a velocity of at least 1000 to
307 /// apply the [carriedVelocity] as well. If the velocity does not meet the
308 /// threshold, the [carriedVelocity] is lost. Decided by fair eyeballing
309 /// with the scroll_overlay platform test.
310 static const double momentumRetainVelocityThresholdFactor = 0.5;
311
312 /// Maximum amount of time interval the drag can have consecutive stationary
313 /// pointer update events before needing to break the
314 /// [motionStartDistanceThreshold] to start motion again.
315 static const Duration motionStoppedDurationThreshold = Duration(milliseconds: 50);
316
317 /// The drag distance past which, a [motionStartDistanceThreshold] breaking
318 /// drag is considered a deliberate fling.
319 static const double _bigThresholdBreakDistance = 24.0;
320
321 bool get _reversed => axisDirectionIsReversed(delegate.axisDirection);
322
323 /// Updates the controller's link to the [ScrollActivityDelegate].
324 ///
325 /// This should only be called when a controller is being moved from a defunct
326 /// (or about-to-be defunct) [ScrollActivityDelegate] object to a new one.
327 void updateDelegate(ScrollActivityDelegate value) {
328 assert(_delegate != value);
329 _delegate = value;
330 }
331
332 /// Determines whether to lose the existing incoming velocity when starting
333 /// the drag.
334 void _maybeLoseMomentum(double offset, Duration? timestamp) {
335 if (_retainMomentum &&
336 offset == 0.0 &&
337 (timestamp == null || // If drag event has no timestamp, we lose momentum.
338 timestamp - _lastNonStationaryTimestamp! > momentumRetainStationaryDurationThreshold)) {
339 // If pointer is stationary for too long, we lose momentum.
340 _retainMomentum = false;
341 }
342 }
343
344 /// If a motion start threshold exists, determine whether the threshold needs
345 /// to be broken to scroll. Also possibly apply an offset adjustment when
346 /// threshold is first broken.
347 ///
348 /// Returns `0.0` when stationary or within threshold. Returns `offset`
349 /// transparently when already in motion.
350 double _adjustForScrollStartThreshold(double offset, Duration? timestamp) {
351 if (timestamp == null) {
352 // If we can't track time, we can't apply thresholds.
353 // May be null for proxied drags like via accessibility.
354 return offset;
355 }
356 if (offset == 0.0) {
357 if (motionStartDistanceThreshold != null &&
358 _offsetSinceLastStop == null &&
359 timestamp - _lastNonStationaryTimestamp! > motionStoppedDurationThreshold) {
360 // Enforce a new threshold.
361 _offsetSinceLastStop = 0.0;
362 }
363 // Not moving can't break threshold.
364 return 0.0;
365 } else {
366 if (_offsetSinceLastStop == null) {
367 // Already in motion or no threshold behavior configured such as for
368 // Android. Allow transparent offset transmission.
369 return offset;
370 } else {
371 _offsetSinceLastStop = _offsetSinceLastStop! + offset;
372 if (_offsetSinceLastStop!.abs() > motionStartDistanceThreshold!) {
373 // Threshold broken.
374 _offsetSinceLastStop = null;
375 if (offset.abs() > _bigThresholdBreakDistance) {
376 // This is heuristically a very deliberate fling. Leave the motion
377 // unaffected.
378 return offset;
379 } else {
380 // This is a normal speed threshold break.
381 return math.min(
382 // Ease into the motion when the threshold is initially broken
383 // to avoid a visible jump.
384 motionStartDistanceThreshold! / 3.0,
385 offset.abs(),
386 ) *
387 offset.sign;
388 }
389 } else {
390 return 0.0;
391 }
392 }
393 }
394 }
395
396 @override
397 void update(DragUpdateDetails details) {
398 assert(details.primaryDelta != null);
399 _lastDetails = details;
400 double offset = details.primaryDelta!;
401 if (offset != 0.0) {
402 _lastNonStationaryTimestamp = details.sourceTimeStamp;
403 }
404 // By default, iOS platforms carries momentum and has a start threshold
405 // (configured in [BouncingScrollPhysics]). The 2 operations below are
406 // no-ops on Android.
407 _maybeLoseMomentum(offset, details.sourceTimeStamp);
408 offset = _adjustForScrollStartThreshold(offset, details.sourceTimeStamp);
409 if (offset == 0.0) {
410 return;
411 }
412 if (_reversed) {
413 offset = -offset;
414 }
415 delegate.applyUserOffset(offset);
416 }
417
418 @override
419 void end(DragEndDetails details) {
420 assert(details.primaryVelocity != null);
421 // We negate the velocity here because if the touch is moving downwards,
422 // the scroll has to move upwards. It's the same reason that update()
423 // above negates the delta before applying it to the scroll offset.
424 double velocity = -details.primaryVelocity!;
425 if (_reversed) {
426 velocity = -velocity;
427 }
428 _lastDetails = details;
429
430 if (_retainMomentum) {
431 // Build momentum only if dragging in the same direction.
432 final bool isFlingingInSameDirection = velocity.sign == carriedVelocity!.sign;
433 // Build momentum only if the velocity of the last drag was not
434 // substantially lower than the carried momentum.
435 final bool isVelocityNotSubstantiallyLessThanCarriedMomentum =
436 velocity.abs() > carriedVelocity!.abs() * momentumRetainVelocityThresholdFactor;
437 if (isFlingingInSameDirection && isVelocityNotSubstantiallyLessThanCarriedMomentum) {
438 velocity += carriedVelocity!;
439 }
440 }
441 delegate.goBallistic(velocity);
442 }
443
444 @override
445 void cancel() {
446 delegate.goBallistic(0.0);
447 }
448
449 /// Called by the delegate when it is no longer sending events to this object.
450 @mustCallSuper
451 void dispose() {
452 assert(debugMaybeDispatchDisposed(this));
453 _lastDetails = null;
454 onDragCanceled?.call();
455 }
456
457 /// The type of input device driving the drag.
458 final PointerDeviceKind? _kind;
459
460 /// The most recently observed [DragStartDetails], [DragUpdateDetails], or
461 /// [DragEndDetails] object.
462 dynamic get lastDetails => _lastDetails;
463 dynamic _lastDetails;
464
465 @override
466 String toString() => describeIdentity(this);
467}
468
469/// The activity a scroll view performs when the user drags their finger
470/// across the screen.
471///
472/// See also:
473///
474/// * [ScrollDragController], which listens to the [Drag] and actually scrolls
475/// the scroll view.
476class DragScrollActivity extends ScrollActivity {
477 /// Creates an activity for when the user drags their finger across the
478 /// screen.
479 DragScrollActivity(super.delegate, ScrollDragController controller) : _controller = controller;
480
481 ScrollDragController? _controller;
482
483 @override
484 void dispatchScrollStartNotification(ScrollMetrics metrics, BuildContext? context) {
485 final dynamic lastDetails = _controller!.lastDetails;
486 assert(lastDetails is DragStartDetails);
487 ScrollStartNotification(
488 metrics: metrics,
489 context: context,
490 dragDetails: lastDetails as DragStartDetails,
491 ).dispatch(context);
492 }
493
494 @override
495 void dispatchScrollUpdateNotification(
496 ScrollMetrics metrics,
497 BuildContext context,
498 double scrollDelta,
499 ) {
500 final dynamic lastDetails = _controller!.lastDetails;
501 assert(lastDetails is DragUpdateDetails);
502 ScrollUpdateNotification(
503 metrics: metrics,
504 context: context,
505 scrollDelta: scrollDelta,
506 dragDetails: lastDetails as DragUpdateDetails,
507 ).dispatch(context);
508 }
509
510 @override
511 void dispatchOverscrollNotification(
512 ScrollMetrics metrics,
513 BuildContext context,
514 double overscroll,
515 ) {
516 final dynamic lastDetails = _controller!.lastDetails;
517 assert(lastDetails is DragUpdateDetails);
518 OverscrollNotification(
519 metrics: metrics,
520 context: context,
521 overscroll: overscroll,
522 dragDetails: lastDetails as DragUpdateDetails,
523 ).dispatch(context);
524 }
525
526 @override
527 void dispatchScrollEndNotification(ScrollMetrics metrics, BuildContext context) {
528 // We might not have DragEndDetails yet if we're being called from beginActivity.
529 final dynamic lastDetails = _controller!.lastDetails;
530 ScrollEndNotification(
531 metrics: metrics,
532 context: context,
533 dragDetails: lastDetails is DragEndDetails ? lastDetails : null,
534 ).dispatch(context);
535 }
536
537 @override
538 bool get shouldIgnorePointer => _controller?._kind != PointerDeviceKind.trackpad;
539
540 @override
541 bool get isScrolling => true;
542
543 // DragScrollActivity is not independently changing velocity yet
544 // until the drag is ended.
545 @override
546 double get velocity => 0.0;
547
548 @override
549 void dispose() {
550 _controller = null;
551 super.dispose();
552 }
553
554 @override
555 String toString() {
556 return '${describeIdentity(this)}($_controller)';
557 }
558}
559
560/// The activity a scroll view performs after being set into motion.
561///
562/// For example, a [BallisticScrollActivity] is used when the user
563/// lifts their finger off the screen after a [DragScrollActivity],
564/// to continue the scrolling motion starting from the current velocity.
565///
566/// [BallisticScrollActivity] is also used to restore a scroll view to a valid
567/// scroll offset when the geometry of the scroll view changes. In these
568/// situations, the [Simulation] typically starts with a zero velocity.
569///
570/// The scrolling will be driven by the given [Simulation]. If a
571/// [BallisticScrollActivity] is in progress when the scroll metrics change,
572/// then the activity will be replaced with a new ballistic activity starting
573/// from the current velocity (see [ScrollPhysics.createBallisticSimulation]).
574/// To ensure the user perceives smooth motion across such a change,
575/// the simulation should typically be the result
576/// of [ScrollPhysics.createBallisticSimulation]
577/// for the scroll physics of the scroll view.
578///
579/// See also:
580///
581/// * [DrivenScrollActivity], which drives a scroll view through
582/// a given animation, without resetting to a ballistic simulation
583/// when scroll metrics change.
584class BallisticScrollActivity extends ScrollActivity {
585 /// Creates an activity that sets into motion a scroll view.
586 ///
587 /// The simulation should typically be the result
588 /// of [ScrollPhysics.createBallisticSimulation]
589 /// for the scroll physics of the scroll view.
590 BallisticScrollActivity(
591 super.delegate,
592 Simulation simulation,
593 TickerProvider vsync,
594 this.shouldIgnorePointer,
595 ) {
596 _controller =
597 AnimationController.unbounded(
598 debugLabel: kDebugMode ? objectRuntimeType(this, 'BallisticScrollActivity') : null,
599 vsync: vsync,
600 )
601 ..addListener(_tick)
602 ..animateWith(
603 simulation,
604 ).whenComplete(_end); // won't trigger if we dispose _controller before it completes.
605 }
606
607 late AnimationController _controller;
608
609 @override
610 void resetActivity() {
611 delegate.goBallistic(velocity);
612 }
613
614 @override
615 void applyNewDimensions() {
616 delegate.goBallistic(velocity);
617 }
618
619 void _tick() {
620 if (!applyMoveTo(_controller.value)) {
621 delegate.goIdle();
622 }
623 }
624
625 /// Move the position to the given location.
626 ///
627 /// If the new position was fully applied, returns true. If there was any
628 /// overflow, returns false.
629 ///
630 /// The default implementation calls [ScrollActivityDelegate.setPixels]
631 /// and returns true if the overflow was zero.
632 @protected
633 bool applyMoveTo(double value) {
634 return delegate.setPixels(value).abs() < precisionErrorTolerance;
635 }
636
637 void _end() {
638 // Check if the activity was disposed before going ballistic because _end might be called
639 // if _controller is disposed just after completion.
640 if (!_isDisposed) {
641 delegate.goBallistic(0.0);
642 }
643 }
644
645 @override
646 void dispatchOverscrollNotification(
647 ScrollMetrics metrics,
648 BuildContext context,
649 double overscroll,
650 ) {
651 OverscrollNotification(
652 metrics: metrics,
653 context: context,
654 overscroll: overscroll,
655 velocity: velocity,
656 ).dispatch(context);
657 }
658
659 @override
660 final bool shouldIgnorePointer;
661
662 @override
663 bool get isScrolling => true;
664
665 @override
666 double get velocity => _controller.velocity;
667
668 @override
669 void dispose() {
670 _controller.dispose();
671 super.dispose();
672 }
673
674 @override
675 String toString() {
676 return '${describeIdentity(this)}($_controller)';
677 }
678}
679
680/// An activity that drives a scroll view through a given animation.
681///
682/// For example, a [DrivenScrollActivity] is used to implement
683/// [ScrollController.animateTo].
684///
685/// The scrolling will be driven by the given animation parameters
686/// or the given [Simulation].
687///
688/// Unlike a [BallisticScrollActivity], if a [DrivenScrollActivity] is
689/// in progress when the scroll metrics change, the activity will continue
690/// with its original animation.
691///
692/// See also:
693///
694/// * [BallisticScrollActivity], which sets into motion a scroll view.
695class DrivenScrollActivity extends ScrollActivity {
696 /// Creates an activity that drives a scroll view through an animation
697 /// given by animation parameters.
698 DrivenScrollActivity(
699 super.delegate, {
700 required double from,
701 required double to,
702 required Duration duration,
703 required Curve curve,
704 required TickerProvider vsync,
705 }) : assert(duration > Duration.zero) {
706 _completer = Completer<void>();
707 _controller =
708 AnimationController.unbounded(
709 value: from,
710 debugLabel: objectRuntimeType(this, 'DrivenScrollActivity'),
711 vsync: vsync,
712 )
713 ..addListener(_tick)
714 ..animateTo(
715 to,
716 duration: duration,
717 curve: curve,
718 ).whenComplete(_end); // won't trigger if we dispose _controller before it completes.
719 }
720
721 /// Creates an activity that drives a scroll view through an animation
722 /// given by a [Simulation].
723 DrivenScrollActivity.simulation(
724 super.delegate,
725 Simulation simulation, {
726 required TickerProvider vsync,
727 }) {
728 _completer = Completer<void>();
729 _controller =
730 AnimationController.unbounded(
731 debugLabel: objectRuntimeType(this, 'DrivenScrollActivity'),
732 vsync: vsync,
733 )
734 ..addListener(_tick)
735 ..animateWith(
736 simulation,
737 ).whenComplete(_end); // won't trigger if we dispose _controller before it completes.
738 }
739
740 late final Completer<void> _completer;
741 late final AnimationController _controller;
742
743 /// A [Future] that completes when the activity stops.
744 ///
745 /// For example, this [Future] will complete if the animation reaches the end
746 /// or if the user interacts with the scroll view in way that causes the
747 /// animation to stop before it reaches the end.
748 Future<void> get done => _completer.future;
749
750 void _tick() {
751 if (!applyMoveTo(_controller.value)) {
752 delegate.goIdle();
753 }
754 }
755
756 /// Move the position to the given location.
757 ///
758 /// If the new position was fully applied, returns true. If there was any
759 /// overflow, returns false.
760 ///
761 /// The default implementation calls [ScrollActivityDelegate.setPixels]
762 /// and returns true if the overflow was zero.
763 @protected
764 bool applyMoveTo(double value) {
765 return delegate.setPixels(value).abs() < precisionErrorTolerance;
766 }
767
768 void _end() {
769 // Check if the activity was disposed before going ballistic because _end might be called
770 // if _controller is disposed just after completion.
771 if (!_isDisposed) {
772 delegate.goBallistic(velocity);
773 }
774 }
775
776 @override
777 void dispatchOverscrollNotification(
778 ScrollMetrics metrics,
779 BuildContext context,
780 double overscroll,
781 ) {
782 OverscrollNotification(
783 metrics: metrics,
784 context: context,
785 overscroll: overscroll,
786 velocity: velocity,
787 ).dispatch(context);
788 }
789
790 @override
791 bool get shouldIgnorePointer => true;
792
793 @override
794 bool get isScrolling => true;
795
796 @override
797 double get velocity => _controller.velocity;
798
799 @override
800 void dispose() {
801 _completer.complete();
802 _controller.dispose();
803 super.dispose();
804 }
805
806 @override
807 String toString() {
808 return '${describeIdentity(this)}($_controller)';
809 }
810}
811