Skip to content

Commit 8e8f618

Browse files
authored
Fix overscroll edge case that puts NestedScrollViews out of sync (flutter#68644)
1 parent c5c2b24 commit 8e8f618

File tree

2 files changed

+58
-1
lines changed

2 files changed

+58
-1
lines changed

packages/flutter/lib/src/widgets/nested_scroll_view.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1343,7 +1343,9 @@ class _NestedScrollPosition extends ScrollPosition implements ScrollActivityDele
13431343
// The logic for max is equivalent but on the other side.
13441344
final double max = delta > 0.0
13451345
? double.infinity
1346-
: math.max(maxScrollExtent, pixels);
1346+
// If pixels < 0.0, then we are currently in overscroll. The max should be
1347+
// 0.0, representing the end of the overscrolled portion.
1348+
: pixels < 0.0 ? 0.0 : math.max(maxScrollExtent, pixels);
13471349
final double oldPixels = pixels;
13481350
final double newPixels = (pixels - delta).clamp(min, max);
13491351
final double clampedDelta = newPixels - pixels;

packages/flutter/test/widgets/nested_scroll_view_test.dart

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1906,6 +1906,61 @@ void main() {
19061906
expect(tester.getCenter(find.text('Item 49')).dy, equals(585.0));
19071907
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }));
19081908
});
1909+
1910+
// Regression test for https://github.com/flutter/flutter/issues/63978
1911+
testWidgets('Inner _NestedScrollPosition.applyClampedDragUpdate correctly calculates range when in overscroll', (WidgetTester tester) async {
1912+
final GlobalKey<NestedScrollViewState> nestedScrollView = GlobalKey();
1913+
await tester.pumpWidget(MaterialApp(
1914+
home: Scaffold(
1915+
body: NestedScrollView(
1916+
key: nestedScrollView,
1917+
headerSliverBuilder: (BuildContext context, bool boxIsScrolled) {
1918+
return <Widget>[
1919+
const SliverAppBar(
1920+
expandedHeight: 200,
1921+
title: Text('Test'),
1922+
)
1923+
];
1924+
},
1925+
body: ListView.builder(
1926+
itemExtent: 100.0,
1927+
itemBuilder: (BuildContext context, int index) => Container(
1928+
padding: const EdgeInsets.all(10.0),
1929+
child: Material(
1930+
color: index.isEven ? Colors.cyan : Colors.deepOrange,
1931+
child: Center(
1932+
child: Text(index.toString()),
1933+
),
1934+
),
1935+
),
1936+
),
1937+
),
1938+
),
1939+
));
1940+
1941+
expect(nestedScrollView.currentState!.outerController.position.pixels, 0.0);
1942+
expect(nestedScrollView.currentState!.innerController.position.pixels, 0.0);
1943+
expect(nestedScrollView.currentState!.outerController.position.maxScrollExtent, 200.0);
1944+
final Offset point = tester.getCenter(find.text('1'));
1945+
// Drag slightly into overscroll in the inner position.
1946+
final TestGesture gesture = await tester.startGesture(point);
1947+
await gesture.moveBy(const Offset(0.0, 5.0));
1948+
await tester.pump();
1949+
expect(nestedScrollView.currentState!.outerController.position.pixels, 0.0);
1950+
expect(nestedScrollView.currentState!.innerController.position.pixels, -5.0);
1951+
// Move by a much larger delta than the amount of over scroll, in a very
1952+
// short period of time.
1953+
await gesture.moveBy(const Offset(0.0, -500.0));
1954+
await tester.pump();
1955+
// The overscrolled inner position should have closed, then passed the
1956+
// correct remaining delta to the outer position, and finally any remainder
1957+
// back to the inner position.
1958+
expect(
1959+
nestedScrollView.currentState!.outerController.position.pixels,
1960+
nestedScrollView.currentState!.outerController.position.maxScrollExtent,
1961+
);
1962+
expect(nestedScrollView.currentState!.innerController.position.pixels, 295.0);
1963+
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
19091964
}
19101965

19111966
class TestHeader extends SliverPersistentHeaderDelegate {

0 commit comments

Comments
 (0)