826 lines
33 KiB
Dart
826 lines
33 KiB
Dart
// 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();
|
|
addTearDown(controller.dispose);
|
|
|
|
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();
|
|
addTearDown(controller.dispose);
|
|
|
|
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();
|
|
addTearDown(controller.dispose);
|
|
|
|
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();
|
|
addTearDown(controller.dispose);
|
|
|
|
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();
|
|
addTearDown(controller.dispose);
|
|
|
|
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();
|
|
addTearDown(controller.dispose);
|
|
|
|
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();
|
|
addTearDown(controller.dispose);
|
|
|
|
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();
|
|
addTearDown(controller.dispose);
|
|
|
|
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();
|
|
addTearDown(controller.dispose);
|
|
|
|
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();
|
|
addTearDown(controller.dispose);
|
|
|
|
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();
|
|
addTearDown(controller.dispose);
|
|
|
|
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();
|
|
addTearDown(controller.dispose);
|
|
|
|
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();
|
|
addTearDown(controller.dispose);
|
|
|
|
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();
|
|
addTearDown(controller.dispose);
|
|
|
|
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();
|
|
addTearDown(controller.dispose);
|
|
|
|
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));
|
|
});
|
|
|
|
testWidgets('SliverMainAxisGroup does not cause extra builds for lazy sliver children', (WidgetTester tester) async {
|
|
// By setting the correct SliverGeometry in the first SliverMainAxisGroup,
|
|
// the following SliverMainAxisGroups will not perform extra work.
|
|
final Map<int, int> buildsPerGroup = <int, int>{
|
|
0 : 0,
|
|
1 : 0,
|
|
2 : 0,
|
|
};
|
|
await tester.pumpWidget(MaterialApp(
|
|
home: CustomScrollView(
|
|
slivers: <Widget>[
|
|
for (int groupIndex = 0; groupIndex < 3; groupIndex++)
|
|
SliverMainAxisGroup(
|
|
slivers: <Widget>[
|
|
SliverList.builder(
|
|
itemCount: 100,
|
|
itemBuilder: (BuildContext context, int index) {
|
|
buildsPerGroup[groupIndex] = buildsPerGroup[groupIndex]! + 1;
|
|
return const SizedBox.square(dimension: 50);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
]
|
|
),
|
|
));
|
|
await tester.pumpAndSettle();
|
|
expect(buildsPerGroup[0], 17); // First sliver filled the screen and cache extent
|
|
expect(buildsPerGroup[1], 1); // Second only lays out one child
|
|
expect(buildsPerGroup[2], 1); // Third only lays out one child
|
|
final RenderSliverMainAxisGroup renderGroup = tester.renderObject(
|
|
find.byType(SliverMainAxisGroup).first,
|
|
) as RenderSliverMainAxisGroup;
|
|
expect(renderGroup.geometry!.cacheExtent, 850.0);
|
|
});
|
|
|
|
testWidgets('SliverMainAxisGroup correctly handles ensureVisible', (WidgetTester tester) async {
|
|
final GlobalKey key = GlobalKey();
|
|
await tester.pumpWidget(
|
|
_buildSliverMainAxisGroup(
|
|
viewportHeight: 300,
|
|
slivers: <Widget>[
|
|
const SliverToBoxAdapter(child: SizedBox(height: 300)),
|
|
SliverToBoxAdapter(child: SizedBox(key: key, height: 100)),
|
|
const SliverToBoxAdapter(child: SizedBox(height: 300)),
|
|
]
|
|
)
|
|
);
|
|
Scrollable.ensureVisible(key.currentContext!);
|
|
await tester.pumpAndSettle();
|
|
expect(tester.getTopLeft(find.byKey(key)), Offset.zero);
|
|
});
|
|
}
|
|
|
|
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;
|
|
}
|