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'; |
12 | library; |
13 | |
14 | import 'dart:async'; |
15 | import 'dart:math' as math; |
16 | |
17 | import 'package:flutter/foundation.dart'; |
18 | import 'package:flutter/gestures.dart'; |
19 | import 'package:flutter/rendering.dart'; |
20 | |
21 | import 'basic.dart'; |
22 | import 'framework.dart'; |
23 | import 'scroll_metrics.dart'; |
24 | import '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. |
35 | abstract 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]. |
67 | abstract 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. |
181 | class 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. |
205 | abstract 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). |
221 | class 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. |
256 | class 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. |
476 | class 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 | /// An activity that animates a scroll view based on a physics [Simulation]. |
561 | /// |
562 | /// A [BallisticScrollActivity] is typically used when the user lifts their |
563 | /// finger off the screen to continue the scrolling gesture with the current velocity. |
564 | /// |
565 | /// [BallisticScrollActivity] is also used to restore a scroll view to a valid |
566 | /// scroll offset when the geometry of the scroll view changes. In these |
567 | /// situations, the [Simulation] typically starts with a zero velocity. |
568 | /// |
569 | /// See also: |
570 | /// |
571 | /// * [DrivenScrollActivity], which animates a scroll view based on a set of |
572 | /// animation parameters. |
573 | class BallisticScrollActivity extends ScrollActivity { |
574 | /// Creates an activity that animates a scroll view based on a [simulation]. |
575 | BallisticScrollActivity( |
576 | super.delegate, |
577 | Simulation simulation, |
578 | TickerProvider vsync, |
579 | this.shouldIgnorePointer, |
580 | ) { |
581 | _controller = |
582 | AnimationController.unbounded( |
583 | debugLabel: kDebugMode ? objectRuntimeType(this, 'BallisticScrollActivity') : null, |
584 | vsync: vsync, |
585 | ) |
586 | ..addListener(_tick) |
587 | ..animateWith( |
588 | simulation, |
589 | ).whenComplete(_end); // won't trigger if we dispose _controller before it completes. |
590 | } |
591 | |
592 | late AnimationController _controller; |
593 | |
594 | @override |
595 | void resetActivity() { |
596 | delegate.goBallistic(velocity); |
597 | } |
598 | |
599 | @override |
600 | void applyNewDimensions() { |
601 | delegate.goBallistic(velocity); |
602 | } |
603 | |
604 | void _tick() { |
605 | if (!applyMoveTo(_controller.value)) { |
606 | delegate.goIdle(); |
607 | } |
608 | } |
609 | |
610 | /// Move the position to the given location. |
611 | /// |
612 | /// If the new position was fully applied, returns true. If there was any |
613 | /// overflow, returns false. |
614 | /// |
615 | /// The default implementation calls [ScrollActivityDelegate.setPixels] |
616 | /// and returns true if the overflow was zero. |
617 | @protected |
618 | bool applyMoveTo(double value) { |
619 | return delegate.setPixels(value).abs() < precisionErrorTolerance; |
620 | } |
621 | |
622 | void _end() { |
623 | // Check if the activity was disposed before going ballistic because _end might be called |
624 | // if _controller is disposed just after completion. |
625 | if (!_isDisposed) { |
626 | delegate.goBallistic(0.0); |
627 | } |
628 | } |
629 | |
630 | @override |
631 | void dispatchOverscrollNotification( |
632 | ScrollMetrics metrics, |
633 | BuildContext context, |
634 | double overscroll, |
635 | ) { |
636 | OverscrollNotification( |
637 | metrics: metrics, |
638 | context: context, |
639 | overscroll: overscroll, |
640 | velocity: velocity, |
641 | ).dispatch(context); |
642 | } |
643 | |
644 | @override |
645 | final bool shouldIgnorePointer; |
646 | |
647 | @override |
648 | bool get isScrolling => true; |
649 | |
650 | @override |
651 | double get velocity => _controller.velocity; |
652 | |
653 | @override |
654 | void dispose() { |
655 | _controller.dispose(); |
656 | super.dispose(); |
657 | } |
658 | |
659 | @override |
660 | String toString() { |
661 | return '${describeIdentity(this)} ($_controller )'; |
662 | } |
663 | } |
664 | |
665 | /// An activity that animates a scroll view based on animation parameters. |
666 | /// |
667 | /// For example, a [DrivenScrollActivity] is used to implement |
668 | /// [ScrollController.animateTo]. |
669 | /// |
670 | /// See also: |
671 | /// |
672 | /// * [BallisticScrollActivity], which animates a scroll view based on a |
673 | /// physics [Simulation]. |
674 | class DrivenScrollActivity extends ScrollActivity { |
675 | /// Creates an activity that animates a scroll view based on animation |
676 | /// parameters. |
677 | DrivenScrollActivity( |
678 | super.delegate, { |
679 | required double from, |
680 | required double to, |
681 | required Duration duration, |
682 | required Curve curve, |
683 | required TickerProvider vsync, |
684 | }) : assert(duration > Duration.zero) { |
685 | _completer = Completer<void>(); |
686 | _controller = |
687 | AnimationController.unbounded( |
688 | value: from, |
689 | debugLabel: objectRuntimeType(this, 'DrivenScrollActivity'), |
690 | vsync: vsync, |
691 | ) |
692 | ..addListener(_tick) |
693 | ..animateTo( |
694 | to, |
695 | duration: duration, |
696 | curve: curve, |
697 | ).whenComplete(_end); // won't trigger if we dispose _controller before it completes. |
698 | } |
699 | |
700 | late final Completer<void> _completer; |
701 | late final AnimationController _controller; |
702 | |
703 | /// A [Future] that completes when the activity stops. |
704 | /// |
705 | /// For example, this [Future] will complete if the animation reaches the end |
706 | /// or if the user interacts with the scroll view in way that causes the |
707 | /// animation to stop before it reaches the end. |
708 | Future<void> get done => _completer.future; |
709 | |
710 | void _tick() { |
711 | if (delegate.setPixels(_controller.value) != 0.0) { |
712 | delegate.goIdle(); |
713 | } |
714 | } |
715 | |
716 | void _end() { |
717 | // Check if the activity was disposed before going ballistic because _end might be called |
718 | // if _controller is disposed just after completion. |
719 | if (!_isDisposed) { |
720 | delegate.goBallistic(velocity); |
721 | } |
722 | } |
723 | |
724 | @override |
725 | void dispatchOverscrollNotification( |
726 | ScrollMetrics metrics, |
727 | BuildContext context, |
728 | double overscroll, |
729 | ) { |
730 | OverscrollNotification( |
731 | metrics: metrics, |
732 | context: context, |
733 | overscroll: overscroll, |
734 | velocity: velocity, |
735 | ).dispatch(context); |
736 | } |
737 | |
738 | @override |
739 | bool get shouldIgnorePointer => true; |
740 | |
741 | @override |
742 | bool get isScrolling => true; |
743 | |
744 | @override |
745 | double get velocity => _controller.velocity; |
746 | |
747 | @override |
748 | void dispose() { |
749 | _completer.complete(); |
750 | _controller.dispose(); |
751 | super.dispose(); |
752 | } |
753 | |
754 | @override |
755 | String toString() { |
756 | return '${describeIdentity(this)} ($_controller )'; |
757 | } |
758 | } |
759 |
Definitions
- ScrollActivityDelegate
- axisDirection
- setPixels
- applyUserOffset
- goIdle
- goBallistic
- ScrollActivity
- ScrollActivity
- delegate
- updateDelegate
- resetActivity
- dispatchScrollStartNotification
- dispatchScrollUpdateNotification
- dispatchOverscrollNotification
- dispatchScrollEndNotification
- applyNewDimensions
- shouldIgnorePointer
- isScrolling
- velocity
- dispose
- toString
- IdleScrollActivity
- IdleScrollActivity
- applyNewDimensions
- shouldIgnorePointer
- isScrolling
- velocity
- ScrollHoldController
- cancel
- HoldScrollActivity
- HoldScrollActivity
- shouldIgnorePointer
- isScrolling
- velocity
- cancel
- dispose
- ScrollDragController
- ScrollDragController
- delegate
- _reversed
- updateDelegate
- _maybeLoseMomentum
- _adjustForScrollStartThreshold
- update
- end
- cancel
- dispose
- lastDetails
- toString
- DragScrollActivity
- DragScrollActivity
- dispatchScrollStartNotification
- dispatchScrollUpdateNotification
- dispatchOverscrollNotification
- dispatchScrollEndNotification
- shouldIgnorePointer
- isScrolling
- velocity
- dispose
- toString
- BallisticScrollActivity
- BallisticScrollActivity
- resetActivity
- applyNewDimensions
- _tick
- applyMoveTo
- _end
- dispatchOverscrollNotification
- isScrolling
- velocity
- dispose
- toString
- DrivenScrollActivity
- DrivenScrollActivity
- done
- _tick
- _end
- dispatchOverscrollNotification
- shouldIgnorePointer
- isScrolling
- velocity
- dispose
Learn more about Flutter for embedded and desktop on industrialflutter.com