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