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