| 1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
| 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | /// @docImport 'package:flutter/material.dart'; |
| 6 | /// |
| 7 | /// @docImport 'page_view.dart'; |
| 8 | /// @docImport 'scroll_controller.dart'; |
| 9 | /// @docImport 'scroll_notification_observer.dart'; |
| 10 | /// @docImport 'scroll_position_with_single_context.dart'; |
| 11 | /// @docImport 'scroll_view.dart'; |
| 12 | /// @docImport 'scrollable.dart'; |
| 13 | /// @docImport 'viewport.dart'; |
| 14 | library; |
| 15 | |
| 16 | import 'dart:async'; |
| 17 | |
| 18 | import 'package:flutter/foundation.dart'; |
| 19 | import 'package:flutter/gestures.dart'; |
| 20 | import 'package:flutter/physics.dart'; |
| 21 | import 'package:flutter/rendering.dart'; |
| 22 | import 'package:flutter/scheduler.dart'; |
| 23 | |
| 24 | import 'basic.dart'; |
| 25 | import 'framework.dart'; |
| 26 | import 'notification_listener.dart'; |
| 27 | import 'page_storage.dart'; |
| 28 | import 'scroll_activity.dart'; |
| 29 | import 'scroll_context.dart'; |
| 30 | import 'scroll_metrics.dart'; |
| 31 | import 'scroll_notification.dart'; |
| 32 | import 'scroll_physics.dart'; |
| 33 | |
| 34 | export 'scroll_activity.dart' show ScrollHoldController; |
| 35 | |
| 36 | /// The policy to use when applying the `alignment` parameter of |
| 37 | /// [ScrollPosition.ensureVisible]. |
| 38 | enum ScrollPositionAlignmentPolicy { |
| 39 | /// Use the `alignment` property of [ScrollPosition.ensureVisible] to decide |
| 40 | /// where to align the visible object. |
| 41 | explicit, |
| 42 | |
| 43 | /// Find the bottom edge of the scroll container, and scroll the container, if |
| 44 | /// necessary, to show the bottom of the object. |
| 45 | /// |
| 46 | /// For example, find the bottom edge of the scroll container. If the bottom |
| 47 | /// edge of the item is below the bottom edge of the scroll container, scroll |
| 48 | /// the item so that the bottom of the item is just visible. If the entire |
| 49 | /// item is already visible, then do nothing. |
| 50 | keepVisibleAtEnd, |
| 51 | |
| 52 | /// Find the top edge of the scroll container, and scroll the container if |
| 53 | /// necessary to show the top of the object. |
| 54 | /// |
| 55 | /// For example, find the top edge of the scroll container. If the top edge of |
| 56 | /// the item is above the top edge of the scroll container, scroll the item so |
| 57 | /// that the top of the item is just visible. If the entire item is already |
| 58 | /// visible, then do nothing. |
| 59 | keepVisibleAtStart, |
| 60 | } |
| 61 | |
| 62 | /// Determines which portion of the content is visible in a scroll view. |
| 63 | /// |
| 64 | /// The [pixels] value determines the scroll offset that the scroll view uses to |
| 65 | /// select which part of its content to display. As the user scrolls the |
| 66 | /// viewport, this value changes, which changes the content that is displayed. |
| 67 | /// |
| 68 | /// The [ScrollPosition] applies [physics] to scrolling, and stores the |
| 69 | /// [minScrollExtent] and [maxScrollExtent]. |
| 70 | /// |
| 71 | /// Scrolling is controlled by the current [activity], which is set by |
| 72 | /// [beginActivity]. [ScrollPosition] itself does not start any activities. |
| 73 | /// Instead, concrete subclasses, such as [ScrollPositionWithSingleContext], |
| 74 | /// typically start activities in response to user input or instructions from a |
| 75 | /// [ScrollController]. |
| 76 | /// |
| 77 | /// This object is a [Listenable] that notifies its listeners when [pixels] |
| 78 | /// changes. |
| 79 | /// |
| 80 | /// {@template flutter.widgets.scrollPosition.listening} |
| 81 | /// ### Accessing Scrolling Information |
| 82 | /// |
| 83 | /// There are several ways to acquire information about scrolling and |
| 84 | /// scrollable widgets, but each provides different types of information about |
| 85 | /// the scrolling activity, the position, and the dimensions of the [Viewport]. |
| 86 | /// |
| 87 | /// A [ScrollController] is a [Listenable]. It notifies its listeners whenever |
| 88 | /// any of the attached [ScrollPosition]s notify _their_ listeners, such as when |
| 89 | /// scrolling occurs. This is very similar to using a [NotificationListener] of |
| 90 | /// type [ScrollNotification] to listen to changes in the scroll position, with |
| 91 | /// the difference being that a notification listener will provide information |
| 92 | /// about the scrolling activity. A notification listener can further listen to |
| 93 | /// specific subclasses of [ScrollNotification], like [UserScrollNotification]. |
| 94 | /// |
| 95 | /// {@tool dartpad} |
| 96 | /// This sample shows the difference between using a [ScrollController] or a |
| 97 | /// [NotificationListener] of type [ScrollNotification] to listen to scrolling |
| 98 | /// activities. Toggling the [Radio] button switches between the two. |
| 99 | /// Using a [ScrollNotification] will provide details about the scrolling |
| 100 | /// activity, along with the metrics of the [ScrollPosition], but not the scroll |
| 101 | /// position object itself. By listening with a [ScrollController], the position |
| 102 | /// object is directly accessible. |
| 103 | /// Both of these types of notifications are only triggered by scrolling. |
| 104 | /// |
| 105 | /// ** See code in examples/api/lib/widgets/scroll_position/scroll_controller_notification.0.dart ** |
| 106 | /// {@end-tool} |
| 107 | /// |
| 108 | /// [ScrollController] does not notify its listeners when the list of |
| 109 | /// [ScrollPosition]s attached to the scroll controller changes. To listen to |
| 110 | /// the attaching and detaching of scroll positions to the controller, use the |
| 111 | /// [ScrollController.onAttach] and [ScrollController.onDetach] methods. This is |
| 112 | /// also useful for adding a listener to the |
| 113 | /// [ScrollPosition.isScrollingNotifier] when the position is created during the |
| 114 | /// build method of the [Scrollable]. |
| 115 | /// |
| 116 | /// At the time that a scroll position is attached, the [ScrollMetrics], such as |
| 117 | /// the [ScrollMetrics.maxScrollExtent], are not yet available. These are not |
| 118 | /// determined until the [Scrollable] has finished laying out its contents and |
| 119 | /// computing things like the full extent of that content. |
| 120 | /// [ScrollPosition.hasContentDimensions] can be used to know when the |
| 121 | /// metrics are available, or a [ScrollMetricsNotification] can be used, |
| 122 | /// discussed further below. |
| 123 | /// |
| 124 | /// {@tool dartpad} |
| 125 | /// This sample shows how to apply a listener to the |
| 126 | /// [ScrollPosition.isScrollingNotifier] using [ScrollController.onAttach]. |
| 127 | /// This is used to change the [AppBar]'s color when scrolling is occurring. |
| 128 | /// |
| 129 | /// ** See code in examples/api/lib/widgets/scroll_position/scroll_controller_on_attach.0.dart ** |
| 130 | /// {@end-tool} |
| 131 | /// |
| 132 | /// #### From a different context |
| 133 | /// |
| 134 | /// When needing to access scrolling information from a context that is within |
| 135 | /// the scrolling widget itself, use [Scrollable.of] to access the |
| 136 | /// [ScrollableState] and the [ScrollableState.position]. This would be the same |
| 137 | /// [ScrollPosition] attached to a [ScrollController]. |
| 138 | /// |
| 139 | /// When needing to access scrolling information from a context that is not an |
| 140 | /// ancestor of the scrolling widget, use [ScrollNotificationObserver]. This is |
| 141 | /// used by [AppBar] to create the scrolled under effect. Since [Scaffold.appBar] |
| 142 | /// is a separate subtree from the [Scaffold.body], scroll notifications would |
| 143 | /// not bubble up to the app bar. Use |
| 144 | /// [ScrollNotificationObserverState.addListener] to listen to scroll |
| 145 | /// notifications happening outside of the current context. |
| 146 | /// |
| 147 | /// #### Dimension changes |
| 148 | /// |
| 149 | /// Lastly, listening to a [ScrollController] or a [ScrollPosition] will |
| 150 | /// _not_ notify when the [ScrollMetrics] of a given scroll position changes, |
| 151 | /// such as when the window is resized, changing the dimensions of the |
| 152 | /// [Viewport] and the previously mentioned extents of the scrollable. In order |
| 153 | /// to listen to changes in scroll metrics, use a [NotificationListener] of type |
| 154 | /// [ScrollMetricsNotification]. This type of notification differs from |
| 155 | /// [ScrollNotification], as it is not associated with the activity of |
| 156 | /// scrolling, but rather the dimensions of the scrollable area, such as the |
| 157 | /// window size. |
| 158 | /// |
| 159 | /// {@tool dartpad} |
| 160 | /// This sample shows how a [ScrollMetricsNotification] is dispatched when |
| 161 | /// the `windowSize` is changed. Press the floating action button to increase |
| 162 | /// the scrollable window's size. |
| 163 | /// |
| 164 | /// ** See code in examples/api/lib/widgets/scroll_position/scroll_metrics_notification.0.dart ** |
| 165 | /// {@end-tool} |
| 166 | /// {@endtemplate} |
| 167 | /// |
| 168 | /// ## Subclassing ScrollPosition |
| 169 | /// |
| 170 | /// Over time, a [Scrollable] might have many different [ScrollPosition] |
| 171 | /// objects. For example, if [Scrollable.physics] changes type, [Scrollable] |
| 172 | /// creates a new [ScrollPosition] with the new physics. To transfer state from |
| 173 | /// the old instance to the new instance, subclasses implement [absorb]. See |
| 174 | /// [absorb] for more details. |
| 175 | /// |
| 176 | /// Subclasses also need to call [didUpdateScrollDirection] whenever |
| 177 | /// [userScrollDirection] changes values. |
| 178 | /// |
| 179 | /// See also: |
| 180 | /// |
| 181 | /// * [Scrollable], which uses a [ScrollPosition] to determine which portion of |
| 182 | /// its content to display. |
| 183 | /// * [ScrollController], which can be used with [ListView], [GridView] and |
| 184 | /// other scrollable widgets to control a [ScrollPosition]. |
| 185 | /// * [ScrollPositionWithSingleContext], which is the most commonly used |
| 186 | /// concrete subclass of [ScrollPosition]. |
| 187 | /// * [ScrollNotification] and [NotificationListener], which can be used to watch |
| 188 | /// the scroll position without using a [ScrollController]. |
| 189 | abstract class ScrollPosition extends ViewportOffset with ScrollMetrics { |
| 190 | /// Creates an object that determines which portion of the content is visible |
| 191 | /// in a scroll view. |
| 192 | ScrollPosition({ |
| 193 | required this.physics, |
| 194 | required this.context, |
| 195 | this.keepScrollOffset = true, |
| 196 | ScrollPosition? oldPosition, |
| 197 | this.debugLabel, |
| 198 | }) { |
| 199 | if (oldPosition != null) { |
| 200 | absorb(oldPosition); |
| 201 | } |
| 202 | if (keepScrollOffset) { |
| 203 | restoreScrollOffset(); |
| 204 | } |
| 205 | } |
| 206 | |
| 207 | /// How the scroll position should respond to user input. |
| 208 | /// |
| 209 | /// For example, determines how the widget continues to animate after the |
| 210 | /// user stops dragging the scroll view. |
| 211 | final ScrollPhysics physics; |
| 212 | |
| 213 | /// Where the scrolling is taking place. |
| 214 | /// |
| 215 | /// Typically implemented by [ScrollableState]. |
| 216 | final ScrollContext context; |
| 217 | |
| 218 | /// Save the current scroll offset with [PageStorage] and restore it if |
| 219 | /// this scroll position's scrollable is recreated. |
| 220 | /// |
| 221 | /// See also: |
| 222 | /// |
| 223 | /// * [ScrollController.keepScrollOffset] and [PageController.keepPage], which |
| 224 | /// create scroll positions and initialize this property. |
| 225 | // TODO(goderbauer): Deprecate this when state restoration supports all features of PageStorage. |
| 226 | final bool keepScrollOffset; |
| 227 | |
| 228 | /// A label that is used in the [toString] output. |
| 229 | /// |
| 230 | /// Intended to aid with identifying animation controller instances in debug |
| 231 | /// output. |
| 232 | final String? debugLabel; |
| 233 | |
| 234 | @override |
| 235 | double get minScrollExtent => _minScrollExtent!; |
| 236 | double? _minScrollExtent; |
| 237 | |
| 238 | @override |
| 239 | double get maxScrollExtent => _maxScrollExtent!; |
| 240 | double? _maxScrollExtent; |
| 241 | |
| 242 | @override |
| 243 | bool get hasContentDimensions => _minScrollExtent != null && _maxScrollExtent != null; |
| 244 | |
| 245 | /// The additional velocity added for a [forcePixels] change in a single |
| 246 | /// frame. |
| 247 | /// |
| 248 | /// This value is used by [recommendDeferredLoading] in addition to the |
| 249 | /// [activity]'s [ScrollActivity.velocity] to ask the [physics] whether or |
| 250 | /// not to defer loading. It accounts for the fact that a [forcePixels] call |
| 251 | /// may involve a [ScrollActivity] with 0 velocity, but the scrollable is |
| 252 | /// still instantaneously moving from its current position to a potentially |
| 253 | /// very far position, and which is of interest to callers of |
| 254 | /// [recommendDeferredLoading]. |
| 255 | /// |
| 256 | /// For example, if a scrollable is currently at 5000 pixels, and we [jumpTo] |
| 257 | /// 0 to get back to the top of the list, we would have an implied velocity of |
| 258 | /// -5000 and an `activity.velocity` of 0. The jump may be going past a |
| 259 | /// number of resource intensive widgets which should avoid doing work if the |
| 260 | /// position jumps past them. |
| 261 | double _impliedVelocity = 0; |
| 262 | |
| 263 | @override |
| 264 | double get pixels => _pixels!; |
| 265 | double? _pixels; |
| 266 | |
| 267 | @override |
| 268 | bool get hasPixels => _pixels != null; |
| 269 | |
| 270 | @override |
| 271 | double get viewportDimension => _viewportDimension!; |
| 272 | double? _viewportDimension; |
| 273 | |
| 274 | @override |
| 275 | bool get hasViewportDimension => _viewportDimension != null; |
| 276 | |
| 277 | /// Whether [viewportDimension], [minScrollExtent], [maxScrollExtent], |
| 278 | /// [outOfRange], and [atEdge] are available. |
| 279 | /// |
| 280 | /// Set to true just before the first time [applyNewDimensions] is called. |
| 281 | bool get haveDimensions => _haveDimensions; |
| 282 | bool _haveDimensions = false; |
| 283 | |
| 284 | /// Whether scrollables should absorb pointer events at this position. |
| 285 | /// |
| 286 | /// This is value relates to the current [ScrollActivity], which determines |
| 287 | /// if additional touch input should be received by the scroll view or its children. |
| 288 | /// If the position is overscrolled, as is allowed by [BouncingScrollPhysics], |
| 289 | /// children of the scroll view will receive pointer events as the scroll view |
| 290 | /// settles back from the overscrolled state. |
| 291 | bool get shouldIgnorePointer => !outOfRange && (activity?.shouldIgnorePointer ?? true); |
| 292 | |
| 293 | /// Take any current applicable state from the given [ScrollPosition]. |
| 294 | /// |
| 295 | /// This method is called by the constructor if it is given an `oldPosition`. |
| 296 | /// The `other` argument might not have the same [runtimeType] as this object. |
| 297 | /// |
| 298 | /// This method can be destructive to the other [ScrollPosition]. The other |
| 299 | /// object must be disposed immediately after this call (in the same call |
| 300 | /// stack, before microtask resolution, by whomever called this object's |
| 301 | /// constructor). |
| 302 | /// |
| 303 | /// If the old [ScrollPosition] object is a different [runtimeType] than this |
| 304 | /// one, the [ScrollActivity.resetActivity] method is invoked on the newly |
| 305 | /// adopted [ScrollActivity]. |
| 306 | /// |
| 307 | /// ## Overriding |
| 308 | /// |
| 309 | /// Overrides of this method must call `super.absorb` after setting any |
| 310 | /// metrics-related or activity-related state, since this method may restart |
| 311 | /// the activity and scroll activities tend to use those metrics when being |
| 312 | /// restarted. |
| 313 | /// |
| 314 | /// Overrides of this method might need to start an [IdleScrollActivity] if |
| 315 | /// they are unable to absorb the activity from the other [ScrollPosition]. |
| 316 | /// |
| 317 | /// Overrides of this method might also need to update the delegates of |
| 318 | /// absorbed scroll activities if they use themselves as a |
| 319 | /// [ScrollActivityDelegate]. |
| 320 | @protected |
| 321 | @mustCallSuper |
| 322 | void absorb(ScrollPosition other) { |
| 323 | assert(other.context == context); |
| 324 | assert(_pixels == null); |
| 325 | if (other.hasContentDimensions) { |
| 326 | _minScrollExtent = other.minScrollExtent; |
| 327 | _maxScrollExtent = other.maxScrollExtent; |
| 328 | } |
| 329 | if (other.hasPixels) { |
| 330 | _pixels = other.pixels; |
| 331 | } |
| 332 | if (other.hasViewportDimension) { |
| 333 | _viewportDimension = other.viewportDimension; |
| 334 | } |
| 335 | |
| 336 | assert(activity == null); |
| 337 | assert(other.activity != null); |
| 338 | _activity = other.activity; |
| 339 | other._activity = null; |
| 340 | if (other.runtimeType != runtimeType) { |
| 341 | activity!.resetActivity(); |
| 342 | } |
| 343 | context.setIgnorePointer(activity!.shouldIgnorePointer); |
| 344 | isScrollingNotifier.value = activity!.isScrolling; |
| 345 | } |
| 346 | |
| 347 | @override |
| 348 | double get devicePixelRatio => context.devicePixelRatio; |
| 349 | |
| 350 | /// Update the scroll position ([pixels]) to a given pixel value. |
| 351 | /// |
| 352 | /// This should only be called by the current [ScrollActivity], either during |
| 353 | /// the transient callback phase or in response to user input. |
| 354 | /// |
| 355 | /// Returns the overscroll, if any. If the return value is 0.0, that means |
| 356 | /// that [pixels] now returns the given `value`. If the return value is |
| 357 | /// positive, then [pixels] is less than the requested `value` by the given |
| 358 | /// amount (overscroll past the max extent), and if it is negative, it is |
| 359 | /// greater than the requested `value` by the given amount (underscroll past |
| 360 | /// the min extent). |
| 361 | /// |
| 362 | /// The amount of overscroll is computed by [applyBoundaryConditions]. |
| 363 | /// |
| 364 | /// The amount of the change that is applied is reported using [didUpdateScrollPositionBy]. |
| 365 | /// If there is any overscroll, it is reported using [didOverscrollBy]. |
| 366 | double setPixels(double newPixels) { |
| 367 | assert(hasPixels); |
| 368 | assert( |
| 369 | SchedulerBinding.instance.schedulerPhase != SchedulerPhase.persistentCallbacks, |
| 370 | "A scrollable's position should not change during the build, layout, and paint phases, otherwise the rendering will be confused." , |
| 371 | ); |
| 372 | if (newPixels != pixels) { |
| 373 | final double overscroll = applyBoundaryConditions(newPixels); |
| 374 | assert(() { |
| 375 | final double delta = newPixels - pixels; |
| 376 | if (overscroll.abs() > delta.abs()) { |
| 377 | throw FlutterError( |
| 378 | ' $runtimeType.applyBoundaryConditions returned invalid overscroll value.\n' |
| 379 | 'setPixels() was called to change the scroll offset from $pixels to $newPixels.\n' |
| 380 | 'That is a delta of $delta units.\n' |
| 381 | ' $runtimeType.applyBoundaryConditions reported an overscroll of $overscroll units.' , |
| 382 | ); |
| 383 | } |
| 384 | return true; |
| 385 | }()); |
| 386 | final double oldPixels = pixels; |
| 387 | _pixels = newPixels - overscroll; |
| 388 | if (_pixels != oldPixels) { |
| 389 | if (outOfRange) { |
| 390 | context.setIgnorePointer(false); |
| 391 | } |
| 392 | notifyListeners(); |
| 393 | didUpdateScrollPositionBy(pixels - oldPixels); |
| 394 | } |
| 395 | if (overscroll.abs() > precisionErrorTolerance) { |
| 396 | didOverscrollBy(overscroll); |
| 397 | return overscroll; |
| 398 | } |
| 399 | } |
| 400 | return 0.0; |
| 401 | } |
| 402 | |
| 403 | /// Change the value of [pixels] to the new value, without notifying any |
| 404 | /// customers. |
| 405 | /// |
| 406 | /// This is used to adjust the position while doing layout. In particular, |
| 407 | /// this is typically called as a response to [applyViewportDimension] or |
| 408 | /// [applyContentDimensions] (in both cases, if this method is called, those |
| 409 | /// methods should then return false to indicate that the position has been |
| 410 | /// adjusted). |
| 411 | /// |
| 412 | /// Calling this is rarely correct in other contexts. It will not immediately |
| 413 | /// cause the rendering to change, since it does not notify the widgets or |
| 414 | /// render objects that might be listening to this object: they will only |
| 415 | /// change when they next read the value, which could be arbitrarily later. It |
| 416 | /// is generally only appropriate in the very specific case of the value being |
| 417 | /// corrected during layout (since then the value is immediately read), in the |
| 418 | /// specific case of a [ScrollPosition] with a single viewport customer. |
| 419 | /// |
| 420 | /// To cause the position to jump or animate to a new value, consider [jumpTo] |
| 421 | /// or [animateTo], which will honor the normal conventions for changing the |
| 422 | /// scroll offset. |
| 423 | /// |
| 424 | /// To force the [pixels] to a particular value without honoring the normal |
| 425 | /// conventions for changing the scroll offset, consider [forcePixels]. (But |
| 426 | /// see the discussion there for why that might still be a bad idea.) |
| 427 | /// |
| 428 | /// See also: |
| 429 | /// |
| 430 | /// * [correctBy], which is a method of [ViewportOffset] used |
| 431 | /// by viewport render objects to correct the offset during layout |
| 432 | /// without notifying its listeners. |
| 433 | /// * [jumpTo], for making changes to position while not in the |
| 434 | /// middle of layout and applying the new position immediately. |
| 435 | /// * [animateTo], which is like [jumpTo] but animating to the |
| 436 | /// destination offset. |
| 437 | // ignore: use_setters_to_change_properties, (API is intended to discourage setting value) |
| 438 | void correctPixels(double value) { |
| 439 | _pixels = value; |
| 440 | } |
| 441 | |
| 442 | /// Apply a layout-time correction to the scroll offset. |
| 443 | /// |
| 444 | /// This method should change the [pixels] value by `correction`, but without |
| 445 | /// calling [notifyListeners]. It is called during layout by the |
| 446 | /// [RenderViewport], before [applyContentDimensions]. After this method is |
| 447 | /// called, the layout will be recomputed and that may result in this method |
| 448 | /// being called again, though this should be very rare. |
| 449 | /// |
| 450 | /// See also: |
| 451 | /// |
| 452 | /// * [jumpTo], for also changing the scroll position when not in layout. |
| 453 | /// [jumpTo] applies the change immediately and notifies its listeners. |
| 454 | /// * [correctPixels], which is used by the [ScrollPosition] itself to |
| 455 | /// set the offset initially during construction or after |
| 456 | /// [applyViewportDimension] or [applyContentDimensions] is called. |
| 457 | @override |
| 458 | void correctBy(double correction) { |
| 459 | assert( |
| 460 | hasPixels, |
| 461 | 'An initial pixels value must exist by calling correctPixels on the ScrollPosition' , |
| 462 | ); |
| 463 | _pixels = _pixels! + correction; |
| 464 | _didChangeViewportDimensionOrReceiveCorrection = true; |
| 465 | } |
| 466 | |
| 467 | /// Change the value of [pixels] to the new value, and notify any customers, |
| 468 | /// but without honoring normal conventions for changing the scroll offset. |
| 469 | /// |
| 470 | /// This is used to implement [jumpTo]. It can also be used adjust the |
| 471 | /// position when the dimensions of the viewport change. It should only be |
| 472 | /// used when manually implementing the logic for honoring the relevant |
| 473 | /// conventions of the class. For example, [ScrollPositionWithSingleContext] |
| 474 | /// introduces [ScrollActivity] objects and uses [forcePixels] in conjunction |
| 475 | /// with adjusting the activity, e.g. by calling |
| 476 | /// [ScrollPositionWithSingleContext.goIdle], so that the activity does |
| 477 | /// not immediately set the value back. (Consider, for instance, a case where |
| 478 | /// one is using a [DrivenScrollActivity]. That object will ignore any calls |
| 479 | /// to [forcePixels], which would result in the rendering stuttering: changing |
| 480 | /// in response to [forcePixels], and then changing back to the next value |
| 481 | /// derived from the animation.) |
| 482 | /// |
| 483 | /// To cause the position to jump or animate to a new value, consider [jumpTo] |
| 484 | /// or [animateTo]. |
| 485 | /// |
| 486 | /// This should not be called during layout (e.g. when setting the initial |
| 487 | /// scroll offset). Consider [correctPixels] if you find you need to adjust |
| 488 | /// the position during layout. |
| 489 | @protected |
| 490 | void forcePixels(double value) { |
| 491 | assert(hasPixels); |
| 492 | _impliedVelocity = value - pixels; |
| 493 | _pixels = value; |
| 494 | notifyListeners(); |
| 495 | SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) { |
| 496 | _impliedVelocity = 0; |
| 497 | }, debugLabel: 'ScrollPosition.resetVelocity' ); |
| 498 | } |
| 499 | |
| 500 | /// Called whenever scrolling ends, to store the current scroll offset in a |
| 501 | /// storage mechanism with a lifetime that matches the app's lifetime. |
| 502 | /// |
| 503 | /// The stored value will be used by [restoreScrollOffset] when the |
| 504 | /// [ScrollPosition] is recreated, in the case of the [Scrollable] being |
| 505 | /// disposed then recreated in the same session. This might happen, for |
| 506 | /// instance, if a [ListView] is on one of the pages inside a [TabBarView], |
| 507 | /// and that page is displayed, then hidden, then displayed again. |
| 508 | /// |
| 509 | /// The default implementation writes the [pixels] using the nearest |
| 510 | /// [PageStorage] found from the [context]'s [ScrollContext.storageContext] |
| 511 | /// property. |
| 512 | // TODO(goderbauer): Deprecate this when state restoration supports all features of PageStorage. |
| 513 | @protected |
| 514 | void saveScrollOffset() { |
| 515 | PageStorage.maybeOf(context.storageContext)?.writeState(context.storageContext, pixels); |
| 516 | } |
| 517 | |
| 518 | /// Called whenever the [ScrollPosition] is created, to restore the scroll |
| 519 | /// offset if possible. |
| 520 | /// |
| 521 | /// The value is stored by [saveScrollOffset] when the scroll position |
| 522 | /// changes, so that it can be restored in the case of the [Scrollable] being |
| 523 | /// disposed then recreated in the same session. This might happen, for |
| 524 | /// instance, if a [ListView] is on one of the pages inside a [TabBarView], |
| 525 | /// and that page is displayed, then hidden, then displayed again. |
| 526 | /// |
| 527 | /// The default implementation reads the value from the nearest [PageStorage] |
| 528 | /// found from the [context]'s [ScrollContext.storageContext] property, and |
| 529 | /// sets it using [correctPixels], if [pixels] is still null. |
| 530 | /// |
| 531 | /// This method is called from the constructor, so layout has not yet |
| 532 | /// occurred, and the viewport dimensions aren't yet known when it is called. |
| 533 | // TODO(goderbauer): Deprecate this when state restoration supports all features of PageStorage. |
| 534 | @protected |
| 535 | void restoreScrollOffset() { |
| 536 | if (!hasPixels) { |
| 537 | final double? value = |
| 538 | PageStorage.maybeOf(context.storageContext)?.readState(context.storageContext) as double?; |
| 539 | if (value != null) { |
| 540 | correctPixels(value); |
| 541 | } |
| 542 | } |
| 543 | } |
| 544 | |
| 545 | /// Called by [context] to restore the scroll offset to the provided value. |
| 546 | /// |
| 547 | /// The provided value has previously been provided to the [context] by |
| 548 | /// calling [ScrollContext.saveOffset], e.g. from [saveOffset]. |
| 549 | /// |
| 550 | /// This method may be called right after the scroll position is created |
| 551 | /// before layout has occurred. In that case, `initialRestore` is set to true |
| 552 | /// and the viewport dimensions will not be known yet. If the [context] |
| 553 | /// doesn't have any information to restore the scroll offset this method is |
| 554 | /// not called. |
| 555 | /// |
| 556 | /// The method may be called multiple times in the lifecycle of a |
| 557 | /// [ScrollPosition] to restore it to different scroll offsets. |
| 558 | void restoreOffset(double offset, {bool initialRestore = false}) { |
| 559 | if (initialRestore) { |
| 560 | correctPixels(offset); |
| 561 | } else { |
| 562 | jumpTo(offset); |
| 563 | } |
| 564 | } |
| 565 | |
| 566 | /// Called whenever scrolling ends, to persist the current scroll offset for |
| 567 | /// state restoration purposes. |
| 568 | /// |
| 569 | /// The default implementation stores the current value of [pixels] on the |
| 570 | /// [context] by calling [ScrollContext.saveOffset]. At a later point in time |
| 571 | /// or after the application restarts, the [context] may restore the scroll |
| 572 | /// position to the persisted offset by calling [restoreOffset]. |
| 573 | @protected |
| 574 | void saveOffset() { |
| 575 | assert(hasPixels); |
| 576 | context.saveOffset(pixels); |
| 577 | } |
| 578 | |
| 579 | /// Returns the overscroll by applying the boundary conditions. |
| 580 | /// |
| 581 | /// If the given value is in bounds, returns 0.0. Otherwise, returns the |
| 582 | /// amount of value that cannot be applied to [pixels] as a result of the |
| 583 | /// boundary conditions. If the [physics] allow out-of-bounds scrolling, this |
| 584 | /// method always returns 0.0. |
| 585 | /// |
| 586 | /// The default implementation defers to the [physics] object's |
| 587 | /// [ScrollPhysics.applyBoundaryConditions]. |
| 588 | @protected |
| 589 | double applyBoundaryConditions(double value) { |
| 590 | final double result = physics.applyBoundaryConditions(this, value); |
| 591 | assert(() { |
| 592 | final double delta = value - pixels; |
| 593 | if (result.abs() > delta.abs()) { |
| 594 | throw FlutterError( |
| 595 | ' ${physics.runtimeType}.applyBoundaryConditions returned invalid overscroll value.\n' |
| 596 | 'The method was called to consider a change from $pixels to $value, which is a ' |
| 597 | 'delta of ${delta.toStringAsFixed(1)} units. However, it returned an overscroll of ' |
| 598 | ' ${result.toStringAsFixed(1)} units, which has a greater magnitude than the delta. ' |
| 599 | 'The applyBoundaryConditions method is only supposed to reduce the possible range ' |
| 600 | 'of movement, not increase it.\n' |
| 601 | 'The scroll extents are $minScrollExtent .. $maxScrollExtent, and the ' |
| 602 | 'viewport dimension is $viewportDimension.' , |
| 603 | ); |
| 604 | } |
| 605 | return true; |
| 606 | }()); |
| 607 | return result; |
| 608 | } |
| 609 | |
| 610 | bool _didChangeViewportDimensionOrReceiveCorrection = true; |
| 611 | |
| 612 | @override |
| 613 | bool applyViewportDimension(double viewportDimension) { |
| 614 | if (_viewportDimension != viewportDimension) { |
| 615 | _viewportDimension = viewportDimension; |
| 616 | _didChangeViewportDimensionOrReceiveCorrection = true; |
| 617 | // If this is called, you can rely on applyContentDimensions being called |
| 618 | // soon afterwards in the same layout phase. So we put all the logic that |
| 619 | // relies on both values being computed into applyContentDimensions. |
| 620 | } |
| 621 | return true; |
| 622 | } |
| 623 | |
| 624 | bool _pendingDimensions = false; |
| 625 | ScrollMetrics? _lastMetrics; |
| 626 | // True indicates that there is a ScrollMetrics update notification pending. |
| 627 | bool _haveScheduledUpdateNotification = false; |
| 628 | Axis? _lastAxis; |
| 629 | |
| 630 | bool _isMetricsChanged() { |
| 631 | assert(haveDimensions); |
| 632 | final ScrollMetrics currentMetrics = copyWith(); |
| 633 | |
| 634 | return _lastMetrics == null || |
| 635 | !(currentMetrics.extentBefore == _lastMetrics!.extentBefore && |
| 636 | currentMetrics.extentInside == _lastMetrics!.extentInside && |
| 637 | currentMetrics.extentAfter == _lastMetrics!.extentAfter && |
| 638 | currentMetrics.axisDirection == _lastMetrics!.axisDirection); |
| 639 | } |
| 640 | |
| 641 | @override |
| 642 | bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) { |
| 643 | assert(haveDimensions == (_lastMetrics != null)); |
| 644 | if (!nearEqual(_minScrollExtent, minScrollExtent, Tolerance.defaultTolerance.distance) || |
| 645 | !nearEqual(_maxScrollExtent, maxScrollExtent, Tolerance.defaultTolerance.distance) || |
| 646 | _didChangeViewportDimensionOrReceiveCorrection || |
| 647 | _lastAxis != axis) { |
| 648 | assert(minScrollExtent <= maxScrollExtent); |
| 649 | _minScrollExtent = minScrollExtent; |
| 650 | _maxScrollExtent = maxScrollExtent; |
| 651 | _lastAxis = axis; |
| 652 | final ScrollMetrics? currentMetrics = haveDimensions ? copyWith() : null; |
| 653 | _didChangeViewportDimensionOrReceiveCorrection = false; |
| 654 | _pendingDimensions = true; |
| 655 | if (haveDimensions && !correctForNewDimensions(_lastMetrics!, currentMetrics!)) { |
| 656 | return false; |
| 657 | } |
| 658 | _haveDimensions = true; |
| 659 | } |
| 660 | assert(haveDimensions); |
| 661 | if (_pendingDimensions) { |
| 662 | applyNewDimensions(); |
| 663 | _pendingDimensions = false; |
| 664 | } |
| 665 | assert( |
| 666 | !_didChangeViewportDimensionOrReceiveCorrection, |
| 667 | 'Use correctForNewDimensions() (and return true) to change the scroll offset during applyContentDimensions().' , |
| 668 | ); |
| 669 | |
| 670 | if (_isMetricsChanged()) { |
| 671 | // It is too late to send useful notifications, because the potential |
| 672 | // listeners have, by definition, already been built this frame. To make |
| 673 | // sure the notification is sent at all, we delay it until after the frame |
| 674 | // is complete. |
| 675 | if (!_haveScheduledUpdateNotification) { |
| 676 | scheduleMicrotask(didUpdateScrollMetrics); |
| 677 | _haveScheduledUpdateNotification = true; |
| 678 | } |
| 679 | _lastMetrics = copyWith(); |
| 680 | } |
| 681 | return true; |
| 682 | } |
| 683 | |
| 684 | /// Verifies that the new content and viewport dimensions are acceptable. |
| 685 | /// |
| 686 | /// Called by [applyContentDimensions] to determine its return value. |
| 687 | /// |
| 688 | /// Should return true if the current scroll offset is correct given |
| 689 | /// the new content and viewport dimensions. |
| 690 | /// |
| 691 | /// Otherwise, should call [correctPixels] to correct the scroll |
| 692 | /// offset given the new dimensions, and then return false. |
| 693 | /// |
| 694 | /// This is only called when [haveDimensions] is true. |
| 695 | /// |
| 696 | /// The default implementation defers to [ScrollPhysics.adjustPositionForNewDimensions]. |
| 697 | @protected |
| 698 | bool correctForNewDimensions(ScrollMetrics oldPosition, ScrollMetrics newPosition) { |
| 699 | final double newPixels = physics.adjustPositionForNewDimensions( |
| 700 | oldPosition: oldPosition, |
| 701 | newPosition: newPosition, |
| 702 | isScrolling: activity!.isScrolling, |
| 703 | velocity: activity!.velocity, |
| 704 | ); |
| 705 | if (newPixels != pixels) { |
| 706 | correctPixels(newPixels); |
| 707 | return false; |
| 708 | } |
| 709 | return true; |
| 710 | } |
| 711 | |
| 712 | /// Notifies the activity that the dimensions of the underlying viewport or |
| 713 | /// contents have changed. |
| 714 | /// |
| 715 | /// Called after [applyViewportDimension] or [applyContentDimensions] have |
| 716 | /// changed the [minScrollExtent], the [maxScrollExtent], or the |
| 717 | /// [viewportDimension]. When this method is called, it should be called |
| 718 | /// _after_ any corrections are applied to [pixels] using [correctPixels], not |
| 719 | /// before. |
| 720 | /// |
| 721 | /// The default implementation informs the [activity] of the new dimensions by |
| 722 | /// calling its [ScrollActivity.applyNewDimensions] method. |
| 723 | /// |
| 724 | /// See also: |
| 725 | /// |
| 726 | /// * [applyViewportDimension], which is called when new |
| 727 | /// viewport dimensions are established. |
| 728 | /// * [applyContentDimensions], which is called after new |
| 729 | /// viewport dimensions are established, and also if new content dimensions |
| 730 | /// are established, and which calls [ScrollPosition.applyNewDimensions]. |
| 731 | @protected |
| 732 | @mustCallSuper |
| 733 | void applyNewDimensions() { |
| 734 | assert(hasPixels); |
| 735 | assert(_pendingDimensions); |
| 736 | activity!.applyNewDimensions(); |
| 737 | _updateSemanticActions(); // will potentially request a semantics update. |
| 738 | } |
| 739 | |
| 740 | Set<SemanticsAction>? _semanticActions; |
| 741 | |
| 742 | /// Called whenever the scroll position or the dimensions of the scroll view |
| 743 | /// change to schedule an update of the available semantics actions. The |
| 744 | /// actual update will be performed in the next frame. If non is pending |
| 745 | /// a frame will be scheduled. |
| 746 | /// |
| 747 | /// For example: If the scroll view has been scrolled all the way to the top, |
| 748 | /// the action to scroll further up needs to be removed as the scroll view |
| 749 | /// cannot be scrolled in that direction anymore. |
| 750 | /// |
| 751 | /// This method is potentially called twice per frame (if scroll position and |
| 752 | /// scroll view dimensions both change) and therefore shouldn't do anything |
| 753 | /// expensive. |
| 754 | void _updateSemanticActions() { |
| 755 | final (SemanticsAction forward, SemanticsAction backward) = switch (axisDirection) { |
| 756 | AxisDirection.up => (SemanticsAction.scrollDown, SemanticsAction.scrollUp), |
| 757 | AxisDirection.down => (SemanticsAction.scrollUp, SemanticsAction.scrollDown), |
| 758 | AxisDirection.left => (SemanticsAction.scrollRight, SemanticsAction.scrollLeft), |
| 759 | AxisDirection.right => (SemanticsAction.scrollLeft, SemanticsAction.scrollRight), |
| 760 | }; |
| 761 | |
| 762 | final Set<SemanticsAction> actions = <SemanticsAction>{ |
| 763 | if (pixels > minScrollExtent) backward, |
| 764 | if (pixels < maxScrollExtent) forward, |
| 765 | }; |
| 766 | |
| 767 | if (setEquals<SemanticsAction>(actions, _semanticActions)) { |
| 768 | return; |
| 769 | } |
| 770 | |
| 771 | _semanticActions = actions; |
| 772 | context.setSemanticsActions(_semanticActions!); |
| 773 | } |
| 774 | |
| 775 | ScrollPositionAlignmentPolicy _maybeFlipAlignment(ScrollPositionAlignmentPolicy alignmentPolicy) { |
| 776 | return switch (alignmentPolicy) { |
| 777 | // Don't flip when explicit. |
| 778 | ScrollPositionAlignmentPolicy.explicit => alignmentPolicy, |
| 779 | ScrollPositionAlignmentPolicy.keepVisibleAtEnd => |
| 780 | ScrollPositionAlignmentPolicy.keepVisibleAtStart, |
| 781 | ScrollPositionAlignmentPolicy.keepVisibleAtStart => |
| 782 | ScrollPositionAlignmentPolicy.keepVisibleAtEnd, |
| 783 | }; |
| 784 | } |
| 785 | |
| 786 | ScrollPositionAlignmentPolicy _applyAxisDirectionToAlignmentPolicy( |
| 787 | ScrollPositionAlignmentPolicy alignmentPolicy, |
| 788 | ) { |
| 789 | return switch (axisDirection) { |
| 790 | // Start and end alignments must account for axis direction. |
| 791 | // When focus is requested for example, it knows the directionality of the |
| 792 | // keyboard keys initiating traversal, but not the direction of the |
| 793 | // Scrollable. |
| 794 | AxisDirection.up || AxisDirection.left => _maybeFlipAlignment(alignmentPolicy), |
| 795 | AxisDirection.down || AxisDirection.right => alignmentPolicy, |
| 796 | }; |
| 797 | } |
| 798 | |
| 799 | /// Animates the position such that the given object is as visible as possible |
| 800 | /// by just scrolling this position. |
| 801 | /// |
| 802 | /// The optional `targetRenderObject` parameter is used to determine which area |
| 803 | /// of that object should be as visible as possible. If `targetRenderObject` |
| 804 | /// is null, the entire [RenderObject] (as defined by its |
| 805 | /// [RenderObject.paintBounds]) will be as visible as possible. If |
| 806 | /// `targetRenderObject` is provided, it must be a descendant of the object. |
| 807 | /// |
| 808 | /// See also: |
| 809 | /// |
| 810 | /// * [ScrollPositionAlignmentPolicy] for the way in which `alignment` is |
| 811 | /// applied, and the way the given `object` is aligned. |
| 812 | Future<void> ensureVisible( |
| 813 | RenderObject object, { |
| 814 | double alignment = 0.0, |
| 815 | Duration duration = Duration.zero, |
| 816 | Curve curve = Curves.ease, |
| 817 | ScrollPositionAlignmentPolicy alignmentPolicy = ScrollPositionAlignmentPolicy.explicit, |
| 818 | RenderObject? targetRenderObject, |
| 819 | }) async { |
| 820 | assert(object.attached); |
| 821 | final RenderAbstractViewport? viewport = RenderAbstractViewport.maybeOf(object); |
| 822 | // If no viewport is found, return. |
| 823 | if (viewport == null) { |
| 824 | return; |
| 825 | } |
| 826 | |
| 827 | Rect? targetRect; |
| 828 | if (targetRenderObject != null && targetRenderObject != object) { |
| 829 | targetRect = MatrixUtils.transformRect( |
| 830 | targetRenderObject.getTransformTo(object), |
| 831 | object.paintBounds.intersect(targetRenderObject.paintBounds), |
| 832 | ); |
| 833 | } |
| 834 | |
| 835 | double target; |
| 836 | switch (_applyAxisDirectionToAlignmentPolicy(alignmentPolicy)) { |
| 837 | case ScrollPositionAlignmentPolicy.explicit: |
| 838 | target = viewport.getOffsetToReveal(object, alignment, rect: targetRect, axis: axis).offset; |
| 839 | target = clampDouble(target, minScrollExtent, maxScrollExtent); |
| 840 | case ScrollPositionAlignmentPolicy.keepVisibleAtEnd: |
| 841 | target = viewport |
| 842 | .getOffsetToReveal( |
| 843 | object, |
| 844 | 1.0, // Aligns to end |
| 845 | rect: targetRect, |
| 846 | axis: axis, |
| 847 | ) |
| 848 | .offset; |
| 849 | target = clampDouble(target, minScrollExtent, maxScrollExtent); |
| 850 | if (target < pixels) { |
| 851 | target = pixels; |
| 852 | } |
| 853 | case ScrollPositionAlignmentPolicy.keepVisibleAtStart: |
| 854 | target = viewport |
| 855 | .getOffsetToReveal( |
| 856 | object, |
| 857 | 0.0, // Aligns to start |
| 858 | rect: targetRect, |
| 859 | axis: axis, |
| 860 | ) |
| 861 | .offset; |
| 862 | target = clampDouble(target, minScrollExtent, maxScrollExtent); |
| 863 | if (target > pixels) { |
| 864 | target = pixels; |
| 865 | } |
| 866 | } |
| 867 | |
| 868 | if (target == pixels) { |
| 869 | return; |
| 870 | } |
| 871 | |
| 872 | if (duration == Duration.zero) { |
| 873 | jumpTo(target); |
| 874 | return; |
| 875 | } |
| 876 | |
| 877 | return animateTo(target, duration: duration, curve: curve); |
| 878 | } |
| 879 | |
| 880 | /// This notifier's value is true if a scroll is underway and false if the scroll |
| 881 | /// position is idle. |
| 882 | /// |
| 883 | /// Listeners added by stateful widgets should be removed in the widget's |
| 884 | /// [State.dispose] method. |
| 885 | /// |
| 886 | /// {@tool dartpad} |
| 887 | /// This sample shows how you can trigger an auto-scroll, which aligns the last |
| 888 | /// partially visible fixed-height list item, by listening to this |
| 889 | /// notifier's value. This sort of thing can also be done by listening for |
| 890 | /// [ScrollEndNotification]s with a [NotificationListener]. An alternative |
| 891 | /// example is provided with [ScrollEndNotification]. |
| 892 | /// |
| 893 | /// ** See code in examples/api/lib/widgets/scroll_position/is_scrolling_listener.0.dart ** |
| 894 | /// {@end-tool} |
| 895 | final ValueNotifier<bool> isScrollingNotifier = ValueNotifier<bool>(false); |
| 896 | |
| 897 | /// Animates the position from its current value to the given value. |
| 898 | /// |
| 899 | /// Any active animation is canceled. If the user is currently scrolling, that |
| 900 | /// action is canceled. |
| 901 | /// |
| 902 | /// The returned [Future] will complete when the animation ends, whether it |
| 903 | /// completed successfully or whether it was interrupted prematurely. |
| 904 | /// |
| 905 | /// An animation will be interrupted whenever the user attempts to scroll |
| 906 | /// manually, or whenever another activity is started, or whenever the |
| 907 | /// animation reaches the edge of the viewport and attempts to overscroll. (If |
| 908 | /// the [ScrollPosition] does not overscroll but instead allows scrolling |
| 909 | /// beyond the extents, then going beyond the extents will not interrupt the |
| 910 | /// animation.) |
| 911 | /// |
| 912 | /// The animation is indifferent to changes to the viewport or content |
| 913 | /// dimensions. |
| 914 | /// |
| 915 | /// Once the animation has completed, the scroll position will attempt to |
| 916 | /// begin a ballistic activity in case its value is not stable (for example, |
| 917 | /// if it is scrolled beyond the extents and in that situation the scroll |
| 918 | /// position would normally bounce back). |
| 919 | /// |
| 920 | /// The duration must not be zero. To jump to a particular value without an |
| 921 | /// animation, use [jumpTo]. |
| 922 | /// |
| 923 | /// The animation is typically handled by an [DrivenScrollActivity]. |
| 924 | @override |
| 925 | Future<void> animateTo(double to, {required Duration duration, required Curve curve}); |
| 926 | |
| 927 | /// Jumps the scroll position from its current value to the given value, |
| 928 | /// without animation, and without checking if the new value is in range. |
| 929 | /// |
| 930 | /// Any active animation is canceled. If the user is currently scrolling, that |
| 931 | /// action is canceled. |
| 932 | /// |
| 933 | /// If this method changes the scroll position, a sequence of start/update/end |
| 934 | /// scroll notifications will be dispatched. No overscroll notifications can |
| 935 | /// be generated by this method. |
| 936 | @override |
| 937 | void jumpTo(double value); |
| 938 | |
| 939 | /// Changes the scrolling position based on a pointer signal from current |
| 940 | /// value to delta without animation and without checking if new value is in |
| 941 | /// range, taking min/max scroll extent into account. |
| 942 | /// |
| 943 | /// Any active animation is canceled. If the user is currently scrolling, that |
| 944 | /// action is canceled. |
| 945 | /// |
| 946 | /// This method dispatches the start/update/end sequence of scrolling |
| 947 | /// notifications. |
| 948 | /// |
| 949 | /// This method is very similar to [jumpTo], but [pointerScroll] will |
| 950 | /// update the [ScrollDirection]. |
| 951 | void pointerScroll(double delta); |
| 952 | |
| 953 | /// Calls [jumpTo] if duration is null or [Duration.zero], otherwise |
| 954 | /// [animateTo] is called. |
| 955 | /// |
| 956 | /// If [clamp] is true (the default) then [to] is adjusted to prevent over or |
| 957 | /// underscroll. |
| 958 | /// |
| 959 | /// If [animateTo] is called then [curve] defaults to [Curves.ease]. |
| 960 | @override |
| 961 | Future<void> moveTo(double to, {Duration? duration, Curve? curve, bool? clamp = true}) { |
| 962 | assert(clamp != null); |
| 963 | |
| 964 | if (clamp!) { |
| 965 | to = clampDouble(to, minScrollExtent, maxScrollExtent); |
| 966 | } |
| 967 | |
| 968 | return super.moveTo(to, duration: duration, curve: curve); |
| 969 | } |
| 970 | |
| 971 | @override |
| 972 | bool get allowImplicitScrolling => physics.allowImplicitScrolling; |
| 973 | |
| 974 | /// Deprecated. Use [jumpTo] or a custom [ScrollPosition] instead. |
| 975 | // flutter_ignore: deprecation_syntax, https://github.com/flutter/flutter/issues/44609 |
| 976 | @Deprecated( |
| 977 | 'This method bypasses scroll activity management and can cause inconsistent layouts ' |
| 978 | 'or scrolling behavior. Use jumpTo or a custom ScrollPosition instead.' , |
| 979 | ) |
| 980 | void jumpToWithoutSettling(double value); |
| 981 | |
| 982 | /// Stop the current activity and start a [HoldScrollActivity]. |
| 983 | ScrollHoldController hold(VoidCallback holdCancelCallback); |
| 984 | |
| 985 | /// Start a drag activity corresponding to the given [DragStartDetails]. |
| 986 | /// |
| 987 | /// The `onDragCanceled` argument will be invoked if the drag is ended |
| 988 | /// prematurely (e.g. from another activity taking over). See |
| 989 | /// [ScrollDragController.onDragCanceled] for details. |
| 990 | Drag drag(DragStartDetails details, VoidCallback dragCancelCallback); |
| 991 | |
| 992 | /// The currently operative [ScrollActivity]. |
| 993 | /// |
| 994 | /// If the scroll position is not performing any more specific activity, the |
| 995 | /// activity will be an [IdleScrollActivity]. To determine whether the scroll |
| 996 | /// position is idle, check the [isScrollingNotifier]. |
| 997 | /// |
| 998 | /// Call [beginActivity] to change the current activity. |
| 999 | @protected |
| 1000 | @visibleForTesting |
| 1001 | ScrollActivity? get activity => _activity; |
| 1002 | ScrollActivity? _activity; |
| 1003 | |
| 1004 | /// Change the current [activity], disposing of the old one and |
| 1005 | /// sending scroll notifications as necessary. |
| 1006 | /// |
| 1007 | /// If the argument is null, this method has no effect. This is convenient for |
| 1008 | /// cases where the new activity is obtained from another method, and that |
| 1009 | /// method might return null, since it means the caller does not have to |
| 1010 | /// explicitly null-check the argument. |
| 1011 | void beginActivity(ScrollActivity? newActivity) { |
| 1012 | if (newActivity == null) { |
| 1013 | return; |
| 1014 | } |
| 1015 | bool wasScrolling, oldIgnorePointer; |
| 1016 | if (_activity != null) { |
| 1017 | oldIgnorePointer = _activity!.shouldIgnorePointer; |
| 1018 | wasScrolling = _activity!.isScrolling; |
| 1019 | if (wasScrolling && !newActivity.isScrolling) { |
| 1020 | // Notifies and then saves the scroll offset. |
| 1021 | didEndScroll(); |
| 1022 | } |
| 1023 | _activity!.dispose(); |
| 1024 | } else { |
| 1025 | oldIgnorePointer = false; |
| 1026 | wasScrolling = false; |
| 1027 | } |
| 1028 | _activity = newActivity; |
| 1029 | if (oldIgnorePointer != activity!.shouldIgnorePointer) { |
| 1030 | context.setIgnorePointer(activity!.shouldIgnorePointer); |
| 1031 | } |
| 1032 | isScrollingNotifier.value = activity!.isScrolling; |
| 1033 | if (!wasScrolling && _activity!.isScrolling) { |
| 1034 | didStartScroll(); |
| 1035 | } |
| 1036 | } |
| 1037 | |
| 1038 | // NOTIFICATION DISPATCH |
| 1039 | |
| 1040 | /// Called by [beginActivity] to report when an activity has started. |
| 1041 | void didStartScroll() { |
| 1042 | activity!.dispatchScrollStartNotification(copyWith(), context.notificationContext); |
| 1043 | } |
| 1044 | |
| 1045 | /// Called by [setPixels] to report a change to the [pixels] position. |
| 1046 | void didUpdateScrollPositionBy(double delta) { |
| 1047 | activity!.dispatchScrollUpdateNotification(copyWith(), context.notificationContext!, delta); |
| 1048 | } |
| 1049 | |
| 1050 | /// Called by [beginActivity] to report when an activity has ended. |
| 1051 | /// |
| 1052 | /// This also saves the scroll offset using [saveScrollOffset]. |
| 1053 | void didEndScroll() { |
| 1054 | activity!.dispatchScrollEndNotification(copyWith(), context.notificationContext!); |
| 1055 | saveOffset(); |
| 1056 | if (keepScrollOffset) { |
| 1057 | saveScrollOffset(); |
| 1058 | } |
| 1059 | } |
| 1060 | |
| 1061 | /// Called by [setPixels] to report overscroll when an attempt is made to |
| 1062 | /// change the [pixels] position. Overscroll is the amount of change that was |
| 1063 | /// not applied to the [pixels] value. |
| 1064 | void didOverscrollBy(double value) { |
| 1065 | assert(activity!.isScrolling); |
| 1066 | activity!.dispatchOverscrollNotification(copyWith(), context.notificationContext!, value); |
| 1067 | } |
| 1068 | |
| 1069 | /// Dispatches a notification that the [userScrollDirection] has changed. |
| 1070 | /// |
| 1071 | /// Subclasses should call this function when they change [userScrollDirection]. |
| 1072 | void didUpdateScrollDirection(ScrollDirection direction) { |
| 1073 | UserScrollNotification( |
| 1074 | metrics: copyWith(), |
| 1075 | context: context.notificationContext!, |
| 1076 | direction: direction, |
| 1077 | ).dispatch(context.notificationContext); |
| 1078 | } |
| 1079 | |
| 1080 | /// Dispatches a notification that the [ScrollMetrics] have changed. |
| 1081 | void didUpdateScrollMetrics() { |
| 1082 | assert(SchedulerBinding.instance.schedulerPhase != SchedulerPhase.persistentCallbacks); |
| 1083 | assert(_haveScheduledUpdateNotification); |
| 1084 | _haveScheduledUpdateNotification = false; |
| 1085 | if (context.notificationContext != null) { |
| 1086 | ScrollMetricsNotification( |
| 1087 | metrics: copyWith(), |
| 1088 | context: context.notificationContext!, |
| 1089 | ).dispatch(context.notificationContext); |
| 1090 | } |
| 1091 | } |
| 1092 | |
| 1093 | /// Provides a heuristic to determine if expensive frame-bound tasks should be |
| 1094 | /// deferred. |
| 1095 | /// |
| 1096 | /// The actual work of this is delegated to the [physics] via |
| 1097 | /// [ScrollPhysics.recommendDeferredLoading] called with the current |
| 1098 | /// [activity]'s [ScrollActivity.velocity]. |
| 1099 | /// |
| 1100 | /// Returning true from this method indicates that the [ScrollPhysics] |
| 1101 | /// evaluate the current scroll velocity to be great enough that expensive |
| 1102 | /// operations impacting the UI should be deferred. |
| 1103 | bool recommendDeferredLoading(BuildContext context) { |
| 1104 | assert(activity != null); |
| 1105 | return physics.recommendDeferredLoading( |
| 1106 | activity!.velocity + _impliedVelocity, |
| 1107 | copyWith(), |
| 1108 | context, |
| 1109 | ); |
| 1110 | } |
| 1111 | |
| 1112 | @override |
| 1113 | void dispose() { |
| 1114 | activity?.dispose(); // it will be null if it got absorbed by another ScrollPosition |
| 1115 | _activity = null; |
| 1116 | isScrollingNotifier.dispose(); |
| 1117 | super.dispose(); |
| 1118 | } |
| 1119 | |
| 1120 | @override |
| 1121 | void notifyListeners() { |
| 1122 | _updateSemanticActions(); // will potentially request a semantics update. |
| 1123 | super.notifyListeners(); |
| 1124 | } |
| 1125 | |
| 1126 | @override |
| 1127 | void debugFillDescription(List<String> description) { |
| 1128 | if (debugLabel != null) { |
| 1129 | description.add(debugLabel!); |
| 1130 | } |
| 1131 | super.debugFillDescription(description); |
| 1132 | description.add( |
| 1133 | 'range: ${_minScrollExtent?.toStringAsFixed(1)}.. ${_maxScrollExtent?.toStringAsFixed(1)}' , |
| 1134 | ); |
| 1135 | description.add('viewport: ${_viewportDimension?.toStringAsFixed(1)}' ); |
| 1136 | } |
| 1137 | } |
| 1138 | |
| 1139 | /// A notification that a scrollable widget's [ScrollMetrics] have changed. |
| 1140 | /// |
| 1141 | /// For example, when the content of a scrollable is altered, making it larger |
| 1142 | /// or smaller, this notification will be dispatched. Similarly, if the size |
| 1143 | /// of the window or parent changes, the scrollable can notify of these |
| 1144 | /// changes in dimensions. |
| 1145 | /// |
| 1146 | /// The above behaviors usually do not trigger [ScrollNotification] events, |
| 1147 | /// so this is useful for listening to [ScrollMetrics] changes that are not |
| 1148 | /// caused by the user scrolling. |
| 1149 | /// |
| 1150 | /// {@tool dartpad} |
| 1151 | /// This sample shows how a [ScrollMetricsNotification] is dispatched when |
| 1152 | /// the `windowSize` is changed. Press the floating action button to increase |
| 1153 | /// the scrollable window's size. |
| 1154 | /// |
| 1155 | /// ** See code in examples/api/lib/widgets/scroll_position/scroll_metrics_notification.0.dart ** |
| 1156 | /// {@end-tool} |
| 1157 | class ScrollMetricsNotification extends Notification with ViewportNotificationMixin { |
| 1158 | /// Creates a notification that the scrollable widget's [ScrollMetrics] have |
| 1159 | /// changed. |
| 1160 | ScrollMetricsNotification({required this.metrics, required this.context}); |
| 1161 | |
| 1162 | /// Description of a scrollable widget's [ScrollMetrics]. |
| 1163 | final ScrollMetrics metrics; |
| 1164 | |
| 1165 | /// The build context of the widget that fired this notification. |
| 1166 | /// |
| 1167 | /// This can be used to find the scrollable widget's render objects to |
| 1168 | /// determine the size of the viewport, for instance. |
| 1169 | final BuildContext context; |
| 1170 | |
| 1171 | /// Convert this notification to a [ScrollNotification]. |
| 1172 | /// |
| 1173 | /// This allows it to be used with [ScrollNotificationPredicate]s. |
| 1174 | ScrollUpdateNotification asScrollUpdate() { |
| 1175 | return ScrollUpdateNotification(metrics: metrics, context: context, depth: depth); |
| 1176 | } |
| 1177 | |
| 1178 | @override |
| 1179 | void debugFillDescription(List<String> description) { |
| 1180 | super.debugFillDescription(description); |
| 1181 | description.add(' $metrics' ); |
| 1182 | } |
| 1183 | } |
| 1184 | |