| 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 = easing |
| 725 | // Apply less resistance when easing the overscroll vs tensioning. |
| 726 | ? frictionFactor((overscrollPast - offset.abs()) / position.viewportDimension) |
| 727 | : frictionFactor(overscrollPast / position.viewportDimension); |
| 728 | final double direction = offset.sign; |
| 729 | |
| 730 | if (easing && decelerationRate == ScrollDecelerationRate.fast) { |
| 731 | return direction * offset.abs(); |
| 732 | } |
| 733 | return direction * _applyFriction(overscrollPast, offset.abs(), friction); |
| 734 | } |
| 735 | |
| 736 | static double _applyFriction(double extentOutside, double absDelta, double gamma) { |
| 737 | assert(absDelta > 0); |
| 738 | double total = 0.0; |
| 739 | if (extentOutside > 0) { |
| 740 | final double deltaToLimit = extentOutside / gamma; |
| 741 | if (absDelta < deltaToLimit) { |
| 742 | return absDelta * gamma; |
| 743 | } |
| 744 | total += extentOutside; |
| 745 | absDelta -= deltaToLimit; |
| 746 | } |
| 747 | return total + absDelta; |
| 748 | } |
| 749 | |
| 750 | @override |
| 751 | double applyBoundaryConditions(ScrollMetrics position, double value) => 0.0; |
| 752 | |
| 753 | @override |
| 754 | Simulation? createBallisticSimulation(ScrollMetrics position, double velocity) { |
| 755 | final Tolerance tolerance = toleranceFor(position); |
| 756 | if (velocity.abs() >= tolerance.velocity || position.outOfRange) { |
| 757 | return BouncingScrollSimulation( |
| 758 | spring: spring, |
| 759 | position: position.pixels, |
| 760 | velocity: velocity, |
| 761 | leadingExtent: position.minScrollExtent, |
| 762 | trailingExtent: position.maxScrollExtent, |
| 763 | tolerance: tolerance, |
| 764 | constantDeceleration: switch (decelerationRate) { |
| 765 | ScrollDecelerationRate.fast => 1400, |
| 766 | ScrollDecelerationRate.normal => 0, |
| 767 | }, |
| 768 | ); |
| 769 | } |
| 770 | return null; |
| 771 | } |
| 772 | |
| 773 | // The ballistic simulation here decelerates more slowly than the one for |
| 774 | // ClampingScrollPhysics so we require a more deliberate input gesture |
| 775 | // to trigger a fling. |
| 776 | @override |
| 777 | double get minFlingVelocity => kMinFlingVelocity * 2.0; |
| 778 | |
| 779 | // Methodology: |
| 780 | // 1- Use https://github.com/flutter/platform_tests/tree/master/scroll_overlay to test with |
| 781 | // Flutter and platform scroll views superimposed. |
| 782 | // 3- If the scrollables stopped overlapping at any moment, adjust the desired |
| 783 | // output value of this function at that input speed. |
| 784 | // 4- Feed new input/output set into a power curve fitter. Change function |
| 785 | // and repeat from 2. |
| 786 | // 5- Repeat from 2 with medium and slow flings. |
| 787 | /// Momentum build-up function that mimics iOS's scroll speed increase with repeated flings. |
| 788 | /// |
| 789 | /// The velocity of the last fling is not an important factor. Existing speed |
| 790 | /// and (related) time since last fling are factors for the velocity transfer |
| 791 | /// calculations. |
| 792 | @override |
| 793 | double carriedMomentum(double existingVelocity) { |
| 794 | return existingVelocity.sign * |
| 795 | math.min(0.000816 * math.pow(existingVelocity.abs(), 1.967).toDouble(), 40000.0); |
| 796 | } |
| 797 | |
| 798 | // Eyeballed from observation to counter the effect of an unintended scroll |
| 799 | // from the natural motion of lifting the finger after a scroll. |
| 800 | @override |
| 801 | double get dragStartDistanceMotionThreshold => 3.5; |
| 802 | |
| 803 | @override |
| 804 | double get maxFlingVelocity => switch (decelerationRate) { |
| 805 | ScrollDecelerationRate.fast => kMaxFlingVelocity * 8.0, |
| 806 | ScrollDecelerationRate.normal => super.maxFlingVelocity, |
| 807 | }; |
| 808 | |
| 809 | @override |
| 810 | SpringDescription get spring { |
| 811 | switch (decelerationRate) { |
| 812 | case ScrollDecelerationRate.fast: |
| 813 | return SpringDescription.withDampingRatio(mass: 0.3, stiffness: 75.0, ratio: 1.3); |
| 814 | case ScrollDecelerationRate.normal: |
| 815 | return super.spring; |
| 816 | } |
| 817 | } |
| 818 | } |
| 819 | |
| 820 | /// Scroll physics for environments that prevent the scroll offset from reaching |
| 821 | /// beyond the bounds of the content. |
| 822 | /// |
| 823 | /// This is the behavior typically seen on Android. |
| 824 | /// |
| 825 | /// See also: |
| 826 | /// |
| 827 | /// * [ScrollConfiguration], which uses this to provide the default |
| 828 | /// scroll behavior on Android. |
| 829 | /// * [BouncingScrollPhysics], which is the analogous physics for iOS' bouncing |
| 830 | /// behavior. |
| 831 | /// * [GlowingOverscrollIndicator], which is used by [ScrollConfiguration] to |
| 832 | /// provide the glowing effect that is usually found with this clamping effect |
| 833 | /// on Android. When using a [MaterialApp], the [GlowingOverscrollIndicator]'s |
| 834 | /// glow color is specified to use the overall theme's |
| 835 | /// [ColorScheme.secondary] color. |
| 836 | class ClampingScrollPhysics extends ScrollPhysics { |
| 837 | /// Creates scroll physics that prevent the scroll offset from exceeding the |
| 838 | /// bounds of the content. |
| 839 | const ClampingScrollPhysics({super.parent}); |
| 840 | |
| 841 | @override |
| 842 | ClampingScrollPhysics applyTo(ScrollPhysics? ancestor) { |
| 843 | return ClampingScrollPhysics(parent: buildParent(ancestor)); |
| 844 | } |
| 845 | |
| 846 | @override |
| 847 | double applyBoundaryConditions(ScrollMetrics position, double value) { |
| 848 | assert(() { |
| 849 | if (value == position.pixels) { |
| 850 | throw FlutterError.fromParts(<DiagnosticsNode>[ |
| 851 | ErrorSummary(' $runtimeType.applyBoundaryConditions() was called redundantly.' ), |
| 852 | ErrorDescription( |
| 853 | 'The proposed new position, $value, is exactly equal to the current position of the ' |
| 854 | 'given ${position.runtimeType}, ${position.pixels}.\n' |
| 855 | 'The applyBoundaryConditions method should only be called when the value is ' |
| 856 | 'going to actually change the pixels, otherwise it is redundant.' , |
| 857 | ), |
| 858 | DiagnosticsProperty<ScrollPhysics>( |
| 859 | 'The physics object in question was' , |
| 860 | this, |
| 861 | style: DiagnosticsTreeStyle.errorProperty, |
| 862 | ), |
| 863 | DiagnosticsProperty<ScrollMetrics>( |
| 864 | 'The position object in question was' , |
| 865 | position, |
| 866 | style: DiagnosticsTreeStyle.errorProperty, |
| 867 | ), |
| 868 | ]); |
| 869 | } |
| 870 | return true; |
| 871 | }()); |
| 872 | if (value < position.pixels && position.pixels <= position.minScrollExtent) { |
| 873 | // Underscroll. |
| 874 | return value - position.pixels; |
| 875 | } |
| 876 | if (position.maxScrollExtent <= position.pixels && position.pixels < value) { |
| 877 | // Overscroll. |
| 878 | return value - position.pixels; |
| 879 | } |
| 880 | if (value < position.minScrollExtent && position.minScrollExtent < position.pixels) { |
| 881 | // Hit top edge. |
| 882 | return value - position.minScrollExtent; |
| 883 | } |
| 884 | if (position.pixels < position.maxScrollExtent && position.maxScrollExtent < value) { |
| 885 | // Hit bottom edge. |
| 886 | return value - position.maxScrollExtent; |
| 887 | } |
| 888 | return 0.0; |
| 889 | } |
| 890 | |
| 891 | @override |
| 892 | Simulation? createBallisticSimulation(ScrollMetrics position, double velocity) { |
| 893 | final Tolerance tolerance = toleranceFor(position); |
| 894 | if (position.outOfRange) { |
| 895 | double? end; |
| 896 | if (position.pixels > position.maxScrollExtent) { |
| 897 | end = position.maxScrollExtent; |
| 898 | } |
| 899 | if (position.pixels < position.minScrollExtent) { |
| 900 | end = position.minScrollExtent; |
| 901 | } |
| 902 | assert(end != null); |
| 903 | return ScrollSpringSimulation( |
| 904 | spring, |
| 905 | position.pixels, |
| 906 | end!, |
| 907 | math.min(0.0, velocity), |
| 908 | tolerance: tolerance, |
| 909 | ); |
| 910 | } |
| 911 | if (velocity.abs() < tolerance.velocity) { |
| 912 | return null; |
| 913 | } |
| 914 | if (velocity > 0.0 && position.pixels >= position.maxScrollExtent) { |
| 915 | return null; |
| 916 | } |
| 917 | if (velocity < 0.0 && position.pixels <= position.minScrollExtent) { |
| 918 | return null; |
| 919 | } |
| 920 | return ClampingScrollSimulation( |
| 921 | position: position.pixels, |
| 922 | velocity: velocity, |
| 923 | tolerance: tolerance, |
| 924 | ); |
| 925 | } |
| 926 | } |
| 927 | |
| 928 | /// Scroll physics that always lets the user scroll. |
| 929 | /// |
| 930 | /// This overrides the default behavior which is to disable scrolling |
| 931 | /// when there is no content to scroll. It does not override the |
| 932 | /// handling of overscrolling. |
| 933 | /// |
| 934 | /// On Android, overscrolls will be clamped by default and result in an |
| 935 | /// overscroll glow. On iOS, overscrolls will load a spring that will return the |
| 936 | /// scroll view to its normal range when released. |
| 937 | /// |
| 938 | /// See also: |
| 939 | /// |
| 940 | /// * [ScrollPhysics], which can be used instead of this class when the default |
| 941 | /// behavior is desired instead. |
| 942 | /// * [BouncingScrollPhysics], which provides the bouncing overscroll behavior |
| 943 | /// found on iOS. |
| 944 | /// * [ClampingScrollPhysics], which provides the clamping overscroll behavior |
| 945 | /// found on Android. |
| 946 | class AlwaysScrollableScrollPhysics extends ScrollPhysics { |
| 947 | /// Creates scroll physics that always lets the user scroll. |
| 948 | const AlwaysScrollableScrollPhysics({super.parent}); |
| 949 | |
| 950 | @override |
| 951 | AlwaysScrollableScrollPhysics applyTo(ScrollPhysics? ancestor) { |
| 952 | return AlwaysScrollableScrollPhysics(parent: buildParent(ancestor)); |
| 953 | } |
| 954 | |
| 955 | @override |
| 956 | bool shouldAcceptUserOffset(ScrollMetrics position) => true; |
| 957 | } |
| 958 | |
| 959 | /// Scroll physics that does not allow the user to scroll. |
| 960 | /// |
| 961 | /// See also: |
| 962 | /// |
| 963 | /// * [ScrollPhysics], which can be used instead of this class when the default |
| 964 | /// behavior is desired instead. |
| 965 | /// * [BouncingScrollPhysics], which provides the bouncing overscroll behavior |
| 966 | /// found on iOS. |
| 967 | /// * [ClampingScrollPhysics], which provides the clamping overscroll behavior |
| 968 | /// found on Android. |
| 969 | class NeverScrollableScrollPhysics extends ScrollPhysics { |
| 970 | /// Creates scroll physics that does not let the user scroll. |
| 971 | const NeverScrollableScrollPhysics({super.parent}); |
| 972 | |
| 973 | @override |
| 974 | NeverScrollableScrollPhysics applyTo(ScrollPhysics? ancestor) { |
| 975 | return NeverScrollableScrollPhysics(parent: buildParent(ancestor)); |
| 976 | } |
| 977 | |
| 978 | @override |
| 979 | bool get allowUserScrolling => false; |
| 980 | |
| 981 | @override |
| 982 | bool get allowImplicitScrolling => false; |
| 983 | } |
| 984 | |