Unverified Commit ef25052c authored by xster's avatar xster Committed by GitHub

Let scrollOffsetCorrection on small lists not leave the list stuck in overscroll (#17599)

parent 1808ac33
...@@ -282,7 +282,8 @@ class CupertinoRefreshControl extends StatefulWidget { ...@@ -282,7 +282,8 @@ class CupertinoRefreshControl extends StatefulWidget {
/// The amount of overscroll the scrollable must be dragged to trigger a reload. /// The amount of overscroll the scrollable must be dragged to trigger a reload.
/// ///
/// Must not be null, must be larger than 0.0 and larger than [refreshIndicatorExtent]. /// Must not be null, must be larger than 0.0 and larger than
/// [refreshIndicatorExtent]. Defaults to 100px when not specified.
/// ///
/// When overscrolled past this distance, [onRefresh] will be called if not /// When overscrolled past this distance, [onRefresh] will be called if not
/// null and the [builder] will build in the [RefreshIndicatorMode.armed] state. /// null and the [builder] will build in the [RefreshIndicatorMode.armed] state.
...@@ -293,6 +294,7 @@ class CupertinoRefreshControl extends StatefulWidget { ...@@ -293,6 +294,7 @@ class CupertinoRefreshControl extends StatefulWidget {
/// ///
/// Must not be null and must be positive, but can be 0.0, in which case the /// Must not be null and must be positive, but can be 0.0, in which case the
/// sliver will start retracting back to 0.0 as soon as the refresh is started. /// sliver will start retracting back to 0.0 as soon as the refresh is started.
/// Defaults to 60px when not specified.
/// ///
/// Must be smaller than [refreshTriggerPullDistance], since the sliver /// Must be smaller than [refreshTriggerPullDistance], since the sliver
/// shouldn't grow further after triggering the refresh. /// shouldn't grow further after triggering the refresh.
......
...@@ -150,10 +150,20 @@ abstract class ViewportOffset extends ChangeNotifier { ...@@ -150,10 +150,20 @@ abstract class ViewportOffset extends ChangeNotifier {
/// [RenderViewport], before [applyContentDimensions]. After this method is /// [RenderViewport], before [applyContentDimensions]. After this method is
/// called, the layout will be recomputed and that may result in this method /// called, the layout will be recomputed and that may result in this method
/// being called again, though this should be very rare. /// 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.
void correctBy(double correction); void correctBy(double correction);
/// Jumps the scroll position from its current value to the given value, /// Jumps the scroll position from its current value to the given value,
/// without animation, and without checking if the new value is in range. /// without animation, and without checking if the new value is in range.
///
/// See also:
///
/// * [correctBy], for changing the current offset in the middle of layout
/// and that defers the notification of its listeners until after layout.
void jumpTo(double pixels); void jumpTo(double pixels);
/// The direction in which the user is trying to change [pixels], relative to /// The direction in which the user is trying to change [pixels], relative to
......
...@@ -250,13 +250,43 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics { ...@@ -250,13 +250,43 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
/// To force the [pixels] to a particular value without honoring the normal /// To force the [pixels] to a particular value without honoring the normal
/// conventions for changing the scroll offset, consider [forcePixels]. (But /// conventions for changing the scroll offset, consider [forcePixels]. (But
/// see the discussion there for why that might still be a bad idea.) /// see the discussion there for why that might still be a bad idea.)
///
/// See also:
///
/// * The method [correctBy], which is a method of [ViewportOffset] used
/// by viewport render objects to correct the offset during layout
/// without notifying its listeners.
/// * The method [jumpTo], for making changes to position while not in the
/// middle of layout and applying the new position immediately.
/// * The method [animateTo], which is like [jumpTo] but animating to the
/// distination offset.
void correctPixels(double value) { void correctPixels(double value) {
_pixels = 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 @override
void correctBy(double correction) { void correctBy(double correction) {
assert(
_pixels != null,
'An initial pixels value must exist by caling correctPixels on the ScrollPosition',
);
_pixels += correction; _pixels += correction;
_didChangeViewportDimensionOrReceiveCorrection = true;
} }
/// Change the value of [pixels] to the new value, and notify any customers, /// Change the value of [pixels] to the new value, and notify any customers,
...@@ -360,13 +390,13 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics { ...@@ -360,13 +390,13 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
return result; return result;
} }
bool _didChangeViewportDimension = true; bool _didChangeViewportDimensionOrReceiveCorrection = true;
@override @override
bool applyViewportDimension(double viewportDimension) { bool applyViewportDimension(double viewportDimension) {
if (_viewportDimension != viewportDimension) { if (_viewportDimension != viewportDimension) {
_viewportDimension = viewportDimension; _viewportDimension = viewportDimension;
_didChangeViewportDimension = true; _didChangeViewportDimensionOrReceiveCorrection = true;
// If this is called, you can rely on applyContentDimensions being called // 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 // soon afterwards in the same layout phase. So we put all the logic that
// relies on both values being computed into applyContentDimensions. // relies on both values being computed into applyContentDimensions.
...@@ -419,12 +449,12 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics { ...@@ -419,12 +449,12 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) { bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
if (!nearEqual(_minScrollExtent, minScrollExtent, Tolerance.defaultTolerance.distance) || if (!nearEqual(_minScrollExtent, minScrollExtent, Tolerance.defaultTolerance.distance) ||
!nearEqual(_maxScrollExtent, maxScrollExtent, Tolerance.defaultTolerance.distance) || !nearEqual(_maxScrollExtent, maxScrollExtent, Tolerance.defaultTolerance.distance) ||
_didChangeViewportDimension) { _didChangeViewportDimensionOrReceiveCorrection) {
_minScrollExtent = minScrollExtent; _minScrollExtent = minScrollExtent;
_maxScrollExtent = maxScrollExtent; _maxScrollExtent = maxScrollExtent;
_haveDimensions = true; _haveDimensions = true;
applyNewDimensions(); applyNewDimensions();
_didChangeViewportDimension = false; _didChangeViewportDimensionOrReceiveCorrection = false;
} }
return true; return true;
} }
......
...@@ -84,11 +84,6 @@ class ScrollPositionWithSingleContext extends ScrollPosition implements ScrollAc ...@@ -84,11 +84,6 @@ class ScrollPositionWithSingleContext extends ScrollPosition implements ScrollAc
return super.setPixels(newPixels); return super.setPixels(newPixels);
} }
@override
void correctBy(double correction) {
correctPixels(pixels + correction);
}
@override @override
void absorb(ScrollPosition other) { void absorb(ScrollPosition other) {
super.absorb(other); super.absorb(other);
......
...@@ -62,6 +62,7 @@ void main() { ...@@ -62,6 +62,7 @@ void main() {
when(mockHelper.refreshTask()).thenAnswer((_) => refreshCompleter.future); when(mockHelper.refreshTask()).thenAnswer((_) => refreshCompleter.future);
}); });
int testListLength = 10;
SliverList buildAListOfStuff() { SliverList buildAListOfStuff() {
return new SliverList( return new SliverList(
delegate: new SliverChildBuilderDelegate( delegate: new SliverChildBuilderDelegate(
...@@ -71,12 +72,12 @@ void main() { ...@@ -71,12 +72,12 @@ void main() {
child: new Center(child: new Text(index.toString())), child: new Center(child: new Text(index.toString())),
); );
}, },
childCount: 10, childCount: testListLength,
), ),
); );
} }
group('UI tests', () { final Function uiTestGroup = () {
testWidgets("doesn't invoke anything without user interaction", (WidgetTester tester) async { testWidgets("doesn't invoke anything without user interaction", (WidgetTester tester) async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS; debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
...@@ -682,6 +683,11 @@ void main() { ...@@ -682,6 +683,11 @@ void main() {
testWidgets( testWidgets(
'sliver scrolled away when task completes properly removes itself', 'sliver scrolled away when task completes properly removes itself',
(WidgetTester tester) async { (WidgetTester tester) async {
if (testListLength < 4) {
// This test only makes sense when the list is long enough that
// the indicator can be scrolled away while refreshing.
return;
}
debugDefaultTargetPlatformOverride = TargetPlatform.iOS; debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
refreshIndicator = const Center(child: const Text('-1')); refreshIndicator = const Center(child: const Text('-1'));
...@@ -849,11 +855,9 @@ void main() { ...@@ -849,11 +855,9 @@ void main() {
debugDefaultTargetPlatformOverride = null; debugDefaultTargetPlatformOverride = null;
} }
); );
}); };
// Test the internal state machine directly to make sure the UI aren't just final Function stateMachineTestGroup = () {
// correct by coincidence.
group('state machine test', () {
testWidgets('starts in inactive state', (WidgetTester tester) async { testWidgets('starts in inactive state', (WidgetTester tester) async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS; debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
...@@ -1228,7 +1232,22 @@ void main() { ...@@ -1228,7 +1232,22 @@ void main() {
debugDefaultTargetPlatformOverride = null; debugDefaultTargetPlatformOverride = null;
} }
); );
}); };
group('UI tests long list', uiTestGroup);
// Test the internal state machine directly to make sure the UI aren't just
// correct by coincidence.
group('state machine test long list', stateMachineTestGroup);
// Retest everything and make sure that it still works when the whole list
// is smaller than the viewport size.
testListLength = 2;
group('UI tests short list', uiTestGroup);
// Test the internal state machine directly to make sure the UI aren't just
// correct by coincidence.
group('state machine test short list', stateMachineTestGroup);
} }
class MockHelper extends Mock { class MockHelper extends Mock {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment