// 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. 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; void main() { testWidgets('SliverMainAxisGroup is laid out properly', (WidgetTester tester) async { final List<int> items = List<int>.generate(20, (int i) => i); final ScrollController controller = ScrollController(); await tester.pumpWidget( _buildSliverMainAxisGroup( controller: controller, slivers: <Widget>[ _buildSliverList(itemMainAxisExtent: 300, items: items, label: (int item) => Text('Group 0 Tile $item')), _buildSliverList(itemMainAxisExtent: 200, items: items, label: (int item) => Text('Group 1 Tile $item')), ], ), ); await tester.pumpAndSettle(); expect(controller.offset, 0); expect(find.text('Group 0 Tile 0'), findsOneWidget); expect(find.text('Group 0 Tile 1'), findsOneWidget); expect(find.text('Group 0 Tile 2'), findsNothing); expect(find.text('Group 1 Tile 0'), findsNothing); const double scrollOffset = 19 * 300.0; controller.jumpTo(scrollOffset); await tester.pumpAndSettle(); expect(controller.offset, scrollOffset); expect(find.text('Group 0 Tile 18'), findsNothing); expect(find.text('Group 0 Tile 19'), findsOneWidget); expect(find.text('Group 1 Tile 0'), findsOneWidget); final List<RenderSliverList> renderSlivers = tester.renderObjectList<RenderSliverList>(find.byType(SliverList)).toList(); final RenderSliverList first = renderSlivers[0]; final RenderSliverList second = renderSlivers[1]; expect(first.geometry!.layoutExtent, equals(300.0)); expect(second.geometry!.layoutExtent, equals(300.0)); expect(first.geometry!.scrollExtent, equals(20 * 300.0)); expect(second.geometry!.scrollExtent, equals(20 * 200.0)); expect((first.parentData! as SliverPhysicalParentData).paintOffset.dy, equals(0.0)); expect(first.constraints.scrollOffset, equals(19 * 300.0)); expect((second.parentData! as SliverPhysicalParentData).paintOffset.dy, equals(1 * 300.0)); final RenderSliverMainAxisGroup renderGroup = tester.renderObject<RenderSliverMainAxisGroup>(find.byType(SliverMainAxisGroup)); expect(renderGroup.geometry!.scrollExtent, equals(300 * 20 + 200 * 20)); expect(renderGroup.geometry!.hasVisualOverflow, isTrue); }); testWidgets('SliverMainAxisGroup is laid out properly when reversed', (WidgetTester tester) async { final List<int> items = List<int>.generate(20, (int i) => i); final ScrollController controller = ScrollController(); await tester.pumpWidget( _buildSliverMainAxisGroup( controller: controller, reverse: true, slivers: <Widget>[ _buildSliverList(itemMainAxisExtent: 300, items: items, label: (int item) => Text('Group 0 Tile $item')), _buildSliverList(itemMainAxisExtent: 200, items: items, label: (int item) => Text('Group 1 Tile $item')), ], ), ); await tester.pumpAndSettle(); expect(controller.offset, 0); expect(find.text('Group 0 Tile 0'), findsOneWidget); expect(find.text('Group 0 Tile 1'), findsOneWidget); expect(find.text('Group 0 Tile 2'), findsNothing); expect(find.text('Group 1 Tile 0'), findsNothing); const double scrollOffset = 19 * 300.0; controller.jumpTo(scrollOffset); await tester.pumpAndSettle(); expect(controller.offset, scrollOffset); expect(find.text('Group 0 Tile 18'), findsNothing); expect(find.text('Group 0 Tile 19'), findsOneWidget); expect(find.text('Group 1 Tile 0'), findsOneWidget); final List<RenderSliverList> renderSlivers = tester.renderObjectList<RenderSliverList>(find.byType(SliverList)).toList(); final RenderSliverList first = renderSlivers[0]; final RenderSliverList second = renderSlivers[1]; expect(first.geometry!.layoutExtent, equals(300.0)); expect(second.geometry!.layoutExtent, equals(300.0)); expect(first.geometry!.scrollExtent, equals(20 * 300.0)); expect(second.geometry!.scrollExtent, equals(20 * 200.0)); expect((first.parentData! as SliverPhysicalParentData).paintOffset.dy, equals(0.0)); expect(first.constraints.scrollOffset, equals(19 * 300.0)); expect((second.parentData! as SliverPhysicalParentData).paintOffset.dy, equals(1 * 300.0)); final RenderSliverMainAxisGroup renderGroup = tester.renderObject<RenderSliverMainAxisGroup>(find.byType(SliverMainAxisGroup)); expect(renderGroup.geometry!.scrollExtent, equals(300 * 20 + 200 * 20)); expect(renderGroup.geometry!.hasVisualOverflow, isTrue); }); testWidgets('SliverMainAxisGroup is laid out properly when horizontal', (WidgetTester tester) async { final List<int> items = List<int>.generate(20, (int i) => i); final ScrollController controller = ScrollController(); await tester.pumpWidget( _buildSliverMainAxisGroup( controller: controller, scrollDirection: Axis.horizontal, slivers: <Widget>[ _buildSliverList(itemMainAxisExtent: 300, items: items, label: (int item) => Text('Group 0 Tile $item'), scrollDirection: Axis.horizontal), _buildSliverList(itemMainAxisExtent: 200, items: items, label: (int item) => Text('Group 1 Tile $item'), scrollDirection: Axis.horizontal), ], ), ); await tester.pumpAndSettle(); expect(controller.offset, 0); expect(find.text('Group 0 Tile 0'), findsOneWidget); expect(find.text('Group 0 Tile 1'), findsNothing); expect(find.text('Group 1 Tile 0'), findsNothing); const double scrollOffset = 19 * 300.0; controller.jumpTo(scrollOffset); await tester.pumpAndSettle(); expect(controller.offset, scrollOffset); expect(find.text('Group 0 Tile 18'), findsNothing); expect(find.text('Group 0 Tile 19'), findsOneWidget); expect(find.text('Group 1 Tile 0'), findsNothing); const double scrollOffset2 = 20 * 300.0; controller.jumpTo(scrollOffset2); await tester.pumpAndSettle(); expect(find.text('Group 0 Tile 19'), findsNothing); expect(find.text('Group 1 Tile 0'), findsOneWidget); final List<RenderSliverList> renderSlivers = tester.renderObjectList<RenderSliverList>(find.byType(SliverList)).toList(); final RenderSliverList first = renderSlivers[0]; final RenderSliverList second = renderSlivers[1]; expect(first.geometry!.layoutExtent, equals(0.0)); expect(second.geometry!.layoutExtent, equals(300.0)); expect(first.geometry!.scrollExtent, equals(20 * 300.0)); expect(second.geometry!.scrollExtent, equals(20 * 200.0)); expect((first.parentData! as SliverPhysicalParentData).paintOffset.dy, equals(0.0)); expect(first.constraints.scrollOffset, equals(20 * 300.0)); expect((second.parentData! as SliverPhysicalParentData).paintOffset.dy, equals(0.0)); final RenderSliverMainAxisGroup renderGroup = tester.renderObject<RenderSliverMainAxisGroup>(find.byType(SliverMainAxisGroup)); expect(renderGroup.geometry!.scrollExtent, equals(300 * 20 + 200 * 20)); expect(renderGroup.geometry!.hasVisualOverflow, isTrue); }); testWidgets('SliverMainAxisGroup is laid out properly when horizontal, reversed', (WidgetTester tester) async { final List<int> items = List<int>.generate(20, (int i) => i); final ScrollController controller = ScrollController(); await tester.pumpWidget( _buildSliverMainAxisGroup( controller: controller, scrollDirection: Axis.horizontal, reverse: true, slivers: <Widget>[ _buildSliverList(itemMainAxisExtent: 300, items: items, label: (int item) => Text('Group 0 Tile $item'), scrollDirection: Axis.horizontal), _buildSliverList(itemMainAxisExtent: 200, items: items, label: (int item) => Text('Group 1 Tile $item'), scrollDirection: Axis.horizontal), ], ), ); await tester.pumpAndSettle(); expect(controller.offset, 0); expect(find.text('Group 0 Tile 0'), findsOneWidget); expect(find.text('Group 0 Tile 1'), findsNothing); expect(find.text('Group 1 Tile 0'), findsNothing); const double scrollOffset = 19 * 300.0; controller.jumpTo(scrollOffset); await tester.pumpAndSettle(); expect(controller.offset, scrollOffset); expect(find.text('Group 0 Tile 18'), findsNothing); expect(find.text('Group 0 Tile 19'), findsOneWidget); expect(find.text('Group 1 Tile 0'), findsNothing); const double scrollOffset2 = 20 * 300.0; controller.jumpTo(scrollOffset2); await tester.pumpAndSettle(); expect(find.text('Group 0 Tile 19'), findsNothing); expect(find.text('Group 1 Tile 0'), findsOneWidget); final List<RenderSliverList> renderSlivers = tester.renderObjectList<RenderSliverList>(find.byType(SliverList)).toList(); final RenderSliverList first = renderSlivers[0]; final RenderSliverList second = renderSlivers[1]; expect(first.geometry!.layoutExtent, equals(0.0)); expect(second.geometry!.layoutExtent, equals(300.0)); expect(first.geometry!.scrollExtent, equals(20 * 300.0)); expect(second.geometry!.scrollExtent, equals(20 * 200.0)); expect((first.parentData! as SliverPhysicalParentData).paintOffset.dy, equals(0.0)); expect(first.constraints.scrollOffset, equals(20 * 300.0)); expect((second.parentData! as SliverPhysicalParentData).paintOffset.dy, equals(0.0)); final RenderSliverMainAxisGroup renderGroup = tester.renderObject<RenderSliverMainAxisGroup>(find.byType(SliverMainAxisGroup)); expect(renderGroup.geometry!.scrollExtent, equals(300 * 20 + 200 * 20)); expect(renderGroup.geometry!.hasVisualOverflow, isTrue); }); testWidgets('Hit test works properly on various parts of SliverMainAxisGroup', (WidgetTester tester) async { final List<int> items = List<int>.generate(20, (int i) => i); final ScrollController controller = ScrollController(); String? clickedTile; int group = 0; int tile = 0; await tester.pumpWidget(_buildSliverMainAxisGroup( controller: controller, slivers: <Widget>[ _buildSliverList( itemMainAxisExtent: 300, items: items, label: (int item) => tile == item && group == 0 ? TextButton( onPressed: () => clickedTile = 'Group 0 Tile $item', child: Text('Group 0 Tile $item'), ) : Text('Group 0 Tile $item'), ), _buildSliverList( items: items, label: (int item) => tile == item && group == 1 ? TextButton( onPressed: () => clickedTile = 'Group 1 Tile $item', child: Text('Group 1 Tile $item'), ) : Text('Group 1 Tile $item'), ), ]), ); await tester.pumpAndSettle(); await tester.tap(find.byType(TextButton)); await tester.pumpAndSettle(); expect(clickedTile, equals('Group 0 Tile 0')); clickedTile = null; group = 1; tile = 2; await tester.pumpWidget(_buildSliverMainAxisGroup( controller: controller, slivers: <Widget>[ _buildSliverList( itemMainAxisExtent: 300, items: items, label: (int item) => tile == item && group == 0 ? TextButton( onPressed: () => clickedTile = 'Group 0 Tile $item', child: Text('Group 0 Tile $item'), ) : Text('Group 0 Tile $item'), ), _buildSliverList( items: items, label: (int item) => tile == item && group == 1 ? TextButton( onPressed: () => clickedTile = 'Group 1 Tile $item', child: Text('Group 1 Tile $item'), ) : Text('Group 1 Tile $item'), ), ]), ); controller.jumpTo(300.0 * 20); await tester.pumpAndSettle(); await tester.tap(find.byType(TextButton)); await tester.pumpAndSettle(); expect(clickedTile, equals('Group 1 Tile 2')); }); testWidgets('applyPaintTransform is implemented properly', (WidgetTester tester) async { await tester.pumpWidget(_buildSliverMainAxisGroup( slivers: <Widget>[ const SliverToBoxAdapter(child: Text('first box')), const SliverToBoxAdapter(child: Text('second box')), ]), ); await tester.pumpAndSettle(); // localToGlobal calculates offset via applyPaintTransform final RenderBox first = tester.renderObject(find.text('first box')) as RenderBox; final RenderBox second = tester.renderObject(find.text('second box')); expect(first.localToGlobal(Offset.zero), Offset.zero); expect(second.localToGlobal(Offset.zero), Offset(0, first.size.height)); }); testWidgets('visitChildrenForSemantics visits children in the correct order', (WidgetTester tester) async { final ScrollController controller = ScrollController(); await tester.pumpWidget(_buildSliverMainAxisGroup( controller: controller, slivers: const <Widget>[ SliverToBoxAdapter(child: SizedBox(height: 200)), SliverToBoxAdapter(child: SizedBox(height: 300)), SliverToBoxAdapter(child: SizedBox(height: 500)), SliverToBoxAdapter(child: SizedBox(height: 400)), ]), ); controller.jumpTo(300); await tester.pumpAndSettle(); final List<RenderSliver> visitedChildren = <RenderSliver>[]; final RenderSliverMainAxisGroup renderGroup = tester.renderObject<RenderSliverMainAxisGroup>(find.byType(SliverMainAxisGroup)); void visitor(RenderObject child) { visitedChildren.add(child as RenderSliver); } renderGroup.visitChildrenForSemantics(visitor); expect(visitedChildren.length, equals(2)); expect(visitedChildren[0].geometry!.scrollExtent, equals(300)); expect(visitedChildren[1].geometry!.scrollExtent, equals(500)); }); testWidgets('SliverPinnedPersistentHeader is painted within bounds of SliverMainAxisGroup', (WidgetTester tester) async { final ScrollController controller = ScrollController(); await tester.pumpWidget(_buildSliverMainAxisGroup( controller: controller, slivers: <Widget>[ SliverPersistentHeader( delegate: TestDelegate(), pinned: true, ), const SliverToBoxAdapter(child: SizedBox(height: 600)), ], otherSlivers: <Widget>[ const SliverToBoxAdapter(child: SizedBox(height: 2400)), ], )); final RenderSliverMainAxisGroup renderGroup = tester.renderObject(find.byType(SliverMainAxisGroup)) as RenderSliverMainAxisGroup; // Scroll extent is the total of the box sliver and the sliver persistent header. expect(renderGroup.geometry!.scrollExtent, equals(600.0 + 60.0)); controller.jumpTo(620); await tester.pumpAndSettle(); final RenderSliverPersistentHeader renderHeader = tester.renderObject(find.byType(SliverPersistentHeader)) as RenderSliverPersistentHeader; // Paint extent after header's layout is 60.0, so we must offset by -20.0 to fit within the 40.0 remaining extent. expect(renderHeader.geometry!.paintExtent, equals(60.0)); expect((renderHeader.parentData! as SliverPhysicalParentData).paintOffset.dy, equals(-20.0)); }); testWidgets('SliverFloatingPersistentHeader is painted within bounds of SliverMainAxisGroup', (WidgetTester tester) async { final ScrollController controller = ScrollController(); await tester.pumpWidget(_buildSliverMainAxisGroup( controller: controller, slivers: <Widget>[ SliverPersistentHeader( delegate: TestDelegate(), floating: true, ), const SliverToBoxAdapter(child: SizedBox(height: 600)), ], otherSlivers: <Widget>[ const SliverToBoxAdapter(child: SizedBox(height: 2400)), ], )); await tester.pumpAndSettle(); final RenderSliverMainAxisGroup renderGroup = tester.renderObject(find.byType(SliverMainAxisGroup)) as RenderSliverMainAxisGroup; expect(renderGroup.geometry!.scrollExtent, equals(660)); controller.jumpTo(660.0); await tester.pumpAndSettle(); final TestGesture gesture = await tester.startGesture(const Offset(150.0, 300.0)); await gesture.moveBy(const Offset(0.0, 40)); await tester.pump(); final RenderSliverPersistentHeader renderHeader = tester.renderObject(find.byType(SliverPersistentHeader)) as RenderSliverPersistentHeader; // Paint extent after header's layout is 40.0, so no need to correct the paintOffset. expect(renderHeader.geometry!.paintExtent, equals(40.0)); expect((renderHeader.parentData! as SliverPhysicalParentData).paintOffset.dy, equals(0.0)); }); testWidgets('SliverPinnedPersistentHeader is painted within bounds of SliverMainAxisGroup with different minExtent/maxExtent', (WidgetTester tester) async { final ScrollController controller = ScrollController(); await tester.pumpWidget(_buildSliverMainAxisGroup( controller: controller, slivers: <Widget>[ SliverPersistentHeader( delegate: TestDelegate(minExtent: 40.0), pinned: true, ), const SliverToBoxAdapter(child: SizedBox(height: 600)), ], otherSlivers: <Widget>[ const SliverToBoxAdapter(child: SizedBox(height: 2400)), ], )); final RenderSliverMainAxisGroup renderGroup = tester.renderObject(find.byType(SliverMainAxisGroup)) as RenderSliverMainAxisGroup; final RenderSliverPersistentHeader renderHeader = tester.renderObject(find.byType(SliverPersistentHeader)) as RenderSliverPersistentHeader; expect(renderGroup.geometry!.scrollExtent, equals(660)); controller.jumpTo(630); await tester.pumpAndSettle(); // Paint extent of the header is 40.0, so we must provide an offset of -10.0 to make it fit in the 30.0 remaining paint extent of the group. expect(renderHeader.geometry!.paintExtent, equals(40.0)); expect((renderHeader.parentData! as SliverPhysicalParentData).paintOffset.dy, equals(-10.0)); controller.jumpTo(610); await tester.pumpAndSettle(); expect(renderHeader.geometry!.paintExtent, equals(40.0)); expect((renderHeader.parentData! as SliverPhysicalParentData).paintOffset.dy, equals(0.0)); }); testWidgets('SliverFloatingPersistentHeader is painted within bounds of SliverMainAxisGroup with different minExtent/maxExtent', (WidgetTester tester) async { final ScrollController controller = ScrollController(); await tester.pumpWidget(_buildSliverMainAxisGroup( controller: controller, slivers: <Widget>[ SliverPersistentHeader( delegate: TestDelegate(minExtent: 40.0), floating: true, ), const SliverToBoxAdapter(child: SizedBox(height: 600)), ], otherSlivers: <Widget>[ const SliverToBoxAdapter(child: SizedBox(height: 2400)), ], )); await tester.pumpAndSettle(); final RenderSliverMainAxisGroup renderGroup = tester.renderObject(find.byType(SliverMainAxisGroup)) as RenderSliverMainAxisGroup; final RenderSliverPersistentHeader renderHeader = tester.renderObject(find.byType(SliverPersistentHeader)) as RenderSliverPersistentHeader; expect(renderGroup.geometry!.scrollExtent, equals(660)); controller.jumpTo(660); await tester.pumpAndSettle(); final TestGesture gesture = await tester.startGesture(const Offset(150.0, 300.0)); await gesture.moveBy(const Offset(0.0, 30.0)); await tester.pump(); // Paint extent after header's layout is 30.0, so no need to correct the paintOffset. expect(renderHeader.geometry!.paintExtent, equals(30.0)); expect((renderHeader.parentData! as SliverPhysicalParentData).paintOffset.dy, equals(0.0)); // Floating headers should expand to maximum extent as we continue scrolling. await gesture.moveBy(const Offset(0.0, 20.0)); await tester.pump(); expect(renderHeader.geometry!.paintExtent, equals(50.0)); expect((renderHeader.parentData! as SliverPhysicalParentData).paintOffset.dy, equals(0.0)); }); testWidgets('SliverPinnedFloatingPersistentHeader is painted within bounds of SliverMainAxisGroup with different minExtent/maxExtent', (WidgetTester tester) async { final ScrollController controller = ScrollController(); await tester.pumpWidget(_buildSliverMainAxisGroup( controller: controller, slivers: <Widget>[ SliverPersistentHeader( delegate: TestDelegate(minExtent: 40.0), pinned: true, floating: true, ), const SliverToBoxAdapter(child: SizedBox(height: 600)), ], otherSlivers: <Widget>[ const SliverToBoxAdapter(child: SizedBox(height: 2400)), ], )); await tester.pumpAndSettle(); final RenderSliverMainAxisGroup renderGroup = tester.renderObject(find.byType(SliverMainAxisGroup)) as RenderSliverMainAxisGroup; final RenderSliverPersistentHeader renderHeader = tester.renderObject(find.byType(SliverPersistentHeader)) as RenderSliverPersistentHeader; expect(renderGroup.geometry!.scrollExtent, equals(660)); controller.jumpTo(660); await tester.pumpAndSettle(); final TestGesture gesture = await tester.startGesture(const Offset(150.0, 300.0)); await gesture.moveBy(const Offset(0.0, 30.0)); await tester.pump(); // Paint extent after header's layout is 40.0, so we need to adjust by -10.0. expect(renderHeader.geometry!.paintExtent, equals(40.0)); expect((renderHeader.parentData! as SliverPhysicalParentData).paintOffset.dy, equals(-10.0)); // Pinned floating headers should expand to maximum extent as we continue scrolling. await gesture.moveBy(const Offset(0.0, 20.0)); await tester.pump(); expect(renderHeader.geometry!.paintExtent, equals(50.0)); expect((renderHeader.parentData! as SliverPhysicalParentData).paintOffset.dy, equals(0.0)); }); testWidgets('SliverAppBar with floating: false, pinned: false, snap: false is painted within bounds of SliverMainAxisGroup', (WidgetTester tester) async { final ScrollController controller = ScrollController(); await tester.pumpWidget(_buildSliverMainAxisGroup( controller: controller, slivers: <Widget>[ const SliverAppBar( toolbarHeight: 30, expandedHeight: 60, ), const SliverToBoxAdapter(child: SizedBox(height: 600)), ], otherSlivers: <Widget>[ const SliverToBoxAdapter(child: SizedBox(height: 2400)), ], )); await tester.pumpAndSettle(); final RenderSliverMainAxisGroup renderGroup = tester.renderObject(find.byType(SliverMainAxisGroup)) as RenderSliverMainAxisGroup; expect(renderGroup.geometry!.scrollExtent, equals(660)); controller.jumpTo(660); await tester.pumpAndSettle(); controller.jumpTo(630); await tester.pumpAndSettle(); // At a scroll offset of 630, a normal scrolling header should be out of view. final RenderSliverPersistentHeader renderHeader = tester.renderObject(find.byType(SliverPersistentHeader)) as RenderSliverPersistentHeader; expect(renderHeader.constraints.scrollOffset, equals(630)); expect(renderHeader.geometry!.layoutExtent, equals(0.0)); }); testWidgets('SliverAppBar with floating: true, pinned: false, snap: true is painted within bounds of SliverMainAxisGroup', (WidgetTester tester) async { final ScrollController controller = ScrollController(); await tester.pumpWidget(_buildSliverMainAxisGroup( controller: controller, slivers: <Widget>[ const SliverAppBar( toolbarHeight: 30, expandedHeight: 60, floating: true, snap: true, ), const SliverToBoxAdapter(child: SizedBox(height: 600)), ], otherSlivers: <Widget>[ const SliverToBoxAdapter(child: SizedBox(height: 2400)), ], )); await tester.pumpAndSettle(); final RenderSliverMainAxisGroup renderGroup = tester.renderObject(find.byType(SliverMainAxisGroup)) as RenderSliverMainAxisGroup; final RenderSliverPersistentHeader renderHeader = tester.renderObject(find.byType(SliverPersistentHeader)) as RenderSliverPersistentHeader; expect(renderGroup.geometry!.scrollExtent, equals(660)); controller.jumpTo(660); await tester.pumpAndSettle(); final TestGesture gesture = await tester.startGesture(const Offset(150.0, 300.0)); await gesture.moveBy(const Offset(0.0, 10)); await tester.pump(); // The snap animation does not go through until the gesture is released. expect(renderHeader.geometry!.paintExtent, equals(10)); expect((renderHeader.parentData! as SliverPhysicalParentData).paintOffset.dy, equals(0.0)); // Once it is released, the header's paint extent becomes the maximum and the group sets an offset of -50.0. await gesture.up(); await tester.pumpAndSettle(); expect(renderHeader.geometry!.paintExtent, equals(60)); expect((renderHeader.parentData! as SliverPhysicalParentData).paintOffset.dy, equals(-50.0)); }); testWidgets('SliverAppBar with floating: true, pinned: true, snap: true is painted within bounds of SliverMainAxisGroup', (WidgetTester tester) async { final ScrollController controller = ScrollController(); await tester.pumpWidget(_buildSliverMainAxisGroup( controller: controller, slivers: <Widget>[ const SliverAppBar( toolbarHeight: 30, expandedHeight: 60, floating: true, pinned: true, snap: true, ), const SliverToBoxAdapter(child: SizedBox(height: 600)), ], otherSlivers: <Widget>[ const SliverToBoxAdapter(child: SizedBox(height: 2400)), ], )); await tester.pumpAndSettle(); final RenderSliverMainAxisGroup renderGroup = tester.renderObject(find.byType(SliverMainAxisGroup)) as RenderSliverMainAxisGroup; final RenderSliverPersistentHeader renderHeader = tester.renderObject(find.byType(SliverPersistentHeader)) as RenderSliverPersistentHeader; expect(renderGroup.geometry!.scrollExtent, equals(660)); controller.jumpTo(660); await tester.pumpAndSettle(); final TestGesture gesture = await tester.startGesture(const Offset(150.0, 300.0)); await gesture.moveBy(const Offset(0.0, 10)); await tester.pump(); expect(renderHeader.geometry!.paintExtent, equals(30.0)); expect((renderHeader.parentData! as SliverPhysicalParentData).paintOffset.dy, equals(-20.0)); // Once we lift the gesture up, the animation should finish. await gesture.up(); await tester.pumpAndSettle(); 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: <Widget>[ 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({ double itemMainAxisExtent = 100, List<int> items = const <int>[], required Widget Function(int) label, Axis scrollDirection = Axis.vertical, }) { return SliverList( delegate: SliverChildBuilderDelegate( (BuildContext context, int i) { return scrollDirection == Axis.vertical ? SizedBox( key: ValueKey<int>(items[i]), height: itemMainAxisExtent, child: label(items[i]), ) : SizedBox( key: ValueKey<int>(items[i]), width: itemMainAxisExtent, child: label(items[i])); }, findChildIndexCallback: (Key key) { final ValueKey<int> valueKey = key as ValueKey<int>; final int index = items.indexOf(valueKey.value); return index == -1 ? null : index; }, childCount: items.length, ), ); } Widget _buildSliverMainAxisGroup({ required List<Widget> slivers, ScrollController? controller, double viewportHeight = VIEWPORT_HEIGHT, double viewportWidth = VIEWPORT_WIDTH, Axis scrollDirection = Axis.vertical, bool reverse = false, List<Widget> otherSlivers = const <Widget>[], }) { return MaterialApp( home: Directionality( textDirection: TextDirection.ltr, child: Align( alignment: Alignment.topLeft, child: SizedBox( height: viewportHeight, width: viewportWidth, child: CustomScrollView( scrollDirection: scrollDirection, reverse: reverse, controller: controller, slivers: <Widget>[SliverMainAxisGroup(slivers: slivers), ...otherSlivers], ), ), ), ), ); } class TestDelegate extends SliverPersistentHeaderDelegate { TestDelegate({ this.maxExtent = 60.0, this.minExtent = 60.0 }); @override final double maxExtent; @override final double minExtent; @override Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) { return Container(height: maxExtent); } @override bool shouldRebuild(TestDelegate oldDelegate) => true; }