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'; |
14 | library; |
15 | |
16 | import 'dart:math' as math; |
17 | |
18 | import 'package:flutter/foundation.dart'; |
19 | import 'package:flutter/gestures.dart'; |
20 | import 'package:flutter/painting.dart' show AxisDirection; |
21 | import 'package:flutter/physics.dart'; |
22 | |
23 | import 'binding.dart' show WidgetsBinding; |
24 | import 'framework.dart'; |
25 | import 'overscroll_indicator.dart'; |
26 | import 'scroll_metrics.dart'; |
27 | import 'scroll_simulation.dart'; |
28 | import 'view.dart'; |
29 | |
30 | export 'package:flutter/physics.dart' show ScrollSpringSimulation, Simulation, Tolerance; |
31 | |
32 | /// The rate at which scroll momentum will be decelerated. |
33 | enum 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 |
88 | class 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. |
567 | class 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. |
678 | class 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. |
837 | class 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. |
947 | class 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. |
970 | class 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 |
Definitions
- ScrollDecelerationRate
- ScrollPhysics
- ScrollPhysics
- buildParent
- applyTo
- applyPhysicsToUserOffset
- shouldAcceptUserOffset
- recommendDeferredLoading
- applyBoundaryConditions
- adjustPositionForNewDimensions
- createBallisticSimulation
- spring
- tolerance
- toleranceFor
- minFlingDistance
- minFlingVelocity
- maxFlingVelocity
- carriedMomentum
- dragStartDistanceMotionThreshold
- allowImplicitScrolling
- allowUserScrolling
- toString
- RangeMaintainingScrollPhysics
- RangeMaintainingScrollPhysics
- applyTo
- adjustPositionForNewDimensions
- BouncingScrollPhysics
- BouncingScrollPhysics
- applyTo
- frictionFactor
- applyPhysicsToUserOffset
- _applyFriction
- applyBoundaryConditions
- createBallisticSimulation
- minFlingVelocity
- carriedMomentum
- dragStartDistanceMotionThreshold
- maxFlingVelocity
- spring
- ClampingScrollPhysics
- ClampingScrollPhysics
- applyTo
- applyBoundaryConditions
- createBallisticSimulation
- AlwaysScrollableScrollPhysics
- AlwaysScrollableScrollPhysics
- applyTo
- shouldAcceptUserOffset
- NeverScrollableScrollPhysics
- NeverScrollableScrollPhysics
- applyTo
- allowUserScrolling
Learn more about Flutter for embedded and desktop on industrialflutter.com