diff --git a/packages/flutter/lib/src/rendering/sliver_group.dart b/packages/flutter/lib/src/rendering/sliver_group.dart index 6057be87ad..8315908408 100644 --- a/packages/flutter/lib/src/rendering/sliver_group.dart +++ b/packages/flutter/lib/src/rendering/sliver_group.dart @@ -129,8 +129,10 @@ class RenderSliverCrossAxisGroup extends RenderSliver with ContainerRenderObject RenderSliver? child = firstChild; while (child != null) { - final SliverPhysicalParentData childParentData = child.parentData! as SliverPhysicalParentData; - context.paintChild(child, offset + childParentData.paintOffset); + if (child.geometry!.visible) { + final SliverPhysicalParentData childParentData = child.parentData! as SliverPhysicalParentData; + context.paintChild(child, offset + childParentData.paintOffset); + } child = childAfter(child); } } @@ -294,8 +296,10 @@ class RenderSliverMainAxisGroup extends RenderSliver with ContainerRenderObjectM RenderSliver? child = lastChild; while (child != null) { - final SliverPhysicalParentData childParentData = child.parentData! as SliverPhysicalParentData; - context.paintChild(child, offset + childParentData.paintOffset); + if (child.geometry!.visible) { + final SliverPhysicalParentData childParentData = child.parentData! as SliverPhysicalParentData; + context.paintChild(child, offset + childParentData.paintOffset); + } child = childBefore(child); } } diff --git a/packages/flutter/test/rendering/sliver_utils.dart b/packages/flutter/test/rendering/sliver_utils.dart new file mode 100644 index 0000000000..8fbe123d09 --- /dev/null +++ b/packages/flutter/test/rendering/sliver_utils.dart @@ -0,0 +1,36 @@ +// 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. + +// Test sliver which always attempts to paint itself whether it is visible or not. +// Use for checking if slivers which take sliver children paints optimally. +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +class RenderMockSliverToBoxAdapter extends RenderSliverToBoxAdapter { + RenderMockSliverToBoxAdapter({ + super.child, + required this.incrementCounter, + }); + final void Function() incrementCounter; + + @override + void paint(PaintingContext context, Offset offset) { + incrementCounter(); + } +} + +class MockSliverToBoxAdapter extends SingleChildRenderObjectWidget { + /// Creates a sliver that contains a single box widget. + const MockSliverToBoxAdapter({ + super.key, + super.child, + required this.incrementCounter, + }); + + final void Function() incrementCounter; + + @override + RenderMockSliverToBoxAdapter createRenderObject(BuildContext context) => + RenderMockSliverToBoxAdapter(incrementCounter: incrementCounter); +} diff --git a/packages/flutter/test/widgets/sliver_cross_axis_group_test.dart b/packages/flutter/test/widgets/sliver_cross_axis_group_test.dart index 745f2c28ff..a3d4dc8fd9 100644 --- a/packages/flutter/test/widgets/sliver_cross_axis_group_test.dart +++ b/packages/flutter/test/widgets/sliver_cross_axis_group_test.dart @@ -6,6 +6,9 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import '../rendering/sliver_utils.dart'; + + const double VIEWPORT_HEIGHT = 600; const double VIEWPORT_WIDTH = 300; @@ -806,8 +809,63 @@ void main() { // If renderHeader._lastStartedScrollDirection is not ScrollDirection.forward, then we shouldn't see the header at all. expect((renderHeader.parentData! as SliverPhysicalParentData).paintOffset.dy, equals(0.0)); }); + + testWidgets('SliverCrossAxisGroup skips painting invisible children', (WidgetTester tester) async { + final ScrollController controller = ScrollController(); + + int counter = 0; + void incrementCounter() { + counter += 1; + } + + await tester.pumpWidget( + _buildSliverCrossAxisGroup( + controller: controller, + slivers: [ + MockSliverToBoxAdapter( + incrementCounter: incrementCounter, + child: Container( + height: 1000, + decoration: const BoxDecoration(color: Colors.amber), + ), + ), + MockSliverToBoxAdapter( + incrementCounter: incrementCounter, + child: Container( + height: 400, + decoration: const BoxDecoration(color: Colors.amber) + ), + ), + MockSliverToBoxAdapter( + incrementCounter: incrementCounter, + child: Container( + height: 500, + decoration: const BoxDecoration(color: Colors.amber) + ), + ), + MockSliverToBoxAdapter( + incrementCounter: incrementCounter, + child: Container( + height: 300, + decoration: const BoxDecoration(color: Colors.amber) + ), + ), + ], + ), + ); + expect(counter, equals(4)); + + // Reset paint counter. + counter = 0; + controller.jumpTo(400); + await tester.pumpAndSettle(); + + expect(controller.offset, 400); + expect(counter, equals(2)); + }); } + Widget _buildSliverList({ double itemMainAxisExtent = 100, List items = const [], 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 7851cb430c..597c196acb 100644 --- a/packages/flutter/test/widgets/sliver_main_axis_group_test.dart +++ b/packages/flutter/test/widgets/sliver_main_axis_group_test.dart @@ -6,6 +6,9 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; +import '../rendering/sliver_utils.dart'; + + const double VIEWPORT_HEIGHT = 600; const double VIEWPORT_WIDTH = 300; @@ -604,6 +607,63 @@ void main() { expect(renderHeader.geometry!.paintExtent, equals(60.0)); expect((renderHeader.parentData! as SliverPhysicalParentData).paintOffset.dy, equals(-50.0)); }); + + testWidgets('SliverMainAxisGroup skips painting invisible children', (WidgetTester tester) async { + final ScrollController controller = ScrollController(); + + int counter = 0; + void incrementCounter() { + counter += 1; + } + + await tester.pumpWidget( + _buildSliverMainAxisGroup( + controller: controller, + slivers: [ + MockSliverToBoxAdapter( + incrementCounter: incrementCounter, + child: Container( + height: 1000, + decoration: const BoxDecoration(color: Colors.amber), + ), + ), + MockSliverToBoxAdapter( + incrementCounter: incrementCounter, + child: Container( + height: 400, + decoration: const BoxDecoration(color: Colors.amber) + ), + ), + MockSliverToBoxAdapter( + incrementCounter: incrementCounter, + child: Container( + height: 500, + decoration: const BoxDecoration(color: Colors.amber) + ), + ), + MockSliverToBoxAdapter( + incrementCounter: incrementCounter, + child: Container( + height: 300, + decoration: const BoxDecoration(color: Colors.amber) + ), + ), + ], + ), + ); + + // Can only see top sliver. + expect(counter, equals(1)); + + // Reset paint counter. + counter = 0; + controller.jumpTo(1000); + await tester.pumpAndSettle(); + + // Can only see second and third slivers. + expect(controller.offset, 1000); + expect(counter, equals(2)); + }); } Widget _buildSliverList({