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 'dart:ui';
6/// @docImport 'package:flutter/material.dart';
7///
8/// @docImport 'scroll_activity.dart';
9/// @docImport 'scroll_configuration.dart';
10/// @docImport 'scroll_position.dart';
11/// @docImport 'scroll_position_with_single_context.dart';
12/// @docImport 'scroll_view.dart';
13/// @docImport 'scrollable.dart';
14library;
15
16import 'dart:math' as math;
17
18import 'package:flutter/foundation.dart';
19import 'package:flutter/gestures.dart';
20import 'package:flutter/painting.dart' show AxisDirection;
21import 'package:flutter/physics.dart';
22
23import 'binding.dart' show WidgetsBinding;
24import 'framework.dart';
25import 'overscroll_indicator.dart';
26import 'scroll_metrics.dart';
27import 'scroll_simulation.dart';
28import 'view.dart';
29
30export 'package:flutter/physics.dart' show ScrollSpringSimulation, Simulation, Tolerance;
31
32/// The rate at which scroll momentum will be decelerated.
33enum ScrollDecelerationRate {
34 /// Standard deceleration, aligned with mobile software expectations.
35 normal,
36
37 /// Increased deceleration, aligned with desktop software expectations.
38 ///
39 /// Appropriate for use with input devices more precise than touch screens,
40 /// such as trackpads or mouse wheels.
41 fast,
42}
43
44// Examples can assume:
45// class FooScrollPhysics extends ScrollPhysics {
46// const FooScrollPhysics({ super.parent });
47// @override
48// FooScrollPhysics applyTo(ScrollPhysics? ancestor) {
49// return FooScrollPhysics(parent: buildParent(ancestor));
50// }
51// }
52// class BarScrollPhysics extends ScrollPhysics {
53// const BarScrollPhysics({ super.parent });
54// }
55
56/// Determines the physics of a [Scrollable] widget.
57///
58/// For example, determines how the [Scrollable] will behave when the user
59/// reaches the maximum scroll extent or when the user stops scrolling.
60///
61/// When starting a physics [Simulation], the current scroll position and
62/// velocity are used as the initial conditions for the particle in the
63/// simulation. The movement of the particle in the simulation is then used to
64/// determine the scroll position for the widget.
65///
66/// Instead of creating your own subclasses, [parent] can be used to combine
67/// [ScrollPhysics] objects of different types to get the desired scroll physics.
68/// For example:
69///
70/// ```dart
71/// const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics())
72/// ```
73///
74/// You can also use `applyTo`, which is useful when you already have
75/// an instance of [ScrollPhysics]:
76///
77/// ```dart
78/// ScrollPhysics physics = const BouncingScrollPhysics();
79/// // ...
80/// final ScrollPhysics mergedPhysics = physics.applyTo(const AlwaysScrollableScrollPhysics());
81/// ```
82///
83/// When implementing a subclass, you must override [applyTo] so that it returns
84/// an appropriate instance of your subclass. Otherwise, classes like
85/// [Scrollable] that inform a [ScrollPosition] will combine them with
86/// the default [ScrollPhysics] object instead of your custom subclass.
87@immutable
88class ScrollPhysics {
89 /// Creates an object with the default scroll physics.
90 const ScrollPhysics({this.parent});
91
92 /// If non-null, determines the default behavior for each method.
93 ///
94 /// If a subclass of [ScrollPhysics] does not override a method, that subclass
95 /// will inherit an implementation from this base class that defers to
96 /// [parent]. This mechanism lets you assemble novel combinations of
97 /// [ScrollPhysics] subclasses at runtime. For example:
98 ///
99 /// ```dart
100 /// const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics())
101 /// ```
102 ///
103 /// will result in a [ScrollPhysics] that has the combined behavior
104 /// of [BouncingScrollPhysics] and [AlwaysScrollableScrollPhysics]:
105 /// behaviors that are not specified in [BouncingScrollPhysics]
106 /// (e.g. [shouldAcceptUserOffset]) will defer to [AlwaysScrollableScrollPhysics].
107 final ScrollPhysics? parent;
108
109 /// If [parent] is null then return ancestor, otherwise recursively build a
110 /// ScrollPhysics that has [ancestor] as its parent.
111 ///
112 /// This method is typically used to define [applyTo] methods like:
113 ///
114 /// ```dart
115 /// class MyScrollPhysics extends ScrollPhysics {
116 /// const MyScrollPhysics({ super.parent });
117 ///
118 /// @override
119 /// MyScrollPhysics applyTo(ScrollPhysics? ancestor) {
120 /// return MyScrollPhysics(parent: buildParent(ancestor));
121 /// }
122 ///
123 /// // ...
124 /// }
125 /// ```
126 @protected
127 ScrollPhysics? buildParent(ScrollPhysics? ancestor) => parent?.applyTo(ancestor) ?? ancestor;
128
129 /// Combines this [ScrollPhysics] instance with the given physics.
130 ///
131 /// The returned object uses this instance's physics when it has an
132 /// opinion, and defers to the given `ancestor` object's physics
133 /// when it does not.
134 ///
135 /// If [parent] is null then this returns a [ScrollPhysics] with the
136 /// same [runtimeType], but where the [parent] has been replaced
137 /// with the [ancestor].
138 ///
139 /// If this scroll physics object already has a parent, then this
140 /// method is applied recursively and ancestor will appear at the
141 /// end of the existing chain of parents.
142 ///
143 /// Calling this method with a null argument will copy the current
144 /// object. This is inefficient.
145 ///
146 /// {@tool snippet}
147 ///
148 /// In the following example, the [applyTo] method is used to combine the
149 /// scroll physics of two [ScrollPhysics] objects. The resulting [ScrollPhysics]
150 /// `x` has the same behavior as `y`.
151 ///
152 /// ```dart
153 /// final FooScrollPhysics x = const FooScrollPhysics().applyTo(const BarScrollPhysics());
154 /// const FooScrollPhysics y = FooScrollPhysics(parent: BarScrollPhysics());
155 /// ```
156 /// {@end-tool}
157 ///
158 /// ## Implementing [applyTo]
159 ///
160 /// When creating a custom [ScrollPhysics] subclass, this method
161 /// must be implemented. If the physics class has no constructor
162 /// arguments, then implementing this method is merely a matter of
163 /// calling the constructor with a [parent] constructed using
164 /// [buildParent], as follows:
165 ///
166 /// ```dart
167 /// class MyScrollPhysics extends ScrollPhysics {
168 /// const MyScrollPhysics({ super.parent });
169 ///
170 /// @override
171 /// MyScrollPhysics applyTo(ScrollPhysics? ancestor) {
172 /// return MyScrollPhysics(parent: buildParent(ancestor));
173 /// }
174 ///
175 /// // ...
176 /// }
177 /// ```
178 ///
179 /// If the physics class has constructor arguments, they must be passed to
180 /// the constructor here as well, so as to create a clone.
181 ///
182 /// See also:
183 ///
184 /// * [buildParent], a utility method that's often used to define [applyTo]
185 /// methods for [ScrollPhysics] subclasses.
186 ScrollPhysics applyTo(ScrollPhysics? ancestor) {
187 return ScrollPhysics(parent: buildParent(ancestor));
188 }
189
190 /// Used by [DragScrollActivity] and other user-driven activities to convert
191 /// an offset in logical pixels as provided by the [DragUpdateDetails] into a
192 /// delta to apply (subtract from the current position) using
193 /// [ScrollActivityDelegate.setPixels].
194 ///
195 /// This is used by some [ScrollPosition] subclasses to apply friction during
196 /// overscroll situations.
197 ///
198 /// This method must not adjust parts of the offset that are entirely within
199 /// the bounds described by the given `position`.
200 ///
201 /// The given `position` is only valid during this method call. Do not keep a
202 /// reference to it to use later, as the values may update, may not update, or
203 /// may update to reflect an entirely unrelated scrollable.
204 double applyPhysicsToUserOffset(ScrollMetrics position, double offset) {
205 return parent?.applyPhysicsToUserOffset(position, offset) ?? offset;
206 }
207
208 /// Whether the scrollable should let the user adjust the scroll offset, for
209 /// example by dragging. If [allowUserScrolling] is false, the scrollable
210 /// will never allow user input to change the scroll position.
211 ///
212 /// By default, the user can manipulate the scroll offset if, and only if,
213 /// there is actually content outside the viewport to reveal.
214 ///
215 /// The given `position` is only valid during this method call. Do not keep a
216 /// reference to it to use later, as the values may update, may not update, or
217 /// may update to reflect an entirely unrelated scrollable.
218 bool shouldAcceptUserOffset(ScrollMetrics position) {
219 if (!allowUserScrolling) {
220 return false;
221 }
222
223 if (parent == null) {
224 return position.pixels != 0.0 || position.minScrollExtent != position.maxScrollExtent;
225 }
226 return parent!.shouldAcceptUserOffset(position);
227 }
228
229 /// Provides a heuristic to determine if expensive frame-bound tasks should be
230 /// deferred.
231 ///
232 /// The `velocity` parameter may be positive, negative, or zero.
233 ///
234 /// The `context` parameter normally refers to the [BuildContext] of the widget
235 /// making the call, such as an [Image] widget in a [ListView].
236 ///
237 /// This can be used to determine whether decoding or fetching complex data
238 /// for the currently visible part of the viewport should be delayed
239 /// to avoid doing work that will not have a chance to appear before a new
240 /// frame is rendered.
241 ///
242 /// For example, a list of images could use this logic to delay decoding
243 /// images until scrolling is slow enough to actually render the decoded
244 /// image to the screen.
245 ///
246 /// The default implementation is a heuristic that compares the current
247 /// scroll velocity in local logical pixels to the longest side of the window
248 /// in physical pixels. Implementers can change this heuristic by overriding
249 /// this method and providing their custom physics to the scrollable widget.
250 /// For example, an application that changes the local coordinate system with
251 /// a large perspective transform could provide a more or less aggressive
252 /// heuristic depending on whether the transform was increasing or decreasing
253 /// the overall scale between the global screen and local scrollable
254 /// coordinate systems.
255 ///
256 /// The default implementation is stateless, and provides a point-in-time
257 /// decision about how fast the scrollable is scrolling. It would always
258 /// return true for a scrollable that is animating back and forth at high
259 /// velocity in a loop. It is assumed that callers will handle such
260 /// a case, or that a custom stateful implementation would be written that
261 /// tracks the sign of the velocity on successive calls.
262 ///
263 /// Returning true from this method indicates that the current scroll velocity
264 /// is great enough that expensive operations impacting the UI should be
265 /// deferred.
266 bool recommendDeferredLoading(double velocity, ScrollMetrics metrics, BuildContext context) {
267 if (parent == null) {
268 final double maxPhysicalPixels = View.of(context).physicalSize.longestSide;
269 return velocity.abs() > maxPhysicalPixels;
270 }
271 return parent!.recommendDeferredLoading(velocity, metrics, context);
272 }
273
274 /// Determines the overscroll by applying the boundary conditions.
275 ///
276 /// Called by [ScrollPosition.applyBoundaryConditions], which is called by
277 /// [ScrollPosition.setPixels] just before the [ScrollPosition.pixels] value
278 /// is updated, to determine how much of the offset is to be clamped off and
279 /// sent to [ScrollPosition.didOverscrollBy].
280 ///
281 /// The `value` argument is guaranteed to not equal the [ScrollMetrics.pixels]
282 /// of the `position` argument when this is called.
283 ///
284 /// It is possible for this method to be called when the `position` describes
285 /// an already-out-of-bounds position. In that case, the boundary conditions
286 /// should usually only prevent a further increase in the extent to which the
287 /// position is out of bounds, allowing a decrease to be applied successfully,
288 /// so that (for instance) an animation can smoothly snap an out of bounds
289 /// position to the bounds. See [BallisticScrollActivity].
290 ///
291 /// This method must not clamp parts of the offset that are entirely within
292 /// the bounds described by the given `position`.
293 ///
294 /// The given `position` is only valid during this method call. Do not keep a
295 /// reference to it to use later, as the values may update, may not update, or
296 /// may update to reflect an entirely unrelated scrollable.
297 ///
298 /// ## Examples
299 ///
300 /// [BouncingScrollPhysics] returns zero. In other words, it allows scrolling
301 /// past the boundary unhindered.
302 ///
303 /// [ClampingScrollPhysics] returns the amount by which the value is beyond
304 /// the position or the boundary, whichever is furthest from the content. In
305 /// other words, it disallows scrolling past the boundary, but allows
306 /// scrolling back from being overscrolled, if for some reason the position
307 /// ends up overscrolled.
308 double applyBoundaryConditions(ScrollMetrics position, double value) {
309 return parent?.applyBoundaryConditions(position, value) ?? 0.0;
310 }
311
312 /// Describes what the scroll position should be given new viewport dimensions.
313 ///
314 /// This is called by [ScrollPosition.correctForNewDimensions].
315 ///
316 /// The arguments consist of the scroll metrics as they stood in the previous
317 /// frame and the scroll metrics as they now stand after the last layout,
318 /// including the position and minimum and maximum scroll extents; a flag
319 /// indicating if the current [ScrollActivity] considers that the user is
320 /// actively scrolling (see [ScrollActivity.isScrolling]); and the current
321 /// velocity of the scroll position, if it is being driven by the scroll
322 /// activity (this is 0.0 during a user gesture) (see
323 /// [ScrollActivity.velocity]).
324 ///
325 /// The scroll metrics will be identical except for the
326 /// [ScrollMetrics.minScrollExtent] and [ScrollMetrics.maxScrollExtent]. They
327 /// are referred to as the `oldPosition` and `newPosition` (even though they
328 /// both technically have the same "position", in the form of
329 /// [ScrollMetrics.pixels]) because they are generated from the
330 /// [ScrollPosition] before and after updating the scroll extents.
331 ///
332 /// If the returned value does not exactly match the scroll offset given by
333 /// the `newPosition` argument (see [ScrollMetrics.pixels]), then the
334 /// [ScrollPosition] will call [ScrollPosition.correctPixels] to update the
335 /// new scroll position to the returned value, and layout will be re-run. This
336 /// is expensive. The new value is subject to further manipulation by
337 /// [applyBoundaryConditions].
338 ///
339 /// If the returned value _does_ match the `newPosition.pixels` scroll offset
340 /// exactly, then [ScrollPosition.applyNewDimensions] will be called next. In
341 /// that case, [applyBoundaryConditions] is not applied to the return value.
342 ///
343 /// The given [ScrollMetrics] are only valid during this method call. Do not
344 /// keep references to them to use later, as the values may update, may not
345 /// update, or may update to reflect an entirely unrelated scrollable.
346 ///
347 /// The default implementation returns the [ScrollMetrics.pixels] of the
348 /// `newPosition`, which indicates that the current scroll offset is
349 /// acceptable.
350 ///
351 /// See also:
352 ///
353 /// * [RangeMaintainingScrollPhysics], which is enabled by default, and
354 /// which prevents unexpected changes to the content dimensions from
355 /// causing the scroll position to get any further out of bounds.
356 double adjustPositionForNewDimensions({
357 required ScrollMetrics oldPosition,
358 required ScrollMetrics newPosition,
359 required bool isScrolling,
360 required double velocity,
361 }) {
362 if (parent == null) {
363 return newPosition.pixels;
364 }
365 return parent!.adjustPositionForNewDimensions(
366 oldPosition: oldPosition,
367 newPosition: newPosition,
368 isScrolling: isScrolling,
369 velocity: velocity,
370 );
371 }
372
373 /// Returns a simulation for ballistic scrolling starting from the given
374 /// position with the given velocity.
375 ///
376 /// This is used by [ScrollPositionWithSingleContext] in the
377 /// [ScrollPositionWithSingleContext.goBallistic] method. If the result
378 /// is non-null, [ScrollPositionWithSingleContext] will begin a
379 /// [BallisticScrollActivity] with the returned value. Otherwise, it will
380 /// begin an idle activity instead.
381 ///
382 /// The given `position` is only valid during this method call. Do not keep a
383 /// reference to it to use later, as the values may update, may not update, or
384 /// may update to reflect an entirely unrelated scrollable.
385 ///
386 /// This method can potentially be called in every frame, even in the middle
387 /// of what the user perceives as a single ballistic scroll. For example, in
388 /// a [ListView] when previously off-screen items come into view and are laid
389 /// out, this method may be called with a new [ScrollMetrics.maxScrollExtent].
390 /// The method implementation should ensure that when the same ballistic
391 /// scroll motion is still intended, these calls have no side effects on the
392 /// physics beyond continuing that motion.
393 ///
394 /// Generally this is ensured by having the [Simulation] conform to a physical
395 /// metaphor of a particle in ballistic flight, where the forces on the
396 /// particle depend only on its position, velocity, and environment, and not
397 /// on the current time or any internal state. This means that the
398 /// time-derivative of [Simulation.dx] should be possible to write
399 /// mathematically as a function purely of the values of [Simulation.x],
400 /// [Simulation.dx], and the parameters used to construct the [Simulation],
401 /// independent of the time.
402 // TODO(gnprice): Some scroll physics in the framework violate that invariant; fix them.
403 // An audit found three cases violating the invariant:
404 // https://github.com/flutter/flutter/issues/120338
405 // https://github.com/flutter/flutter/issues/120340
406 // https://github.com/flutter/flutter/issues/109675
407 Simulation? createBallisticSimulation(ScrollMetrics position, double velocity) {
408 return parent?.createBallisticSimulation(position, velocity);
409 }
410
411 static final SpringDescription _kDefaultSpring = SpringDescription.withDampingRatio(
412 mass: 0.5,
413 stiffness: 100.0,
414 ratio: 1.1,
415 );
416
417 /// The spring to use for ballistic simulations.
418 SpringDescription get spring => parent?.spring ?? _kDefaultSpring;
419
420 /// Deprecated. Call [toleranceFor] instead.
421 @Deprecated(
422 'Call toleranceFor instead. '
423 'This feature was deprecated after v3.7.0-13.0.pre.',
424 )
425 Tolerance get tolerance {
426 return toleranceFor(
427 FixedScrollMetrics(
428 minScrollExtent: null,
429 maxScrollExtent: null,
430 pixels: null,
431 viewportDimension: null,
432 axisDirection: AxisDirection.down,
433 devicePixelRatio: WidgetsBinding.instance.window.devicePixelRatio,
434 ),
435 );
436 }
437
438 /// The tolerance to use for ballistic simulations.
439 Tolerance toleranceFor(ScrollMetrics metrics) {
440 return parent?.toleranceFor(metrics) ??
441 Tolerance(
442 velocity: 1.0 / (0.050 * metrics.devicePixelRatio), // logical pixels per second
443 distance: 1.0 / metrics.devicePixelRatio, // logical pixels
444 );
445 }
446
447 /// The minimum distance an input pointer drag must have moved to be
448 /// considered a scroll fling gesture.
449 ///
450 /// This value is typically compared with the distance traveled along the
451 /// scrolling axis.
452 ///
453 /// See also:
454 ///
455 /// * [VelocityTracker.getVelocityEstimate], which computes the velocity
456 /// of a press-drag-release gesture.
457 double get minFlingDistance => parent?.minFlingDistance ?? kTouchSlop;
458
459 /// The minimum velocity for an input pointer drag to be considered a
460 /// scroll fling.
461 ///
462 /// This value is typically compared with the magnitude of fling gesture's
463 /// velocity along the scrolling axis.
464 ///
465 /// See also:
466 ///
467 /// * [VelocityTracker.getVelocityEstimate], which computes the velocity
468 /// of a press-drag-release gesture.
469 double get minFlingVelocity => parent?.minFlingVelocity ?? kMinFlingVelocity;
470
471 /// Scroll fling velocity magnitudes will be clamped to this value.
472 double get maxFlingVelocity => parent?.maxFlingVelocity ?? kMaxFlingVelocity;
473
474 /// Returns the velocity carried on repeated flings.
475 ///
476 /// The function is applied to the existing scroll velocity when another
477 /// scroll drag is applied in the same direction.
478 ///
479 /// By default, physics for platforms other than iOS doesn't carry momentum.
480 double carriedMomentum(double existingVelocity) {
481 return parent?.carriedMomentum(existingVelocity) ?? 0.0;
482 }
483
484 /// The minimum amount of pixel distance drags must move by to start motion
485 /// the first time or after each time the drag motion stopped.
486 ///
487 /// If null, no minimum threshold is enforced.
488 double? get dragStartDistanceMotionThreshold => parent?.dragStartDistanceMotionThreshold;
489
490 /// Whether a viewport is allowed to change its scroll position implicitly in
491 /// response to a call to [RenderObject.showOnScreen].
492 ///
493 /// [RenderObject.showOnScreen] is for example used to bring a text field
494 /// fully on screen after it has received focus. This property controls
495 /// whether the viewport associated with this object is allowed to change the
496 /// scroll position to fulfill such a request.
497 bool get allowImplicitScrolling => true;
498
499 /// Whether a viewport is allowed to change the scroll position as the result of user input.
500 bool get allowUserScrolling => true;
501
502 @override
503 String toString() {
504 if (parent == null) {
505 return objectRuntimeType(this, 'ScrollPhysics');
506 }
507 return '${objectRuntimeType(this, 'ScrollPhysics')} -> $parent';
508 }
509}
510
511/// Scroll physics that attempt to keep the scroll position in range when the
512/// contents change dimensions suddenly.
513///
514/// This attempts to maintain the amount of overscroll or underscroll already present,
515/// if the scroll position is already out of range _and_ the extents
516/// have decreased, meaning that some content was removed. The reason for this
517/// condition is that when new content is added, keeping the same overscroll
518/// would mean that instead of showing it to the user, all of it is
519/// being skipped by jumping right to the max extent.
520///
521/// If the scroll activity is animating the scroll position, sudden changes to
522/// the scroll dimensions are allowed to happen (so as to prevent animations
523/// from jumping back and forth between in-range and out-of-range values).
524///
525/// These physics should be combined with other scroll physics, e.g.
526/// [BouncingScrollPhysics] or [ClampingScrollPhysics], to obtain a complete
527/// description of typical scroll physics. See [applyTo].
528///
529/// ## Implementation details
530///
531/// Specifically, these physics perform two adjustments.
532///
533/// The first is to maintain overscroll when the position is out of range.
534///
535/// The second is to enforce the boundary when the position is in range.
536///
537/// If the current velocity is non-zero, neither adjustment is made. The
538/// assumption is that there is an ongoing animation and therefore
539/// further changing the scroll position would disrupt the experience.
540///
541/// If the extents haven't changed, then the overscroll adjustment is
542/// not made. The assumption is that if the position is overscrolled,
543/// it is intentional, otherwise the position could not have reached
544/// that position. (Consider [ClampingScrollPhysics] vs
545/// [BouncingScrollPhysics] for example.)
546///
547/// If the position itself changed since the last animation frame,
548/// then the overscroll is not maintained. The assumption is similar
549/// to the previous case: the position would not have been placed out
550/// of range unless it was intentional.
551///
552/// In addition, if the position changed and the boundaries were and
553/// still are finite, then the boundary isn't enforced either, for
554/// the same reason. However, if any of the boundaries were or are
555/// now infinite, the boundary _is_ enforced, on the assumption that
556/// infinite boundaries indicate a lazy-loading scroll view, which
557/// cannot enforce boundaries while the full list has not loaded.
558///
559/// If the range was out of range, then the boundary is not enforced
560/// even if the range is not maintained. If the range is maintained,
561/// then the distance between the old position and the old boundary is
562/// applied to the new boundary to obtain the new position.
563///
564/// If the range was in range, and the boundary is to be enforced,
565/// then the new position is obtained by deferring to the other physics,
566/// if any, and then clamped to the new range.
567class RangeMaintainingScrollPhysics extends ScrollPhysics {
568 /// Creates scroll physics that maintain the scroll position in range.
569 const RangeMaintainingScrollPhysics({super.parent});
570
571 @override
572 RangeMaintainingScrollPhysics applyTo(ScrollPhysics? ancestor) {
573 return RangeMaintainingScrollPhysics(parent: buildParent(ancestor));
574 }
575
576 @override
577 double adjustPositionForNewDimensions({
578 required ScrollMetrics oldPosition,
579 required ScrollMetrics newPosition,
580 required bool isScrolling,
581 required double velocity,
582 }) {
583 bool maintainOverscroll = true;
584 bool enforceBoundary = true;
585 if (velocity != 0.0) {
586 // Don't try to adjust an animating position, the jumping around
587 // would be distracting.
588 maintainOverscroll = false;
589 enforceBoundary = false;
590 }
591 if ((oldPosition.minScrollExtent == newPosition.minScrollExtent) &&
592 (oldPosition.maxScrollExtent == newPosition.maxScrollExtent)) {
593 // If the extents haven't changed then ignore overscroll.
594 maintainOverscroll = false;
595 }
596 if (oldPosition.pixels != newPosition.pixels) {
597 // If the position has been changed already, then it might have
598 // been adjusted to expect new overscroll, so don't try to
599 // maintain the relative overscroll.
600 maintainOverscroll = false;
601 if (oldPosition.minScrollExtent.isFinite &&
602 oldPosition.maxScrollExtent.isFinite &&
603 newPosition.minScrollExtent.isFinite &&
604 newPosition.maxScrollExtent.isFinite) {
605 // In addition, if the position changed then we don't enforce the new
606 // boundary if both the new and previous boundaries are entirely finite.
607 // A common case where the position changes while one
608 // of the extents is infinite is a lazily-loaded list. (If the
609 // boundaries were finite, and the position changed, then we
610 // assume it was intentional.)
611 enforceBoundary = false;
612 }
613 }
614 if ((oldPosition.pixels < oldPosition.minScrollExtent) ||
615 (oldPosition.pixels > oldPosition.maxScrollExtent)) {
616 // If the old position was out of range, then we should
617 // not try to keep the new position in range.
618 enforceBoundary = false;
619 }
620 if (maintainOverscroll) {
621 // Force the new position to be no more out of range than it was before, if:
622 // * it was overscrolled, and
623 // * the extents have decreased, meaning that some content was removed. The
624 // reason for this condition is that when new content is added, keeping
625 // the same overscroll would mean that instead of showing it to the user,
626 // all of it is being skipped by jumping right to the max extent.
627 if (oldPosition.pixels < oldPosition.minScrollExtent &&
628 newPosition.minScrollExtent > oldPosition.minScrollExtent) {
629 final double oldDelta = oldPosition.minScrollExtent - oldPosition.pixels;
630 return newPosition.minScrollExtent - oldDelta;
631 }
632 if (oldPosition.pixels > oldPosition.maxScrollExtent &&
633 newPosition.maxScrollExtent < oldPosition.maxScrollExtent) {
634 final double oldDelta = oldPosition.pixels - oldPosition.maxScrollExtent;
635 return newPosition.maxScrollExtent + oldDelta;
636 }
637 }
638 // If we're not forcing the overscroll, defer to other physics.
639 double result = super.adjustPositionForNewDimensions(
640 oldPosition: oldPosition,
641 newPosition: newPosition,
642 isScrolling: isScrolling,
643 velocity: velocity,
644 );
645 if (enforceBoundary) {
646 // ...but if they put us out of range then reinforce the boundary.
647 result = clampDouble(result, newPosition.minScrollExtent, newPosition.maxScrollExtent);
648 }
649 return result;
650 }
651}
652
653/// Scroll physics for environments that allow the scroll offset to go beyond
654/// the bounds of the content, but then bounce the content back to the edge of
655/// those bounds.
656///
657/// This is the behavior typically seen on iOS.
658///
659/// [BouncingScrollPhysics] by itself will not create an overscroll effect if
660/// the contents of the scroll view do not extend beyond the size of the
661/// viewport. To create the overscroll and bounce effect regardless of the
662/// length of your scroll view, combine with [AlwaysScrollableScrollPhysics].
663///
664/// {@tool snippet}
665/// ```dart
666/// const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics())
667/// ```
668/// {@end-tool}
669///
670/// See also:
671///
672/// * [ScrollConfiguration], which uses this to provide the default
673/// scroll behavior on iOS.
674/// * [ClampingScrollPhysics], which is the analogous physics for Android's
675/// clamping behavior.
676/// * [ScrollPhysics], for more examples of combining [ScrollPhysics] objects
677/// of different types to get the desired scroll physics.
678class BouncingScrollPhysics extends ScrollPhysics {
679 /// Creates scroll physics that bounce back from the edge.
680 const BouncingScrollPhysics({
681 this.decelerationRate = ScrollDecelerationRate.normal,
682 super.parent,
683 });
684
685 /// Used to determine parameters for friction simulations.
686 final ScrollDecelerationRate decelerationRate;
687
688 @override
689 BouncingScrollPhysics applyTo(ScrollPhysics? ancestor) {
690 return BouncingScrollPhysics(parent: buildParent(ancestor), decelerationRate: decelerationRate);
691 }
692
693 /// The multiple applied to overscroll to make it appear that scrolling past
694 /// the edge of the scrollable contents is harder than scrolling the list.
695 /// This is done by reducing the ratio of the scroll effect output vs the
696 /// scroll gesture input.
697 ///
698 /// This factor starts at 0.52 and progressively becomes harder to overscroll
699 /// as more of the area past the edge is dragged in (represented by an increasing
700 /// `overscrollFraction` which starts at 0 when there is no overscroll).
701 double frictionFactor(double overscrollFraction) {
702 return math.pow(1 - overscrollFraction, 2) *
703 switch (decelerationRate) {
704 ScrollDecelerationRate.fast => 0.26,
705 ScrollDecelerationRate.normal => 0.52,
706 };
707 }
708
709 @override
710 double applyPhysicsToUserOffset(ScrollMetrics position, double offset) {
711 assert(offset != 0.0);
712 assert(position.minScrollExtent <= position.maxScrollExtent);
713
714 if (!position.outOfRange) {
715 return offset;
716 }
717
718 final double overscrollPastStart = math.max(position.minScrollExtent - position.pixels, 0.0);
719 final double overscrollPastEnd = math.max(position.pixels - position.maxScrollExtent, 0.0);
720 final double overscrollPast = math.max(overscrollPastStart, overscrollPastEnd);
721 final bool easing =
722 (overscrollPastStart > 0.0 && offset < 0.0) || (overscrollPastEnd > 0.0 && offset > 0.0);
723
724 final double friction =
725 easing
726 // Apply less resistance when easing the overscroll vs tensioning.
727 ? frictionFactor((overscrollPast - offset.abs()) / position.viewportDimension)
728 : frictionFactor(overscrollPast / position.viewportDimension);
729 final double direction = offset.sign;
730
731 if (easing && decelerationRate == ScrollDecelerationRate.fast) {
732 return direction * offset.abs();
733 }
734 return direction * _applyFriction(overscrollPast, offset.abs(), friction);
735 }
736
737 static double _applyFriction(double extentOutside, double absDelta, double gamma) {
738 assert(absDelta > 0);
739 double total = 0.0;
740 if (extentOutside > 0) {
741 final double deltaToLimit = extentOutside / gamma;
742 if (absDelta < deltaToLimit) {
743 return absDelta * gamma;
744 }
745 total += extentOutside;
746 absDelta -= deltaToLimit;
747 }
748 return total + absDelta;
749 }
750
751 @override
752 double applyBoundaryConditions(ScrollMetrics position, double value) => 0.0;
753
754 @override
755 Simulation? createBallisticSimulation(ScrollMetrics position, double velocity) {
756 final Tolerance tolerance = toleranceFor(position);
757 if (velocity.abs() >= tolerance.velocity || position.outOfRange) {
758 return BouncingScrollSimulation(
759 spring: spring,
760 position: position.pixels,
761 velocity: velocity,
762 leadingExtent: position.minScrollExtent,
763 trailingExtent: position.maxScrollExtent,
764 tolerance: tolerance,
765 constantDeceleration: switch (decelerationRate) {
766 ScrollDecelerationRate.fast => 1400,
767 ScrollDecelerationRate.normal => 0,
768 },
769 );
770 }
771 return null;
772 }
773
774 // The ballistic simulation here decelerates more slowly than the one for
775 // ClampingScrollPhysics so we require a more deliberate input gesture
776 // to trigger a fling.
777 @override
778 double get minFlingVelocity => kMinFlingVelocity * 2.0;
779
780 // Methodology:
781 // 1- Use https://github.com/flutter/platform_tests/tree/master/scroll_overlay to test with
782 // Flutter and platform scroll views superimposed.
783 // 3- If the scrollables stopped overlapping at any moment, adjust the desired
784 // output value of this function at that input speed.
785 // 4- Feed new input/output set into a power curve fitter. Change function
786 // and repeat from 2.
787 // 5- Repeat from 2 with medium and slow flings.
788 /// Momentum build-up function that mimics iOS's scroll speed increase with repeated flings.
789 ///
790 /// The velocity of the last fling is not an important factor. Existing speed
791 /// and (related) time since last fling are factors for the velocity transfer
792 /// calculations.
793 @override
794 double carriedMomentum(double existingVelocity) {
795 return existingVelocity.sign *
796 math.min(0.000816 * math.pow(existingVelocity.abs(), 1.967).toDouble(), 40000.0);
797 }
798
799 // Eyeballed from observation to counter the effect of an unintended scroll
800 // from the natural motion of lifting the finger after a scroll.
801 @override
802 double get dragStartDistanceMotionThreshold => 3.5;
803
804 @override
805 double get maxFlingVelocity => switch (decelerationRate) {
806 ScrollDecelerationRate.fast => kMaxFlingVelocity * 8.0,
807 ScrollDecelerationRate.normal => super.maxFlingVelocity,
808 };
809
810 @override
811 SpringDescription get spring {
812 switch (decelerationRate) {
813 case ScrollDecelerationRate.fast:
814 return SpringDescription.withDampingRatio(mass: 0.3, stiffness: 75.0, ratio: 1.3);
815 case ScrollDecelerationRate.normal:
816 return super.spring;
817 }
818 }
819}
820
821/// Scroll physics for environments that prevent the scroll offset from reaching
822/// beyond the bounds of the content.
823///
824/// This is the behavior typically seen on Android.
825///
826/// See also:
827///
828/// * [ScrollConfiguration], which uses this to provide the default
829/// scroll behavior on Android.
830/// * [BouncingScrollPhysics], which is the analogous physics for iOS' bouncing
831/// behavior.
832/// * [GlowingOverscrollIndicator], which is used by [ScrollConfiguration] to
833/// provide the glowing effect that is usually found with this clamping effect
834/// on Android. When using a [MaterialApp], the [GlowingOverscrollIndicator]'s
835/// glow color is specified to use the overall theme's
836/// [ColorScheme.secondary] color.
837class ClampingScrollPhysics extends ScrollPhysics {
838 /// Creates scroll physics that prevent the scroll offset from exceeding the
839 /// bounds of the content.
840 const ClampingScrollPhysics({super.parent});
841
842 @override
843 ClampingScrollPhysics applyTo(ScrollPhysics? ancestor) {
844 return ClampingScrollPhysics(parent: buildParent(ancestor));
845 }
846
847 @override
848 double applyBoundaryConditions(ScrollMetrics position, double value) {
849 assert(() {
850 if (value == position.pixels) {
851 throw FlutterError.fromParts(<DiagnosticsNode>[
852 ErrorSummary('$runtimeType.applyBoundaryConditions() was called redundantly.'),
853 ErrorDescription(
854 'The proposed new position, $value, is exactly equal to the current position of the '
855 'given ${position.runtimeType}, ${position.pixels}.\n'
856 'The applyBoundaryConditions method should only be called when the value is '
857 'going to actually change the pixels, otherwise it is redundant.',
858 ),
859 DiagnosticsProperty<ScrollPhysics>(
860 'The physics object in question was',
861 this,
862 style: DiagnosticsTreeStyle.errorProperty,
863 ),
864 DiagnosticsProperty<ScrollMetrics>(
865 'The position object in question was',
866 position,
867 style: DiagnosticsTreeStyle.errorProperty,
868 ),
869 ]);
870 }
871 return true;
872 }());
873 if (value < position.pixels && position.pixels <= position.minScrollExtent) {
874 // Underscroll.
875 return value - position.pixels;
876 }
877 if (position.maxScrollExtent <= position.pixels && position.pixels < value) {
878 // Overscroll.
879 return value - position.pixels;
880 }
881 if (value < position.minScrollExtent && position.minScrollExtent < position.pixels) {
882 // Hit top edge.
883 return value - position.minScrollExtent;
884 }
885 if (position.pixels < position.maxScrollExtent && position.maxScrollExtent < value) {
886 // Hit bottom edge.
887 return value - position.maxScrollExtent;
888 }
889 return 0.0;
890 }
891
892 @override
893 Simulation? createBallisticSimulation(ScrollMetrics position, double velocity) {
894 final Tolerance tolerance = toleranceFor(position);
895 if (position.outOfRange) {
896 double? end;
897 if (position.pixels > position.maxScrollExtent) {
898 end = position.maxScrollExtent;
899 }
900 if (position.pixels < position.minScrollExtent) {
901 end = position.minScrollExtent;
902 }
903 assert(end != null);
904 return ScrollSpringSimulation(
905 spring,
906 position.pixels,
907 end!,
908 math.min(0.0, velocity),
909 tolerance: tolerance,
910 );
911 }
912 if (velocity.abs() < tolerance.velocity) {
913 return null;
914 }
915 if (velocity > 0.0 && position.pixels >= position.maxScrollExtent) {
916 return null;
917 }
918 if (velocity < 0.0 && position.pixels <= position.minScrollExtent) {
919 return null;
920 }
921 return ClampingScrollSimulation(
922 position: position.pixels,
923 velocity: velocity,
924 tolerance: tolerance,
925 );
926 }
927}
928
929/// Scroll physics that always lets the user scroll.
930///
931/// This overrides the default behavior which is to disable scrolling
932/// when there is no content to scroll. It does not override the
933/// handling of overscrolling.
934///
935/// On Android, overscrolls will be clamped by default and result in an
936/// overscroll glow. On iOS, overscrolls will load a spring that will return the
937/// scroll view to its normal range when released.
938///
939/// See also:
940///
941/// * [ScrollPhysics], which can be used instead of this class when the default
942/// behavior is desired instead.
943/// * [BouncingScrollPhysics], which provides the bouncing overscroll behavior
944/// found on iOS.
945/// * [ClampingScrollPhysics], which provides the clamping overscroll behavior
946/// found on Android.
947class AlwaysScrollableScrollPhysics extends ScrollPhysics {
948 /// Creates scroll physics that always lets the user scroll.
949 const AlwaysScrollableScrollPhysics({super.parent});
950
951 @override
952 AlwaysScrollableScrollPhysics applyTo(ScrollPhysics? ancestor) {
953 return AlwaysScrollableScrollPhysics(parent: buildParent(ancestor));
954 }
955
956 @override
957 bool shouldAcceptUserOffset(ScrollMetrics position) => true;
958}
959
960/// Scroll physics that does not allow the user to scroll.
961///
962/// See also:
963///
964/// * [ScrollPhysics], which can be used instead of this class when the default
965/// behavior is desired instead.
966/// * [BouncingScrollPhysics], which provides the bouncing overscroll behavior
967/// found on iOS.
968/// * [ClampingScrollPhysics], which provides the clamping overscroll behavior
969/// found on Android.
970class NeverScrollableScrollPhysics extends ScrollPhysics {
971 /// Creates scroll physics that does not let the user scroll.
972 const NeverScrollableScrollPhysics({super.parent});
973
974 @override
975 NeverScrollableScrollPhysics applyTo(ScrollPhysics? ancestor) {
976 return NeverScrollableScrollPhysics(parent: buildParent(ancestor));
977 }
978
979 @override
980 bool get allowUserScrolling => false;
981
982 @override
983 bool get allowImplicitScrolling => false;
984}
985

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com