Make large jumpTo recommend deferred loading (#64271)
diff --git a/packages/flutter/lib/src/widgets/scroll_position.dart b/packages/flutter/lib/src/widgets/scroll_position.dart
index f58d83a..3f805cc 100644
--- a/packages/flutter/lib/src/widgets/scroll_position.dart
+++ b/packages/flutter/lib/src/widgets/scroll_position.dart
@@ -145,6 +145,24 @@
double get maxScrollExtent => _maxScrollExtent;
double _maxScrollExtent;
+ /// 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;
@@ -343,8 +361,13 @@
@protected
void forcePixels(double value) {
assert(pixels != null);
+ assert(value != null);
+ _impliedVelocity = value - _pixels;
_pixels = value;
notifyListeners();
+ SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) {
+ _impliedVelocity = 0;
+ });
}
/// Called whenever scrolling ends, to store the current scroll offset in a
@@ -834,7 +857,12 @@
assert(context != null);
assert(activity != null);
assert(activity.velocity != null);
- return physics.recommendDeferredLoading(activity.velocity, copyWith(), context);
+ assert(_impliedVelocity != null);
+ return physics.recommendDeferredLoading(
+ activity.velocity + _impliedVelocity,
+ copyWith(),
+ context,
+ );
}
@override
diff --git a/packages/flutter/test/widgets/scroll_position_test.dart b/packages/flutter/test/widgets/scroll_position_test.dart
index 11aa00a..f536e9f 100644
--- a/packages/flutter/test/widgets/scroll_position_test.dart
+++ b/packages/flutter/test/widgets/scroll_position_test.dart
@@ -229,4 +229,41 @@
);
expect(controller.position.pixels, equals(0.0));
});
+
+ testWidgets('jumpTo recomends deferred loading', (WidgetTester tester) async {
+ int loadedWithDeferral = 0;
+ int buildCount = 0;
+ const double height = 500;
+ await tester.pumpWidget(MaterialApp(
+ home: ListView.builder(
+ itemBuilder: (BuildContext context, int index) {
+ buildCount += 1;
+ if (Scrollable.recommendDeferredLoadingForContext(context)) {
+ loadedWithDeferral += 1;
+ }
+ return const SizedBox(height: height);
+ },
+ ),
+ ));
+
+ // The two visible on screen should have loaded without deferral.
+ expect(buildCount, 2);
+ expect(loadedWithDeferral, 0);
+
+ final ScrollPosition position = tester.state<ScrollableState>(find.byType(Scrollable)).position;
+ position.jumpTo(height * 100);
+ await tester.pump();
+
+ // All but the first two that were loaded normally should have gotten a
+ // recommendation to defer.
+ expect(buildCount, 102);
+ expect(loadedWithDeferral, 100);
+
+ position.jumpTo(height * 102);
+ await tester.pump();
+
+ // The smaller jump should not have recommended deferral.
+ expect(buildCount, 104);
+ expect(loadedWithDeferral, 100);
+ });
}