From 2e861f3527f2ec97a499d2a223bb1706555eec52 Mon Sep 17 00:00:00 2001 From: manu-sncf <108678139+manu-sncf@users.noreply.github.com> Date: Tue, 21 Oct 2025 19:04:26 +0200 Subject: [PATCH] Fix SliverMainAxisGroup.cacheOrigin (#175760) Fix #175759 The fix involves using the same logic for cache calculation on RenderViewport. ## Pre-launch Checklist - [X] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [X] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [X] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [X] I signed the [CLA]. - [X] I listed at least one issue that this PR fixes in the description above. - [X] I updated/added relevant documentation (doc comments with `///`). - [X] I added new tests to check the change I am making, or this PR is [test-exempt]. - [X] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [X] All existing and new tests are passing. --- .../lib/src/rendering/sliver_group.dart | 24 +++++++++++--- .../widgets/sliver_main_axis_group_test.dart | 32 +++++++++++++++++++ 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/packages/flutter/lib/src/rendering/sliver_group.dart b/packages/flutter/lib/src/rendering/sliver_group.dart index 190fd295083..f4c01c49ab3 100644 --- a/packages/flutter/lib/src/rendering/sliver_group.dart +++ b/packages/flutter/lib/src/rendering/sliver_group.dart @@ -296,6 +296,9 @@ class RenderSliverMainAxisGroup extends RenderSliver double paintOffset = constraints.overlap; double maxScrollObstructionExtent = 0; + double cacheOrigin = constraints.cacheOrigin; + double remainingCacheExtent = constraints.remainingCacheExtent; + final ( RenderSliver? leadingChild, RenderSliver? Function(RenderSliver child) advance, @@ -310,17 +313,22 @@ class RenderSliverMainAxisGroup extends RenderSliver from: 0.0, to: scrollOffset, ); + + final double childScrollOffset = math.max(0.0, constraints.scrollOffset - scrollOffset); + final double correctedCacheOrigin = math.max(cacheOrigin, -childScrollOffset); + final double cacheExtentCorrection = cacheOrigin - correctedCacheOrigin; + child.layout( constraints.copyWith( - scrollOffset: math.max(0.0, constraints.scrollOffset - scrollOffset), - cacheOrigin: math.min(0.0, constraints.cacheOrigin + scrollOffset), + scrollOffset: childScrollOffset, + cacheOrigin: correctedCacheOrigin, overlap: math.max(0.0, _fixPrecisionError(paintOffset - beforeOffsetPaintExtent)), remainingPaintExtent: _fixPrecisionError( constraints.remainingPaintExtent - beforeOffsetPaintExtent, ), - remainingCacheExtent: _fixPrecisionError( - constraints.remainingCacheExtent - - calculateCacheOffset(constraints, from: 0.0, to: scrollOffset), + remainingCacheExtent: math.max( + 0.0, + _fixPrecisionError(remainingCacheExtent + cacheExtentCorrection), ), precedingScrollExtent: scrollOffset + constraints.precedingScrollExtent, ), @@ -349,6 +357,12 @@ class RenderSliverMainAxisGroup extends RenderSliver maxPaintExtent += childLayoutGeometry.maxPaintExtent; maxScrollObstructionExtent += childLayoutGeometry.maxScrollObstructionExtent; paintOffset = math.max(childPaintOffset + childLayoutGeometry.paintExtent, paintOffset); + if (childLayoutGeometry.cacheExtent != 0.0) { + remainingCacheExtent = _fixPrecisionError( + remainingCacheExtent - childLayoutGeometry.cacheExtent - cacheExtentCorrection, + ); + cacheOrigin = math.min(correctedCacheOrigin + childLayoutGeometry.cacheExtent, 0.0); + } child = advance(child); assert(() { if (child != null && maxPaintExtent.isInfinite) { diff --git a/packages/flutter/test/widgets/sliver_main_axis_group_test.dart b/packages/flutter/test/widgets/sliver_main_axis_group_test.dart index 2a110a72384..58896a22f70 100644 --- a/packages/flutter/test/widgets/sliver_main_axis_group_test.dart +++ b/packages/flutter/test/widgets/sliver_main_axis_group_test.dart @@ -856,6 +856,38 @@ void main() { expect(renderGroup.geometry!.cacheExtent, 850.0); }); + testWidgets('SliverMainAxisGroup has consistent cacheOrigin', (WidgetTester tester) async { + const Widget item = SizedBox.square(dimension: 50); + + await tester.pumpWidget( + MaterialApp( + home: CustomScrollView( + slivers: [ + SliverMainAxisGroup( + slivers: [ + const PinnedHeaderSliver(child: SizedBox(height: 500)), + SliverList.builder( + itemCount: 100, + itemBuilder: (BuildContext context, int index) => item, + ), + const SliverToBoxAdapter(child: item), + ], + ), + ], + ), + ), + ); + + await tester.scrollUntilVisible(find.byType(SliverToBoxAdapter), 500); + await tester.pumpAndSettle(); + + final RenderSliver sliverList = + find.byType(SliverList).evaluate().single.findRenderObject()! as RenderSliver; + + expect(sliverList.constraints.cacheOrigin, -250.0); + expect(sliverList.constraints.remainingCacheExtent, 1100); + }); + testWidgets('SliverMainAxisGroup correctly handles ensureVisible', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); await tester.pumpWidget(