mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
WIP Commits separated as follows: - Update lints in analysis_options files - Run `dart fix --apply` - Clean up leftover analysis issues - Run `dart format .` in the right places. Local analysis and testing passes. Checking CI now. Part of https://github.com/flutter/flutter/issues/178827 - Adoption of flutter_lints in examples/api coming in a separate change (cc @loic-sharma) ## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [ ] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [ ] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
2607 lines
92 KiB
Dart
2607 lines
92 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.
|
|
|
|
// This file is separate from viewport_caching_test.dart because we can't use
|
|
// both testWidgets and rendering_tester in the same file - testWidgets will
|
|
// initialize a binding, which rendering_tester will attempt to re-initialize
|
|
// (or vice versa).
|
|
|
|
@Tags(<String>['reduced-test-set'])
|
|
library;
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/gestures.dart';
|
|
import 'package:flutter/rendering.dart';
|
|
import 'package:flutter/widgets.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
class _TestSliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate {
|
|
_TestSliverPersistentHeaderDelegate({
|
|
this.key,
|
|
required this.minExtent,
|
|
required this.maxExtent,
|
|
this.vsync = const TestVSync(),
|
|
this.showOnScreenConfiguration = const PersistentHeaderShowOnScreenConfiguration(),
|
|
});
|
|
|
|
final Key? key;
|
|
|
|
@override
|
|
final double maxExtent;
|
|
|
|
@override
|
|
final double minExtent;
|
|
|
|
@override
|
|
final TickerProvider? vsync;
|
|
|
|
@override
|
|
final PersistentHeaderShowOnScreenConfiguration showOnScreenConfiguration;
|
|
|
|
@override
|
|
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) =>
|
|
SizedBox.expand(key: key);
|
|
|
|
@override
|
|
bool shouldRebuild(_TestSliverPersistentHeaderDelegate oldDelegate) => true;
|
|
}
|
|
|
|
void main() {
|
|
testWidgets('Scrollable widget scrollDirection update test', (WidgetTester tester) async {
|
|
final controller = ScrollController();
|
|
addTearDown(controller.dispose);
|
|
Widget buildFrame(Axis axis) {
|
|
return Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: SizedBox(
|
|
height: 100.0,
|
|
width: 100.0,
|
|
child: SingleChildScrollView(
|
|
controller: controller,
|
|
scrollDirection: axis,
|
|
child: const SizedBox(width: 200, height: 200, child: SizedBox.shrink()),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
await tester.pumpWidget(buildFrame(Axis.vertical));
|
|
expect(controller.position.pixels, 0.0);
|
|
|
|
// Change the SingleChildScrollView.scrollDirection to horizontal.
|
|
await tester.pumpWidget(buildFrame(Axis.horizontal));
|
|
expect(controller.position.pixels, 0.0);
|
|
|
|
final TestGesture gesture = await tester.startGesture(const Offset(400.0, 300.0));
|
|
// Drag in the vertical direction should not cause scrolling.
|
|
await gesture.moveBy(const Offset(0.0, 10.0));
|
|
expect(controller.position.pixels, 0.0);
|
|
await gesture.moveBy(const Offset(0.0, -10.0));
|
|
expect(controller.position.pixels, 0.0);
|
|
|
|
// Drag in the horizontal direction should cause scrolling.
|
|
await gesture.moveBy(const Offset(-10.0, 0.0));
|
|
expect(controller.position.pixels, 10.0);
|
|
await gesture.moveBy(const Offset(10.0, 0.0));
|
|
expect(controller.position.pixels, 0.0);
|
|
});
|
|
|
|
testWidgets('Viewport getOffsetToReveal - down', (WidgetTester tester) async {
|
|
final controller = ScrollController(initialScrollOffset: 300.0);
|
|
addTearDown(controller.dispose);
|
|
List<Widget> children;
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: SizedBox(
|
|
height: 200.0,
|
|
width: 300.0,
|
|
child: ListView(
|
|
controller: controller,
|
|
children: children = List<Widget>.generate(20, (int i) {
|
|
return SizedBox(height: 100.0, width: 300.0, child: Text('Tile $i'));
|
|
}),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final RenderAbstractViewport viewport = tester.allRenderObjects
|
|
.whereType<RenderAbstractViewport>()
|
|
.first;
|
|
|
|
final RenderObject target = tester.renderObject(
|
|
find.byWidget(children[5], skipOffstage: false),
|
|
);
|
|
RevealedOffset revealed = viewport.getOffsetToReveal(target, 0.0);
|
|
expect(revealed.offset, 500.0);
|
|
expect(revealed.rect, const Rect.fromLTWH(0.0, 0.0, 300.0, 100.0));
|
|
|
|
revealed = viewport.getOffsetToReveal(target, 1.0);
|
|
expect(revealed.offset, 400.0);
|
|
expect(revealed.rect, const Rect.fromLTWH(0.0, 100.0, 300.0, 100.0));
|
|
|
|
revealed = viewport.getOffsetToReveal(
|
|
target,
|
|
0.0,
|
|
rect: const Rect.fromLTWH(40.0, 40.0, 10.0, 10.0),
|
|
);
|
|
expect(revealed.offset, 540.0);
|
|
expect(revealed.rect, const Rect.fromLTWH(40.0, 0.0, 10.0, 10.0));
|
|
|
|
revealed = viewport.getOffsetToReveal(
|
|
target,
|
|
1.0,
|
|
rect: const Rect.fromLTWH(40.0, 40.0, 10.0, 10.0),
|
|
);
|
|
expect(revealed.offset, 350.0);
|
|
expect(revealed.rect, const Rect.fromLTWH(40.0, 190.0, 10.0, 10.0));
|
|
});
|
|
|
|
testWidgets('Viewport getOffsetToReveal - right', (WidgetTester tester) async {
|
|
final controller = ScrollController(initialScrollOffset: 300.0);
|
|
addTearDown(controller.dispose);
|
|
List<Widget> children;
|
|
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: SizedBox(
|
|
height: 300.0,
|
|
width: 200.0,
|
|
child: ListView(
|
|
scrollDirection: Axis.horizontal,
|
|
controller: controller,
|
|
children: children = List<Widget>.generate(20, (int i) {
|
|
return SizedBox(height: 300.0, width: 100.0, child: Text('Tile $i'));
|
|
}),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final RenderAbstractViewport viewport = tester.allRenderObjects
|
|
.whereType<RenderAbstractViewport>()
|
|
.first;
|
|
|
|
final RenderObject target = tester.renderObject(
|
|
find.byWidget(children[5], skipOffstage: false),
|
|
);
|
|
RevealedOffset revealed = viewport.getOffsetToReveal(target, 0.0);
|
|
expect(revealed.offset, 500.0);
|
|
expect(revealed.rect, const Rect.fromLTWH(0.0, 0.0, 100.0, 300.0));
|
|
|
|
revealed = viewport.getOffsetToReveal(target, 1.0);
|
|
expect(revealed.offset, 400.0);
|
|
expect(revealed.rect, const Rect.fromLTWH(100.0, 0.0, 100.0, 300.0));
|
|
|
|
revealed = viewport.getOffsetToReveal(
|
|
target,
|
|
0.0,
|
|
rect: const Rect.fromLTWH(40.0, 40.0, 10.0, 10.0),
|
|
);
|
|
expect(revealed.offset, 540.0);
|
|
expect(revealed.rect, const Rect.fromLTWH(0.0, 40.0, 10.0, 10.0));
|
|
|
|
revealed = viewport.getOffsetToReveal(
|
|
target,
|
|
1.0,
|
|
rect: const Rect.fromLTWH(40.0, 40.0, 10.0, 10.0),
|
|
);
|
|
expect(revealed.offset, 350.0);
|
|
expect(revealed.rect, const Rect.fromLTWH(190.0, 40.0, 10.0, 10.0));
|
|
});
|
|
|
|
testWidgets('Viewport getOffsetToReveal - up', (WidgetTester tester) async {
|
|
final controller = ScrollController(initialScrollOffset: 300.0);
|
|
addTearDown(controller.dispose);
|
|
List<Widget> children;
|
|
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: SizedBox(
|
|
height: 200.0,
|
|
width: 300.0,
|
|
child: ListView(
|
|
controller: controller,
|
|
reverse: true,
|
|
children: children = List<Widget>.generate(20, (int i) {
|
|
return SizedBox(height: 100.0, width: 300.0, child: Text('Tile $i'));
|
|
}),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final RenderAbstractViewport viewport = tester.allRenderObjects
|
|
.whereType<RenderAbstractViewport>()
|
|
.first;
|
|
|
|
final RenderObject target = tester.renderObject(
|
|
find.byWidget(children[5], skipOffstage: false),
|
|
);
|
|
RevealedOffset revealed = viewport.getOffsetToReveal(target, 0.0);
|
|
expect(revealed.offset, 500.0);
|
|
expect(revealed.rect, const Rect.fromLTWH(0.0, 100.0, 300.0, 100.0));
|
|
|
|
revealed = viewport.getOffsetToReveal(target, 1.0);
|
|
expect(revealed.offset, 400.0);
|
|
expect(revealed.rect, const Rect.fromLTWH(0.0, 0.0, 300.0, 100.0));
|
|
|
|
revealed = viewport.getOffsetToReveal(
|
|
target,
|
|
0.0,
|
|
rect: const Rect.fromLTWH(40.0, 40.0, 10.0, 10.0),
|
|
);
|
|
expect(revealed.offset, 550.0);
|
|
expect(revealed.rect, const Rect.fromLTWH(40.0, 190.0, 10.0, 10.0));
|
|
|
|
revealed = viewport.getOffsetToReveal(
|
|
target,
|
|
1.0,
|
|
rect: const Rect.fromLTWH(40.0, 40.0, 10.0, 10.0),
|
|
);
|
|
expect(revealed.offset, 360.0);
|
|
expect(revealed.rect, const Rect.fromLTWH(40.0, 0.0, 10.0, 10.0));
|
|
});
|
|
|
|
testWidgets('Viewport getOffsetToReveal - left', (WidgetTester tester) async {
|
|
final controller = ScrollController(initialScrollOffset: 300.0);
|
|
addTearDown(controller.dispose);
|
|
List<Widget> children;
|
|
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: SizedBox(
|
|
height: 300.0,
|
|
width: 200.0,
|
|
child: ListView(
|
|
scrollDirection: Axis.horizontal,
|
|
reverse: true,
|
|
controller: controller,
|
|
children: children = List<Widget>.generate(20, (int i) {
|
|
return SizedBox(height: 300.0, width: 100.0, child: Text('Tile $i'));
|
|
}),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final RenderAbstractViewport viewport = tester.allRenderObjects
|
|
.whereType<RenderAbstractViewport>()
|
|
.first;
|
|
|
|
final RenderObject target = tester.renderObject(
|
|
find.byWidget(children[5], skipOffstage: false),
|
|
);
|
|
RevealedOffset revealed = viewport.getOffsetToReveal(target, 0.0);
|
|
expect(revealed.offset, 500.0);
|
|
expect(revealed.rect, const Rect.fromLTWH(100.0, 0.0, 100.0, 300.0));
|
|
|
|
revealed = viewport.getOffsetToReveal(target, 1.0);
|
|
expect(revealed.offset, 400.0);
|
|
expect(revealed.rect, const Rect.fromLTWH(0.0, 0.0, 100.0, 300.0));
|
|
|
|
revealed = viewport.getOffsetToReveal(
|
|
target,
|
|
0.0,
|
|
rect: const Rect.fromLTWH(40.0, 40.0, 10.0, 10.0),
|
|
);
|
|
expect(revealed.offset, 550.0);
|
|
expect(revealed.rect, const Rect.fromLTWH(190.0, 40.0, 10.0, 10.0));
|
|
|
|
revealed = viewport.getOffsetToReveal(
|
|
target,
|
|
1.0,
|
|
rect: const Rect.fromLTWH(40.0, 40.0, 10.0, 10.0),
|
|
);
|
|
expect(revealed.offset, 360.0);
|
|
expect(revealed.rect, const Rect.fromLTWH(0.0, 40.0, 10.0, 10.0));
|
|
});
|
|
|
|
testWidgets('Viewport getOffsetToReveal Sliver - down', (WidgetTester tester) async {
|
|
final controller = ScrollController(initialScrollOffset: 300.0);
|
|
addTearDown(controller.dispose);
|
|
final children = <Widget>[];
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: SizedBox(
|
|
height: 200.0,
|
|
width: 300.0,
|
|
child: CustomScrollView(
|
|
controller: controller,
|
|
slivers: List<Widget>.generate(20, (int i) {
|
|
final Widget sliver = SliverToBoxAdapter(
|
|
child: SizedBox(height: 100.0, child: Text('Tile $i')),
|
|
);
|
|
children.add(sliver);
|
|
return SliverPadding(
|
|
padding: const EdgeInsets.only(top: 22.0, bottom: 23.0),
|
|
sliver: sliver,
|
|
);
|
|
}),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final RenderAbstractViewport viewport = tester.allRenderObjects
|
|
.whereType<RenderAbstractViewport>()
|
|
.first;
|
|
|
|
final RenderObject target = tester.renderObject(
|
|
find.byWidget(children[5], skipOffstage: false),
|
|
);
|
|
RevealedOffset revealed = viewport.getOffsetToReveal(target, 0.0);
|
|
expect(revealed.offset, 5 * (100 + 22 + 23) + 22);
|
|
|
|
revealed = viewport.getOffsetToReveal(target, 1.0);
|
|
expect(revealed.offset, 5 * (100 + 22 + 23) + 22 - 100);
|
|
|
|
// With rect specified.
|
|
revealed = viewport.getOffsetToReveal(target, 0.0, rect: const Rect.fromLTRB(1, 2, 3, 4));
|
|
expect(revealed.offset, 5 * (100 + 22 + 23) + 22 + 2);
|
|
revealed = viewport.getOffsetToReveal(target, 1.0, rect: const Rect.fromLTRB(1, 2, 3, 4));
|
|
expect(revealed.offset, 5 * (100 + 22 + 23) + 22 - (200 - 4));
|
|
});
|
|
|
|
testWidgets('Viewport getOffsetToReveal Sliver - right', (WidgetTester tester) async {
|
|
final controller = ScrollController(initialScrollOffset: 300.0);
|
|
addTearDown(controller.dispose);
|
|
final children = <Widget>[];
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: SizedBox(
|
|
height: 300.0,
|
|
width: 200.0,
|
|
child: CustomScrollView(
|
|
scrollDirection: Axis.horizontal,
|
|
controller: controller,
|
|
slivers: List<Widget>.generate(20, (int i) {
|
|
final Widget sliver = SliverToBoxAdapter(
|
|
child: SizedBox(width: 100.0, child: Text('Tile $i')),
|
|
);
|
|
children.add(sliver);
|
|
return SliverPadding(
|
|
padding: const EdgeInsets.only(left: 22.0, right: 23.0),
|
|
sliver: sliver,
|
|
);
|
|
}),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final RenderAbstractViewport viewport = tester.allRenderObjects
|
|
.whereType<RenderAbstractViewport>()
|
|
.first;
|
|
|
|
final RenderObject target = tester.renderObject(
|
|
find.byWidget(children[5], skipOffstage: false),
|
|
);
|
|
RevealedOffset revealed = viewport.getOffsetToReveal(target, 0.0);
|
|
expect(revealed.offset, 5 * (100 + 22 + 23) + 22);
|
|
|
|
revealed = viewport.getOffsetToReveal(target, 1.0);
|
|
expect(revealed.offset, 5 * (100 + 22 + 23) + 22 - 100);
|
|
|
|
// With rect specified.
|
|
revealed = viewport.getOffsetToReveal(target, 0.0, rect: const Rect.fromLTRB(1, 2, 3, 4));
|
|
expect(revealed.offset, 5 * (100 + 22 + 23) + 22 + 1);
|
|
revealed = viewport.getOffsetToReveal(target, 1.0, rect: const Rect.fromLTRB(1, 2, 3, 4));
|
|
expect(revealed.offset, 5 * (100 + 22 + 23) + 22 - (200 - 3));
|
|
});
|
|
|
|
testWidgets('Viewport getOffsetToReveal Sliver - up', (WidgetTester tester) async {
|
|
final controller = ScrollController(initialScrollOffset: 300.0);
|
|
addTearDown(controller.dispose);
|
|
final children = <Widget>[];
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: SizedBox(
|
|
height: 200.0,
|
|
width: 300.0,
|
|
child: CustomScrollView(
|
|
controller: controller,
|
|
reverse: true,
|
|
slivers: List<Widget>.generate(20, (int i) {
|
|
final Widget sliver = SliverToBoxAdapter(
|
|
child: SizedBox(height: 100.0, child: Text('Tile $i')),
|
|
);
|
|
children.add(sliver);
|
|
return SliverPadding(
|
|
padding: const EdgeInsets.only(top: 22.0, bottom: 23.0),
|
|
sliver: sliver,
|
|
);
|
|
}),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final RenderAbstractViewport viewport = tester.allRenderObjects
|
|
.whereType<RenderAbstractViewport>()
|
|
.first;
|
|
|
|
final RenderObject target = tester.renderObject(
|
|
find.byWidget(children[5], skipOffstage: false),
|
|
);
|
|
RevealedOffset revealed = viewport.getOffsetToReveal(target, 0.0);
|
|
// Does not include the bottom padding of children[5] thus + 23 instead of + 22.
|
|
expect(revealed.offset, 5 * (100 + 22 + 23) + 23);
|
|
|
|
revealed = viewport.getOffsetToReveal(target, 1.0);
|
|
expect(revealed.offset, 5 * (100 + 22 + 23) + 23 - 100);
|
|
|
|
// With rect specified.
|
|
revealed = viewport.getOffsetToReveal(target, 0.0, rect: const Rect.fromLTRB(1, 2, 3, 4));
|
|
expect(revealed.offset, 5 * (100 + 22 + 23) + 23 + (100 - 4));
|
|
revealed = viewport.getOffsetToReveal(target, 1.0, rect: const Rect.fromLTRB(1, 2, 3, 4));
|
|
expect(revealed.offset, -200 + 6 * (100 + 22 + 23) - 22 - 2);
|
|
});
|
|
|
|
testWidgets('Viewport getOffsetToReveal Sliver - up - reverse growth', (
|
|
WidgetTester tester,
|
|
) async {
|
|
const Key centerKey = ValueKey<String>('center');
|
|
const padding = EdgeInsets.only(top: 22.0, bottom: 23.0);
|
|
const Widget centerSliver = SliverPadding(
|
|
key: centerKey,
|
|
padding: padding,
|
|
sliver: SliverToBoxAdapter(child: SizedBox(height: 100.0, child: Text('Tile center'))),
|
|
);
|
|
const Widget lowerItem = SizedBox(height: 100.0, child: Text('Tile lower'));
|
|
const Widget lowerSliver = SliverPadding(
|
|
padding: padding,
|
|
sliver: SliverToBoxAdapter(child: lowerItem),
|
|
);
|
|
|
|
await tester.pumpWidget(
|
|
const Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: SizedBox(
|
|
height: 200.0,
|
|
width: 300.0,
|
|
child: CustomScrollView(
|
|
center: centerKey,
|
|
reverse: true,
|
|
slivers: <Widget>[lowerSliver, centerSliver],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final RenderAbstractViewport viewport = tester.allRenderObjects
|
|
.whereType<RenderAbstractViewport>()
|
|
.first;
|
|
|
|
final RenderObject target = tester.renderObject(find.byWidget(lowerItem, skipOffstage: false));
|
|
RevealedOffset revealed = viewport.getOffsetToReveal(target, 0.0);
|
|
expect(revealed.offset, -100 - 22);
|
|
|
|
revealed = viewport.getOffsetToReveal(target, 1.0);
|
|
expect(revealed.offset, -100 - 22 - 100);
|
|
|
|
// With rect specified.
|
|
revealed = viewport.getOffsetToReveal(target, 0.0, rect: const Rect.fromLTRB(1, 2, 3, 4));
|
|
expect(revealed.offset, -22 - 4);
|
|
revealed = viewport.getOffsetToReveal(target, 1.0, rect: const Rect.fromLTRB(1, 2, 3, 4));
|
|
expect(revealed.offset, -200 - 22 - 2);
|
|
});
|
|
|
|
testWidgets('Viewport getOffsetToReveal Sliver - left - reverse growth', (
|
|
WidgetTester tester,
|
|
) async {
|
|
const Key centerKey = ValueKey<String>('center');
|
|
const padding = EdgeInsets.only(left: 22.0, right: 23.0);
|
|
const Widget centerSliver = SliverPadding(
|
|
key: centerKey,
|
|
padding: padding,
|
|
sliver: SliverToBoxAdapter(child: SizedBox(width: 100.0, child: Text('Tile center'))),
|
|
);
|
|
const Widget lowerItem = SizedBox(width: 100.0, child: Text('Tile lower'));
|
|
const Widget lowerSliver = SliverPadding(
|
|
padding: padding,
|
|
sliver: SliverToBoxAdapter(child: lowerItem),
|
|
);
|
|
|
|
await tester.pumpWidget(
|
|
const Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: SizedBox(
|
|
height: 200.0,
|
|
width: 300.0,
|
|
child: CustomScrollView(
|
|
scrollDirection: Axis.horizontal,
|
|
center: centerKey,
|
|
reverse: true,
|
|
slivers: <Widget>[lowerSliver, centerSliver],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final RenderAbstractViewport viewport = tester.allRenderObjects
|
|
.whereType<RenderAbstractViewport>()
|
|
.first;
|
|
|
|
final RenderObject target = tester.renderObject(find.byWidget(lowerItem, skipOffstage: false));
|
|
RevealedOffset revealed = viewport.getOffsetToReveal(target, 0.0);
|
|
expect(revealed.offset, -100 - 22);
|
|
|
|
revealed = viewport.getOffsetToReveal(target, 1.0);
|
|
expect(revealed.offset, -100 - 22 - 200);
|
|
|
|
// With rect specified.
|
|
revealed = viewport.getOffsetToReveal(target, 0.0, rect: const Rect.fromLTRB(1, 2, 3, 4));
|
|
expect(revealed.offset, -22 - 3);
|
|
revealed = viewport.getOffsetToReveal(target, 1.0, rect: const Rect.fromLTRB(1, 2, 3, 4));
|
|
expect(revealed.offset, -300 - 22 - 1);
|
|
});
|
|
|
|
testWidgets('Viewport getOffsetToReveal Sliver - left', (WidgetTester tester) async {
|
|
final controller = ScrollController(initialScrollOffset: 300.0);
|
|
addTearDown(controller.dispose);
|
|
final children = <Widget>[];
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: SizedBox(
|
|
height: 300.0,
|
|
width: 200.0,
|
|
child: CustomScrollView(
|
|
scrollDirection: Axis.horizontal,
|
|
reverse: true,
|
|
controller: controller,
|
|
slivers: List<Widget>.generate(20, (int i) {
|
|
final Widget sliver = SliverToBoxAdapter(
|
|
child: SizedBox(width: 100.0, child: Text('Tile $i')),
|
|
);
|
|
children.add(sliver);
|
|
return SliverPadding(
|
|
padding: const EdgeInsets.only(left: 22.0, right: 23.0),
|
|
sliver: sliver,
|
|
);
|
|
}),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final RenderAbstractViewport viewport = tester.allRenderObjects
|
|
.whereType<RenderAbstractViewport>()
|
|
.first;
|
|
|
|
final RenderObject target = tester.renderObject(
|
|
find.byWidget(children[5], skipOffstage: false),
|
|
);
|
|
RevealedOffset revealed = viewport.getOffsetToReveal(target, 0.0);
|
|
expect(revealed.offset, 5 * (100 + 22 + 23) + 23);
|
|
|
|
revealed = viewport.getOffsetToReveal(target, 1.0);
|
|
expect(revealed.offset, 5 * (100 + 22 + 23) + 23 - 100);
|
|
|
|
// With rect specified.
|
|
revealed = viewport.getOffsetToReveal(target, 0.0, rect: const Rect.fromLTRB(1, 2, 3, 4));
|
|
expect(revealed.offset, 6 * (100 + 22 + 23) - 22 - 3);
|
|
revealed = viewport.getOffsetToReveal(target, 1.0, rect: const Rect.fromLTRB(1, 2, 3, 4));
|
|
expect(revealed.offset, -200 + 6 * (100 + 22 + 23) - 22 - 1);
|
|
});
|
|
|
|
testWidgets('Nested Viewports showOnScreen', (WidgetTester tester) async {
|
|
final controllersX = List<ScrollController>.generate(
|
|
10,
|
|
(int i) => ScrollController(initialScrollOffset: 400.0),
|
|
);
|
|
final controllerY = ScrollController(initialScrollOffset: 400.0);
|
|
|
|
addTearDown(() {
|
|
controllerY.dispose();
|
|
for (final controller in controllersX) {
|
|
controller.dispose();
|
|
}
|
|
});
|
|
|
|
final children = List<List<Widget>>.generate(10, (int y) {
|
|
return List<Widget>.generate(10, (int x) {
|
|
return SizedBox(height: 100.0, width: 100.0, child: Text('$x,$y'));
|
|
});
|
|
});
|
|
|
|
/// Builds a grid:
|
|
///
|
|
/// <- x ->
|
|
/// 0 1 2 3 4 5 6 7 8 9
|
|
/// 0 c c c c c c c c c c
|
|
/// 1 c c c c c c c c c c
|
|
/// 2 c c c c c c c c c c
|
|
/// 3 c c c c c c c c c c y
|
|
/// 4 c c c c v v c c c c
|
|
/// 5 c c c c v v c c c c
|
|
/// 6 c c c c c c c c c c
|
|
/// 7 c c c c c c c c c c
|
|
/// 8 c c c c c c c c c c
|
|
/// 9 c c c c c c c c c c
|
|
///
|
|
/// Each c is a 100x100 container, v are containers visible in initial
|
|
/// viewport.
|
|
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: SizedBox(
|
|
height: 200.0,
|
|
width: 200.0,
|
|
child: ListView(
|
|
controller: controllerY,
|
|
children: List<Widget>.generate(10, (int y) {
|
|
return SizedBox(
|
|
height: 100.0,
|
|
child: ListView(
|
|
scrollDirection: Axis.horizontal,
|
|
controller: controllersX[y],
|
|
children: children[y],
|
|
),
|
|
);
|
|
}),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Already in viewport
|
|
tester.renderObject(find.byWidget(children[4][4], skipOffstage: false)).showOnScreen();
|
|
await tester.pumpAndSettle();
|
|
expect(controllersX[4].offset, 400.0);
|
|
expect(controllerY.offset, 400.0);
|
|
|
|
controllersX[4].jumpTo(400.0);
|
|
controllerY.jumpTo(400.0);
|
|
await tester.pumpAndSettle();
|
|
|
|
// Above viewport
|
|
tester.renderObject(find.byWidget(children[3][4], skipOffstage: false)).showOnScreen();
|
|
await tester.pumpAndSettle();
|
|
expect(controllersX[3].offset, 400.0);
|
|
expect(controllerY.offset, 300.0);
|
|
|
|
controllersX[3].jumpTo(400.0);
|
|
controllerY.jumpTo(400.0);
|
|
await tester.pumpAndSettle();
|
|
|
|
// Below viewport
|
|
tester.renderObject(find.byWidget(children[6][4], skipOffstage: false)).showOnScreen();
|
|
await tester.pumpAndSettle();
|
|
expect(controllersX[6].offset, 400.0);
|
|
expect(controllerY.offset, 500.0);
|
|
|
|
controllersX[6].jumpTo(400.0);
|
|
controllerY.jumpTo(400.0);
|
|
await tester.pumpAndSettle();
|
|
|
|
// Left of viewport
|
|
tester.renderObject(find.byWidget(children[4][3], skipOffstage: false)).showOnScreen();
|
|
await tester.pumpAndSettle();
|
|
expect(controllersX[4].offset, 300.0);
|
|
expect(controllerY.offset, 400.0);
|
|
|
|
controllersX[4].jumpTo(400.0);
|
|
controllerY.jumpTo(400.0);
|
|
await tester.pumpAndSettle();
|
|
|
|
// Right of viewport
|
|
tester.renderObject(find.byWidget(children[4][6], skipOffstage: false)).showOnScreen();
|
|
await tester.pumpAndSettle();
|
|
expect(controllersX[4].offset, 500.0);
|
|
expect(controllerY.offset, 400.0);
|
|
|
|
controllersX[4].jumpTo(400.0);
|
|
controllerY.jumpTo(400.0);
|
|
await tester.pumpAndSettle();
|
|
|
|
// Above and left of viewport
|
|
tester.renderObject(find.byWidget(children[3][3], skipOffstage: false)).showOnScreen();
|
|
await tester.pumpAndSettle();
|
|
expect(controllersX[3].offset, 300.0);
|
|
expect(controllerY.offset, 300.0);
|
|
|
|
controllersX[3].jumpTo(400.0);
|
|
controllerY.jumpTo(400.0);
|
|
await tester.pumpAndSettle();
|
|
|
|
// Below and left of viewport
|
|
tester.renderObject(find.byWidget(children[6][3], skipOffstage: false)).showOnScreen();
|
|
await tester.pumpAndSettle();
|
|
expect(controllersX[6].offset, 300.0);
|
|
expect(controllerY.offset, 500.0);
|
|
|
|
controllersX[6].jumpTo(400.0);
|
|
controllerY.jumpTo(400.0);
|
|
await tester.pumpAndSettle();
|
|
|
|
// Above and right of viewport
|
|
tester.renderObject(find.byWidget(children[3][6], skipOffstage: false)).showOnScreen();
|
|
await tester.pumpAndSettle();
|
|
expect(controllersX[3].offset, 500.0);
|
|
expect(controllerY.offset, 300.0);
|
|
|
|
controllersX[3].jumpTo(400.0);
|
|
controllerY.jumpTo(400.0);
|
|
await tester.pumpAndSettle();
|
|
|
|
// Below and right of viewport
|
|
tester.renderObject(find.byWidget(children[6][6], skipOffstage: false)).showOnScreen();
|
|
await tester.pumpAndSettle();
|
|
expect(controllersX[6].offset, 500.0);
|
|
expect(controllerY.offset, 500.0);
|
|
|
|
controllersX[6].jumpTo(400.0);
|
|
controllerY.jumpTo(400.0);
|
|
await tester.pumpAndSettle();
|
|
|
|
// Below and right of viewport with animations
|
|
tester
|
|
.renderObject(find.byWidget(children[6][6], skipOffstage: false))
|
|
.showOnScreen(duration: const Duration(seconds: 2));
|
|
await tester.pump();
|
|
await tester.pump(const Duration(seconds: 1));
|
|
expect(tester.hasRunningAnimations, isTrue);
|
|
expect(controllersX[6].offset, greaterThan(400.0));
|
|
expect(controllersX[6].offset, lessThan(500.0));
|
|
expect(controllerY.offset, greaterThan(400.0));
|
|
expect(controllerY.offset, lessThan(500.0));
|
|
await tester.pumpAndSettle();
|
|
expect(controllersX[6].offset, 500.0);
|
|
expect(controllerY.offset, 500.0);
|
|
});
|
|
|
|
group('Nested viewports (same orientation) showOnScreen', () {
|
|
final children = List<Widget>.generate(10, (int i) {
|
|
return SizedBox(height: 100.0, width: 300.0, child: Text('$i'));
|
|
});
|
|
|
|
Future<void> buildNestedScroller({
|
|
required WidgetTester tester,
|
|
required ScrollController inner,
|
|
required ScrollController outer,
|
|
}) {
|
|
return tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: SizedBox(
|
|
height: 200.0,
|
|
width: 300.0,
|
|
child: ListView(
|
|
controller: outer,
|
|
children: <Widget>[
|
|
const SizedBox(height: 200.0),
|
|
SizedBox(
|
|
height: 200.0,
|
|
width: 300.0,
|
|
child: ListView(controller: inner, children: children),
|
|
),
|
|
const SizedBox(height: 200.0),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
testWidgets('Reverse List showOnScreen', (WidgetTester tester) async {
|
|
addTearDown(tester.view.reset);
|
|
const screenHeight = 400.0;
|
|
const screenWidth = 400.0;
|
|
const double itemHeight = screenHeight / 10.0;
|
|
const centerKey = ValueKey<String>('center');
|
|
|
|
tester.view.devicePixelRatio = 1.0;
|
|
tester.view.physicalSize = const Size(screenWidth, screenHeight);
|
|
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: CustomScrollView(
|
|
center: centerKey,
|
|
reverse: true,
|
|
slivers: <Widget>[
|
|
SliverList.builder(
|
|
itemCount: 10,
|
|
itemBuilder: (BuildContext context, int index) {
|
|
return SizedBox(height: itemHeight, child: Text('Item ${-index - 1}'));
|
|
},
|
|
),
|
|
SliverList.list(
|
|
key: centerKey,
|
|
children: const <Widget>[SizedBox(height: itemHeight, child: Text('Item 0'))],
|
|
),
|
|
SliverList.builder(
|
|
itemCount: 10,
|
|
itemBuilder: (BuildContext context, int index) {
|
|
return SizedBox(height: itemHeight, child: Text('Item ${index + 1}'));
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(find.text('Item -1'), findsNothing);
|
|
|
|
final RenderBox itemNeg1 = tester.renderObject(find.text('Item -1', skipOffstage: false));
|
|
|
|
itemNeg1.showOnScreen(duration: const Duration(seconds: 1));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('Item -1'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('in view in inner, but not in outer', (WidgetTester tester) async {
|
|
final inner = ScrollController();
|
|
final outer = ScrollController();
|
|
addTearDown(inner.dispose);
|
|
addTearDown(outer.dispose);
|
|
|
|
await buildNestedScroller(tester: tester, inner: inner, outer: outer);
|
|
expect(outer.offset, 0.0);
|
|
expect(inner.offset, 0.0);
|
|
|
|
tester.renderObject(find.byWidget(children[0], skipOffstage: false)).showOnScreen();
|
|
await tester.pumpAndSettle();
|
|
expect(inner.offset, 0.0);
|
|
expect(outer.offset, 100.0);
|
|
});
|
|
|
|
testWidgets('not in view of neither inner nor outer', (WidgetTester tester) async {
|
|
final inner = ScrollController();
|
|
final outer = ScrollController();
|
|
addTearDown(inner.dispose);
|
|
addTearDown(outer.dispose);
|
|
|
|
await buildNestedScroller(tester: tester, inner: inner, outer: outer);
|
|
expect(outer.offset, 0.0);
|
|
expect(inner.offset, 0.0);
|
|
|
|
tester.renderObject(find.byWidget(children[4], skipOffstage: false)).showOnScreen();
|
|
await tester.pumpAndSettle();
|
|
expect(inner.offset, 300.0);
|
|
expect(outer.offset, 200.0);
|
|
});
|
|
|
|
testWidgets('in view in inner and outer', (WidgetTester tester) async {
|
|
final inner = ScrollController(initialScrollOffset: 200.0);
|
|
final outer = ScrollController(initialScrollOffset: 200.0);
|
|
addTearDown(inner.dispose);
|
|
addTearDown(outer.dispose);
|
|
|
|
await buildNestedScroller(tester: tester, inner: inner, outer: outer);
|
|
expect(outer.offset, 200.0);
|
|
expect(inner.offset, 200.0);
|
|
|
|
tester.renderObject(find.byWidget(children[2])).showOnScreen();
|
|
await tester.pumpAndSettle();
|
|
expect(outer.offset, 200.0);
|
|
expect(inner.offset, 200.0);
|
|
});
|
|
|
|
testWidgets('inner shown in outer, but item not visible', (WidgetTester tester) async {
|
|
final inner = ScrollController(initialScrollOffset: 200.0);
|
|
final outer = ScrollController(initialScrollOffset: 200.0);
|
|
addTearDown(inner.dispose);
|
|
addTearDown(outer.dispose);
|
|
|
|
await buildNestedScroller(tester: tester, inner: inner, outer: outer);
|
|
expect(outer.offset, 200.0);
|
|
expect(inner.offset, 200.0);
|
|
|
|
tester.renderObject(find.byWidget(children[5], skipOffstage: false)).showOnScreen();
|
|
await tester.pumpAndSettle();
|
|
expect(outer.offset, 200.0);
|
|
expect(inner.offset, 400.0);
|
|
});
|
|
|
|
testWidgets('inner half shown in outer, item only visible in inner', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final inner = ScrollController();
|
|
final outer = ScrollController(initialScrollOffset: 100.0);
|
|
addTearDown(inner.dispose);
|
|
addTearDown(outer.dispose);
|
|
|
|
await buildNestedScroller(tester: tester, inner: inner, outer: outer);
|
|
expect(outer.offset, 100.0);
|
|
expect(inner.offset, 0.0);
|
|
|
|
tester.renderObject(find.byWidget(children[1])).showOnScreen();
|
|
await tester.pumpAndSettle();
|
|
expect(outer.offset, 200.0);
|
|
expect(inner.offset, 0.0);
|
|
});
|
|
});
|
|
|
|
testWidgets(
|
|
'Nested Viewports showOnScreen with allowImplicitScrolling=false for inner viewport',
|
|
(WidgetTester tester) async {
|
|
// Regression test for https://github.com/flutter/flutter/issues/20893.
|
|
|
|
List<Widget> slivers;
|
|
final controllerX = ScrollController();
|
|
final controllerY = ScrollController();
|
|
addTearDown(controllerX.dispose);
|
|
addTearDown(controllerY.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: SizedBox(
|
|
height: 200.0,
|
|
width: 200.0,
|
|
child: ListView(
|
|
controller: controllerY,
|
|
children: <Widget>[
|
|
const SizedBox(height: 150.0),
|
|
SizedBox(
|
|
height: 100.0,
|
|
child: ListView(
|
|
physics: const PageScrollPhysics(), // Turns off `allowImplicitScrolling`
|
|
scrollDirection: Axis.horizontal,
|
|
controller: controllerX,
|
|
children: slivers = <Widget>[
|
|
Container(width: 150.0),
|
|
Container(width: 150.0),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 150.0),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
tester.renderObject(find.byWidget(slivers[1])).showOnScreen();
|
|
await tester.pumpAndSettle();
|
|
expect(controllerX.offset, 0.0);
|
|
expect(controllerY.offset, 50.0);
|
|
},
|
|
);
|
|
|
|
testWidgets(
|
|
'Nested Viewports showOnScreen on Sliver with allowImplicitScrolling=false for inner viewport',
|
|
(WidgetTester tester) async {
|
|
Widget sliver;
|
|
final controllerX = ScrollController();
|
|
final controllerY = ScrollController();
|
|
addTearDown(controllerX.dispose);
|
|
addTearDown(controllerY.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: SizedBox(
|
|
height: 200.0,
|
|
width: 200.0,
|
|
child: ListView(
|
|
controller: controllerY,
|
|
children: <Widget>[
|
|
const SizedBox(height: 150.0),
|
|
SizedBox(
|
|
height: 100.0,
|
|
child: CustomScrollView(
|
|
physics: const PageScrollPhysics(), // Turns off `allowImplicitScrolling`
|
|
scrollDirection: Axis.horizontal,
|
|
controller: controllerX,
|
|
slivers: <Widget>[
|
|
SliverPadding(
|
|
padding: const EdgeInsets.all(25.0),
|
|
sliver: SliverToBoxAdapter(child: Container(width: 100.0)),
|
|
),
|
|
SliverPadding(
|
|
padding: const EdgeInsets.all(25.0),
|
|
sliver: sliver = SliverToBoxAdapter(child: Container(width: 100.0)),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 150.0),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
tester.renderObject(find.byWidget(sliver)).showOnScreen();
|
|
await tester.pumpAndSettle();
|
|
expect(controllerX.offset, 0.0);
|
|
expect(controllerY.offset, 25.0);
|
|
},
|
|
);
|
|
|
|
testWidgets('Viewport showOnScreen with objects larger than viewport', (
|
|
WidgetTester tester,
|
|
) async {
|
|
List<Widget> children;
|
|
final controller = ScrollController(initialScrollOffset: 300.0);
|
|
addTearDown(controller.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: SizedBox(
|
|
height: 200.0,
|
|
child: ListView(
|
|
controller: controller,
|
|
children: children = List<Widget>.generate(20, (int i) {
|
|
return SizedBox(height: 300.0, child: Text('Tile $i'));
|
|
}),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(controller.offset, 300.0);
|
|
|
|
// Already aligned with leading edge, nothing happens.
|
|
tester.renderObject(find.byWidget(children[1], skipOffstage: false)).showOnScreen();
|
|
await tester.pumpAndSettle();
|
|
expect(controller.offset, 300.0);
|
|
|
|
// Above leading edge aligns trailing edges
|
|
tester.renderObject(find.byWidget(children[0], skipOffstage: false)).showOnScreen();
|
|
await tester.pumpAndSettle();
|
|
expect(controller.offset, 100.0);
|
|
|
|
// Below trailing edge aligns leading edges
|
|
tester.renderObject(find.byWidget(children[1], skipOffstage: false)).showOnScreen();
|
|
await tester.pumpAndSettle();
|
|
expect(controller.offset, 300.0);
|
|
|
|
controller.jumpTo(250.0);
|
|
await tester.pumpAndSettle();
|
|
expect(controller.offset, 250.0);
|
|
|
|
// Partly visible across leading edge aligns trailing edges
|
|
tester.renderObject(find.byWidget(children[0], skipOffstage: false)).showOnScreen();
|
|
await tester.pumpAndSettle();
|
|
expect(controller.offset, 100.0);
|
|
|
|
controller.jumpTo(150.0);
|
|
await tester.pumpAndSettle();
|
|
expect(controller.offset, 150.0);
|
|
|
|
// Partly visible across trailing edge aligns leading edges
|
|
tester.renderObject(find.byWidget(children[1], skipOffstage: false)).showOnScreen();
|
|
await tester.pumpAndSettle();
|
|
expect(controller.offset, 300.0);
|
|
});
|
|
|
|
testWidgets(
|
|
'Viewport showOnScreen should not scroll if the rect is already visible, even if it does not scroll linearly',
|
|
(WidgetTester tester) async {
|
|
List<Widget> children;
|
|
final controller = ScrollController(initialScrollOffset: 300.0);
|
|
addTearDown(controller.dispose);
|
|
|
|
const headerKey = Key('header');
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: SizedBox(
|
|
height: 600.0,
|
|
child: CustomScrollView(
|
|
controller: controller,
|
|
slivers: children = List<Widget>.generate(20, (int i) {
|
|
return i == 10
|
|
? SliverPersistentHeader(
|
|
pinned: true,
|
|
delegate: _TestSliverPersistentHeaderDelegate(
|
|
minExtent: 100,
|
|
maxExtent: 300,
|
|
key: headerKey,
|
|
),
|
|
)
|
|
: SliverToBoxAdapter(child: SizedBox(height: 300.0, child: Text('Tile $i')));
|
|
}),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
controller.jumpTo(300.0 * 15);
|
|
await tester.pumpAndSettle();
|
|
|
|
final Finder pinnedHeaderContent = find.descendant(
|
|
of: find.byWidget(children[10]),
|
|
matching: find.byKey(headerKey),
|
|
);
|
|
|
|
// The persistent header is pinned to the leading edge thus still visible,
|
|
// the viewport should not scroll.
|
|
tester.renderObject(pinnedHeaderContent).showOnScreen();
|
|
await tester.pumpAndSettle();
|
|
expect(controller.offset, 300.0 * 15);
|
|
|
|
// The 11th child will be partially obstructed by the persistent header,
|
|
// the viewport should scroll to reveal it.
|
|
controller.jumpTo(
|
|
11 *
|
|
300.0 // Preceding headers
|
|
+
|
|
200.0 // Shrinks the pinned header to minExtent
|
|
+
|
|
100.0, // Obstructs the leading 100 pixels of the 11th header
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
tester.renderObject(find.byWidget(children[11], skipOffstage: false)).showOnScreen();
|
|
await tester.pumpAndSettle();
|
|
expect(controller.offset, lessThan(11 * 300.0 + 200.0 + 100.0));
|
|
},
|
|
);
|
|
|
|
void testFloatingHeaderShowOnScreen({bool animated = true, Axis axis = Axis.vertical}) {
|
|
final TickerProvider? vsync = animated ? const TestVSync() : null;
|
|
const headerKey = Key('header');
|
|
late List<Widget> children;
|
|
final controller = ScrollController(initialScrollOffset: 300.0);
|
|
|
|
Widget buildList({required SliverPersistentHeader floatingHeader, bool reversed = false}) {
|
|
return Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: SizedBox(
|
|
height: 400.0,
|
|
width: 400.0,
|
|
child: CustomScrollView(
|
|
scrollDirection: axis,
|
|
center: reversed ? const Key('19') : null,
|
|
controller: controller,
|
|
slivers: children = List<Widget>.generate(20, (int i) {
|
|
return i == 10
|
|
? floatingHeader
|
|
: SliverToBoxAdapter(
|
|
key: (i == 19) ? const Key('19') : null,
|
|
child: SizedBox(height: 300.0, width: 300, child: Text('Tile $i')),
|
|
);
|
|
}),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
double mainAxisExtent(WidgetTester tester, Finder finder) {
|
|
final RenderObject renderObject = tester.renderObject(finder);
|
|
if (renderObject is RenderSliver) {
|
|
return renderObject.geometry!.paintExtent;
|
|
}
|
|
|
|
final renderBox = renderObject as RenderBox;
|
|
return switch (axis) {
|
|
Axis.horizontal => renderBox.size.width,
|
|
Axis.vertical => renderBox.size.height,
|
|
};
|
|
}
|
|
|
|
group('animated: $animated, scrollDirection: $axis', () {
|
|
testWidgets('RenderViewportBase.showOnScreen', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
buildList(
|
|
floatingHeader: SliverPersistentHeader(
|
|
pinned: true,
|
|
floating: true,
|
|
delegate: _TestSliverPersistentHeaderDelegate(
|
|
minExtent: 100,
|
|
maxExtent: 300,
|
|
key: headerKey,
|
|
vsync: vsync,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final Finder pinnedHeaderContent = find.byKey(headerKey, skipOffstage: false);
|
|
|
|
controller.jumpTo(300.0 * 15);
|
|
await tester.pumpAndSettle();
|
|
expect(mainAxisExtent(tester, pinnedHeaderContent), lessThan(300));
|
|
|
|
// The persistent header is pinned to the leading edge thus still visible,
|
|
// the viewport should not scroll.
|
|
tester
|
|
.renderObject(pinnedHeaderContent)
|
|
.showOnScreen(
|
|
descendant: tester.renderObject(pinnedHeaderContent),
|
|
rect: Offset.zero & const Size(300, 300),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
// The header expands but doesn't move.
|
|
expect(controller.offset, 300.0 * 15);
|
|
expect(mainAxisExtent(tester, pinnedHeaderContent), 300);
|
|
|
|
// The rect specifies that the persistent header needs to be 1 pixel away
|
|
// from the leading edge of the viewport. Ignore the 1 pixel, the viewport
|
|
// should not scroll.
|
|
//
|
|
// See: https://github.com/flutter/flutter/issues/25507.
|
|
tester
|
|
.renderObject(pinnedHeaderContent)
|
|
.showOnScreen(
|
|
descendant: tester.renderObject(pinnedHeaderContent),
|
|
rect: const Offset(-1, -1) & const Size(300, 300),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
expect(controller.offset, 300.0 * 15);
|
|
expect(mainAxisExtent(tester, pinnedHeaderContent), 300);
|
|
});
|
|
|
|
testWidgets('RenderViewportBase.showOnScreen twice almost instantly', (
|
|
WidgetTester tester,
|
|
) async {
|
|
// Regression test for https://github.com/flutter/flutter/issues/137901
|
|
await tester.pumpWidget(
|
|
buildList(
|
|
floatingHeader: SliverPersistentHeader(
|
|
pinned: true,
|
|
floating: true,
|
|
delegate: _TestSliverPersistentHeaderDelegate(
|
|
minExtent: 100,
|
|
maxExtent: 300,
|
|
key: headerKey,
|
|
vsync: vsync,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final Finder pinnedHeaderContent = find.byKey(headerKey, skipOffstage: false);
|
|
|
|
controller.jumpTo(300.0 * 15);
|
|
await tester.pumpAndSettle();
|
|
expect(mainAxisExtent(tester, pinnedHeaderContent), lessThan(300));
|
|
|
|
tester
|
|
.renderObject(pinnedHeaderContent)
|
|
.showOnScreen(
|
|
descendant: tester.renderObject(pinnedHeaderContent),
|
|
// Adding different rect to check if the second showOnScreen call
|
|
// leads to a different result.
|
|
// When the animation has forward status and the second showOnScreen
|
|
// is called, the new animation won't start.
|
|
rect: Offset.zero & const Size(150, 150),
|
|
duration: const Duration(seconds: 3),
|
|
);
|
|
await tester.pump(const Duration(seconds: 1));
|
|
|
|
tester
|
|
.renderObject(pinnedHeaderContent)
|
|
.showOnScreen(
|
|
descendant: tester.renderObject(pinnedHeaderContent),
|
|
rect: Offset.zero & const Size(300, 300),
|
|
);
|
|
|
|
await tester.pumpAndSettle();
|
|
expect(controller.offset, 300.0 * 15);
|
|
expect(mainAxisExtent(tester, pinnedHeaderContent), 150);
|
|
});
|
|
|
|
testWidgets('RenderViewportBase.showOnScreen but no child', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
buildList(
|
|
floatingHeader: SliverPersistentHeader(
|
|
key: headerKey,
|
|
pinned: true,
|
|
floating: true,
|
|
delegate: _TestSliverPersistentHeaderDelegate(
|
|
minExtent: 100,
|
|
maxExtent: 300,
|
|
vsync: vsync,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final Finder pinnedHeaderContent = find.byKey(headerKey, skipOffstage: false);
|
|
|
|
controller.jumpTo(300.0 * 15);
|
|
await tester.pumpAndSettle();
|
|
expect(mainAxisExtent(tester, pinnedHeaderContent), lessThan(300));
|
|
|
|
// The persistent header is pinned to the leading edge thus still visible,
|
|
// the viewport should not scroll.
|
|
tester
|
|
.renderObject(pinnedHeaderContent)
|
|
.showOnScreen(rect: Offset.zero & const Size(300, 300));
|
|
await tester.pumpAndSettle();
|
|
// The header expands but doesn't move.
|
|
expect(controller.offset, 300.0 * 15);
|
|
expect(mainAxisExtent(tester, pinnedHeaderContent), 300);
|
|
|
|
// The rect specifies that the persistent header needs to be 1 pixel away
|
|
// from the leading edge of the viewport. Ignore the 1 pixel, the viewport
|
|
// should not scroll.
|
|
//
|
|
// See: https://github.com/flutter/flutter/issues/25507.
|
|
tester
|
|
.renderObject(pinnedHeaderContent)
|
|
.showOnScreen(rect: const Offset(-1, -1) & const Size(300, 300));
|
|
await tester.pumpAndSettle();
|
|
expect(controller.offset, 300.0 * 15);
|
|
expect(mainAxisExtent(tester, pinnedHeaderContent), 300);
|
|
});
|
|
|
|
testWidgets('RenderViewportBase.showOnScreen with maxShowOnScreenExtent ', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
buildList(
|
|
floatingHeader: SliverPersistentHeader(
|
|
pinned: true,
|
|
floating: true,
|
|
delegate: _TestSliverPersistentHeaderDelegate(
|
|
minExtent: 100,
|
|
maxExtent: 300,
|
|
key: headerKey,
|
|
vsync: vsync,
|
|
showOnScreenConfiguration: const PersistentHeaderShowOnScreenConfiguration(
|
|
maxShowOnScreenExtent: 200,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final Finder pinnedHeaderContent = find.byKey(headerKey, skipOffstage: false);
|
|
|
|
controller.jumpTo(300.0 * 15);
|
|
await tester.pumpAndSettle();
|
|
// childExtent was initially 100.
|
|
expect(mainAxisExtent(tester, pinnedHeaderContent), 100);
|
|
|
|
tester
|
|
.renderObject(pinnedHeaderContent)
|
|
.showOnScreen(
|
|
descendant: tester.renderObject(pinnedHeaderContent),
|
|
rect: Offset.zero & const Size(300, 300),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
// The header doesn't move. It would have expanded to 300 but
|
|
// maxShowOnScreenExtent is 200, preventing it from doing so.
|
|
expect(controller.offset, 300.0 * 15);
|
|
expect(mainAxisExtent(tester, pinnedHeaderContent), 200);
|
|
|
|
// ignoreLeading still works.
|
|
tester
|
|
.renderObject(pinnedHeaderContent)
|
|
.showOnScreen(
|
|
descendant: tester.renderObject(pinnedHeaderContent),
|
|
rect: const Offset(-1, -1) & const Size(300, 300),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
expect(controller.offset, 300.0 * 15);
|
|
expect(mainAxisExtent(tester, pinnedHeaderContent), 200);
|
|
|
|
// Move the viewport so that its childExtent reaches 250.
|
|
controller.jumpTo(300.0 * 10 + 50.0);
|
|
await tester.pumpAndSettle();
|
|
expect(mainAxisExtent(tester, pinnedHeaderContent), 250);
|
|
|
|
// Doesn't move, doesn't expand or shrink, leading still ignored.
|
|
tester
|
|
.renderObject(pinnedHeaderContent)
|
|
.showOnScreen(
|
|
descendant: tester.renderObject(pinnedHeaderContent),
|
|
rect: const Offset(-1, -1) & const Size(300, 300),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
expect(controller.offset, 300.0 * 10 + 50.0);
|
|
expect(mainAxisExtent(tester, pinnedHeaderContent), 250);
|
|
});
|
|
|
|
testWidgets('RenderViewportBase.showOnScreen with minShowOnScreenExtent ', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
buildList(
|
|
floatingHeader: SliverPersistentHeader(
|
|
pinned: true,
|
|
floating: true,
|
|
delegate: _TestSliverPersistentHeaderDelegate(
|
|
minExtent: 100,
|
|
maxExtent: 300,
|
|
key: headerKey,
|
|
vsync: vsync,
|
|
showOnScreenConfiguration: const PersistentHeaderShowOnScreenConfiguration(
|
|
minShowOnScreenExtent: 200,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final Finder pinnedHeaderContent = find.byKey(headerKey, skipOffstage: false);
|
|
|
|
controller.jumpTo(300.0 * 15);
|
|
await tester.pumpAndSettle();
|
|
// childExtent was initially 100.
|
|
expect(mainAxisExtent(tester, pinnedHeaderContent), 100);
|
|
|
|
tester
|
|
.renderObject(pinnedHeaderContent)
|
|
.showOnScreen(
|
|
descendant: tester.renderObject(pinnedHeaderContent),
|
|
rect: Offset.zero & const Size(110, 110),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
// The header doesn't move. It would have expanded to 110 but
|
|
// minShowOnScreenExtent is 200, preventing it from doing so.
|
|
expect(controller.offset, 300.0 * 15);
|
|
expect(mainAxisExtent(tester, pinnedHeaderContent), 200);
|
|
|
|
// ignoreLeading still works.
|
|
tester
|
|
.renderObject(pinnedHeaderContent)
|
|
.showOnScreen(
|
|
descendant: tester.renderObject(pinnedHeaderContent),
|
|
rect: const Offset(-1, -1) & const Size(110, 110),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
expect(controller.offset, 300.0 * 15);
|
|
expect(mainAxisExtent(tester, pinnedHeaderContent), 200);
|
|
|
|
// Move the viewport so that its childExtent reaches 250.
|
|
controller.jumpTo(300.0 * 10 + 50.0);
|
|
await tester.pumpAndSettle();
|
|
expect(mainAxisExtent(tester, pinnedHeaderContent), 250);
|
|
|
|
// Doesn't move, doesn't expand or shrink, leading still ignored.
|
|
tester
|
|
.renderObject(pinnedHeaderContent)
|
|
.showOnScreen(
|
|
descendant: tester.renderObject(pinnedHeaderContent),
|
|
rect: const Offset(-1, -1) & const Size(110, 110),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
expect(controller.offset, 300.0 * 10 + 50.0);
|
|
expect(mainAxisExtent(tester, pinnedHeaderContent), 250);
|
|
});
|
|
|
|
testWidgets(
|
|
'RenderViewportBase.showOnScreen should not scroll if the rect is already visible, '
|
|
'even if it does not scroll linearly (reversed order version)',
|
|
(WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
buildList(
|
|
floatingHeader: SliverPersistentHeader(
|
|
pinned: true,
|
|
floating: true,
|
|
delegate: _TestSliverPersistentHeaderDelegate(
|
|
minExtent: 100,
|
|
maxExtent: 300,
|
|
key: headerKey,
|
|
vsync: vsync,
|
|
),
|
|
),
|
|
reversed: true,
|
|
),
|
|
);
|
|
|
|
controller.jumpTo(-300.0 * 15);
|
|
await tester.pumpAndSettle();
|
|
|
|
final Finder pinnedHeaderContent = find.byKey(headerKey, skipOffstage: false);
|
|
|
|
// The persistent header is pinned to the leading edge thus still visible,
|
|
// the viewport should not scroll.
|
|
tester.renderObject(pinnedHeaderContent).showOnScreen();
|
|
await tester.pumpAndSettle();
|
|
expect(controller.offset, -300.0 * 15);
|
|
|
|
// children[9] will be partially obstructed by the persistent header,
|
|
// the viewport should scroll to reveal it.
|
|
controller.jumpTo(
|
|
-8 *
|
|
300.0 // Preceding headers 11 - 18, children[11]'s top edge is aligned to the leading edge.
|
|
-
|
|
400.0 // Viewport height. children[10] (the pinned header) becomes pinned at the bottom of the screen.
|
|
-
|
|
200.0 // Shrinks the pinned header to minExtent (100).
|
|
-
|
|
100.0, // Obstructs the leading 100 pixels of the 11th header
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
tester.renderObject(find.byWidget(children[9], skipOffstage: false)).showOnScreen();
|
|
await tester.pumpAndSettle();
|
|
expect(controller.offset, -8 * 300.0 - 400.0 - 200.0);
|
|
},
|
|
);
|
|
});
|
|
}
|
|
|
|
group('Floating header showOnScreen', () {
|
|
testFloatingHeaderShowOnScreen();
|
|
testFloatingHeaderShowOnScreen(axis: Axis.horizontal);
|
|
});
|
|
|
|
group('RenderViewport getOffsetToReveal renderBox to sliver coordinates conversion', () {
|
|
const padding = EdgeInsets.fromLTRB(22, 22, 34, 34);
|
|
const centerKey = Key('5');
|
|
Widget buildList({required Axis axis, bool reverse = false, bool reverseGrowth = false}) {
|
|
return Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: SizedBox(
|
|
height: 400.0,
|
|
width: 400.0,
|
|
child: CustomScrollView(
|
|
scrollDirection: axis,
|
|
reverse: reverse,
|
|
center: reverseGrowth ? centerKey : null,
|
|
slivers: List<Widget>.generate(6, (int i) {
|
|
return SliverPadding(
|
|
key: i == 5 ? centerKey : null,
|
|
padding: padding,
|
|
sliver: SliverToBoxAdapter(
|
|
child: Container(
|
|
padding: padding,
|
|
height: 300.0,
|
|
width: 300.0,
|
|
child: Text('Tile $i'),
|
|
),
|
|
),
|
|
);
|
|
}),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
testWidgets('up, forward growth', (WidgetTester tester) async {
|
|
await tester.pumpWidget(buildList(axis: Axis.vertical, reverse: true));
|
|
final RenderAbstractViewport viewport = tester.allRenderObjects
|
|
.whereType<RenderAbstractViewport>()
|
|
.first;
|
|
|
|
final RenderObject target = tester.renderObject(find.text('Tile 5', skipOffstage: false));
|
|
final double revealOffset = viewport.getOffsetToReveal(target, 0.0).offset;
|
|
expect(revealOffset, (300.0 + padding.horizontal) * 5 + 34.0 * 2);
|
|
});
|
|
|
|
testWidgets('up, reverse growth', (WidgetTester tester) async {
|
|
await tester.pumpWidget(buildList(axis: Axis.vertical, reverse: true, reverseGrowth: true));
|
|
final RenderAbstractViewport viewport = tester.allRenderObjects
|
|
.whereType<RenderAbstractViewport>()
|
|
.first;
|
|
|
|
final RenderObject target = tester.renderObject(find.text('Tile 0', skipOffstage: false));
|
|
final double revealOffset = viewport.getOffsetToReveal(target, 0.0).offset;
|
|
expect(revealOffset, -(300.0 + padding.horizontal) * 5 + 34.0 * 2);
|
|
});
|
|
|
|
testWidgets('right, forward growth', (WidgetTester tester) async {
|
|
await tester.pumpWidget(buildList(axis: Axis.horizontal));
|
|
final RenderAbstractViewport viewport = tester.allRenderObjects
|
|
.whereType<RenderAbstractViewport>()
|
|
.first;
|
|
|
|
final RenderObject target = tester.renderObject(find.text('Tile 5', skipOffstage: false));
|
|
final double revealOffset = viewport.getOffsetToReveal(target, 0.0).offset;
|
|
expect(revealOffset, (300.0 + padding.horizontal) * 5 + 22.0 * 2);
|
|
});
|
|
|
|
testWidgets('right, reverse growth', (WidgetTester tester) async {
|
|
await tester.pumpWidget(buildList(axis: Axis.horizontal, reverseGrowth: true));
|
|
final RenderAbstractViewport viewport = tester.allRenderObjects
|
|
.whereType<RenderAbstractViewport>()
|
|
.first;
|
|
|
|
final RenderObject target = tester.renderObject(find.text('Tile 0', skipOffstage: false));
|
|
final double revealOffset = viewport.getOffsetToReveal(target, 0.0).offset;
|
|
expect(revealOffset, -(300.0 + padding.horizontal) * 5 + 22.0 * 2);
|
|
});
|
|
|
|
testWidgets('down, forward growth', (WidgetTester tester) async {
|
|
await tester.pumpWidget(buildList(axis: Axis.vertical));
|
|
final RenderAbstractViewport viewport = tester.allRenderObjects
|
|
.whereType<RenderAbstractViewport>()
|
|
.first;
|
|
|
|
final RenderObject target = tester.renderObject(find.text('Tile 5', skipOffstage: false));
|
|
final double revealOffset = viewport.getOffsetToReveal(target, 0.0).offset;
|
|
expect(revealOffset, (300.0 + padding.horizontal) * 5 + 22.0 * 2);
|
|
});
|
|
|
|
testWidgets('down, reverse growth', (WidgetTester tester) async {
|
|
await tester.pumpWidget(buildList(axis: Axis.vertical, reverseGrowth: true));
|
|
final RenderAbstractViewport viewport = tester.allRenderObjects
|
|
.whereType<RenderAbstractViewport>()
|
|
.first;
|
|
|
|
final RenderObject target = tester.renderObject(find.text('Tile 0', skipOffstage: false));
|
|
final double revealOffset = viewport.getOffsetToReveal(target, 0.0).offset;
|
|
expect(revealOffset, -(300.0 + padding.horizontal) * 5 + 22.0 * 2);
|
|
});
|
|
|
|
testWidgets('left, forward growth', (WidgetTester tester) async {
|
|
await tester.pumpWidget(buildList(axis: Axis.horizontal, reverse: true));
|
|
final RenderAbstractViewport viewport = tester.allRenderObjects
|
|
.whereType<RenderAbstractViewport>()
|
|
.first;
|
|
|
|
final RenderObject target = tester.renderObject(find.text('Tile 5', skipOffstage: false));
|
|
final double revealOffset = viewport.getOffsetToReveal(target, 0.0).offset;
|
|
expect(revealOffset, (300.0 + padding.horizontal) * 5 + 34.0 * 2);
|
|
});
|
|
|
|
testWidgets('left, reverse growth', (WidgetTester tester) async {
|
|
await tester.pumpWidget(buildList(axis: Axis.horizontal, reverse: true, reverseGrowth: true));
|
|
final RenderAbstractViewport viewport = tester.allRenderObjects
|
|
.whereType<RenderAbstractViewport>()
|
|
.first;
|
|
|
|
final RenderObject target = tester.renderObject(find.text('Tile 0', skipOffstage: false));
|
|
final double revealOffset = viewport.getOffsetToReveal(target, 0.0).offset;
|
|
expect(revealOffset, -(300.0 + padding.horizontal) * 5 + 34.0 * 2);
|
|
});
|
|
|
|
testWidgets('will not assert on mismatched axis', (WidgetTester tester) async {
|
|
await tester.pumpWidget(buildList(axis: Axis.vertical, reverse: true, reverseGrowth: true));
|
|
final RenderAbstractViewport viewport = tester.allRenderObjects
|
|
.whereType<RenderAbstractViewport>()
|
|
.first;
|
|
|
|
final RenderObject target = tester.renderObject(find.text('Tile 0', skipOffstage: false));
|
|
viewport.getOffsetToReveal(target, 0.0, axis: Axis.horizontal);
|
|
});
|
|
});
|
|
|
|
testWidgets('RenderViewportBase.showOnScreen reports the correct targetRect', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final innerController = ScrollController();
|
|
final outerController = ScrollController();
|
|
addTearDown(innerController.dispose);
|
|
addTearDown(outerController.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: SizedBox(
|
|
height: 300.0,
|
|
child: CustomScrollView(
|
|
cacheExtent: 0,
|
|
controller: outerController,
|
|
slivers: <Widget>[
|
|
SliverToBoxAdapter(
|
|
child: SizedBox(
|
|
height: 300,
|
|
child: CustomScrollView(
|
|
controller: innerController,
|
|
slivers: List<Widget>.generate(5, (int i) {
|
|
return SliverToBoxAdapter(
|
|
child: SizedBox(height: 300.0, child: Text('Tile $i')),
|
|
);
|
|
}),
|
|
),
|
|
),
|
|
),
|
|
const SliverToBoxAdapter(child: SizedBox(height: 300.0, child: Text('hidden'))),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
tester
|
|
.renderObject(find.widgetWithText(SizedBox, 'Tile 1', skipOffstage: false).first)
|
|
.showOnScreen();
|
|
await tester.pumpAndSettle();
|
|
// The inner viewport scrolls to reveal the 2nd tile.
|
|
expect(innerController.offset, 300.0);
|
|
expect(outerController.offset, 0);
|
|
});
|
|
|
|
group('unbounded constraints control test', () {
|
|
Widget buildNestedWidget([Axis a1 = Axis.vertical, Axis a2 = Axis.horizontal]) {
|
|
return Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: ListView(
|
|
scrollDirection: a1,
|
|
children: List<Widget>.generate(10, (int y) {
|
|
return ListView(scrollDirection: a2);
|
|
}),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> expectFlutterError({
|
|
required Widget widget,
|
|
required WidgetTester tester,
|
|
required String message,
|
|
}) async {
|
|
final errors = <FlutterErrorDetails>[];
|
|
final FlutterExceptionHandler? oldHandler = FlutterError.onError;
|
|
FlutterError.onError = (FlutterErrorDetails error) => errors.add(error);
|
|
try {
|
|
await tester.pumpWidget(widget);
|
|
} finally {
|
|
FlutterError.onError = oldHandler;
|
|
}
|
|
expect(errors, isNotEmpty);
|
|
expect(errors.first.exception, isFlutterError);
|
|
expect((errors.first.exception as FlutterError).toStringDeep(), message);
|
|
}
|
|
|
|
testWidgets('Horizontal viewport was given unbounded height', (WidgetTester tester) async {
|
|
await expectFlutterError(
|
|
widget: buildNestedWidget(),
|
|
tester: tester,
|
|
message:
|
|
'FlutterError\n'
|
|
' Horizontal viewport was given unbounded height.\n'
|
|
' Viewports expand in the cross axis to fill their container and\n'
|
|
' constrain their children to match their extent in the cross axis.\n'
|
|
' In this case, a horizontal viewport was given an unlimited amount\n'
|
|
' of vertical space in which to expand.\n',
|
|
);
|
|
});
|
|
|
|
testWidgets('Horizontal viewport was given unbounded width', (WidgetTester tester) async {
|
|
await expectFlutterError(
|
|
widget: buildNestedWidget(Axis.horizontal),
|
|
tester: tester,
|
|
message:
|
|
'FlutterError\n'
|
|
' Horizontal viewport was given unbounded width.\n'
|
|
' Viewports expand in the scrolling direction to fill their\n'
|
|
' container. In this case, a horizontal viewport was given an\n'
|
|
' unlimited amount of horizontal space in which to expand. This\n'
|
|
' situation typically happens when a scrollable widget is nested\n'
|
|
' inside another scrollable widget.\n'
|
|
' If this widget is always nested in a scrollable widget there is\n'
|
|
' no need to use a viewport because there will always be enough\n'
|
|
' horizontal space for the children. In this case, consider using a\n'
|
|
' Row or Wrap instead. Otherwise, consider using a CustomScrollView\n'
|
|
' to concatenate arbitrary slivers into a single scrollable.\n',
|
|
);
|
|
});
|
|
|
|
testWidgets('Vertical viewport was given unbounded width', (WidgetTester tester) async {
|
|
await expectFlutterError(
|
|
widget: buildNestedWidget(Axis.horizontal, Axis.vertical),
|
|
tester: tester,
|
|
message:
|
|
'FlutterError\n'
|
|
' Vertical viewport was given unbounded width.\n'
|
|
' Viewports expand in the cross axis to fill their container and\n'
|
|
' constrain their children to match their extent in the cross axis.\n'
|
|
' In this case, a vertical viewport was given an unlimited amount\n'
|
|
' of horizontal space in which to expand.\n',
|
|
);
|
|
});
|
|
|
|
testWidgets('Vertical viewport was given unbounded height', (WidgetTester tester) async {
|
|
await expectFlutterError(
|
|
widget: buildNestedWidget(Axis.vertical, Axis.vertical),
|
|
tester: tester,
|
|
message:
|
|
'FlutterError\n'
|
|
' Vertical viewport was given unbounded height.\n'
|
|
' Viewports expand in the scrolling direction to fill their\n'
|
|
' container. In this case, a vertical viewport was given an\n'
|
|
' unlimited amount of vertical space in which to expand. This\n'
|
|
' situation typically happens when a scrollable widget is nested\n'
|
|
' inside another scrollable widget.\n'
|
|
' If this widget is always nested in a scrollable widget there is\n'
|
|
' no need to use a viewport because there will always be enough\n'
|
|
' vertical space for the children. In this case, consider using a\n'
|
|
' Column or Wrap instead. Otherwise, consider using a\n'
|
|
' CustomScrollView to concatenate arbitrary slivers into a single\n'
|
|
' scrollable.\n',
|
|
);
|
|
});
|
|
});
|
|
|
|
test('Viewport debugThrowIfNotCheckingIntrinsics() control test', () {
|
|
final renderViewport = RenderViewport(
|
|
crossAxisDirection: AxisDirection.right,
|
|
offset: ViewportOffset.zero(),
|
|
);
|
|
late FlutterError error;
|
|
try {
|
|
renderViewport.computeMinIntrinsicHeight(0);
|
|
} on FlutterError catch (e) {
|
|
error = e;
|
|
}
|
|
expect(
|
|
error.toStringDeep(),
|
|
'FlutterError\n'
|
|
' RenderViewport does not support returning intrinsic dimensions.\n'
|
|
' Calculating the intrinsic dimensions would require instantiating\n'
|
|
' every child of the viewport, which defeats the point of viewports\n'
|
|
' being lazy.\n'
|
|
' If you are merely trying to shrink-wrap the viewport in the main\n'
|
|
' axis direction, consider a RenderShrinkWrappingViewport render\n'
|
|
' object (ShrinkWrappingViewport widget), which achieves that\n'
|
|
' effect without implementing the intrinsic dimension API.\n',
|
|
);
|
|
|
|
final renderShrinkWrappingViewport = RenderShrinkWrappingViewport(
|
|
crossAxisDirection: AxisDirection.right,
|
|
offset: ViewportOffset.zero(),
|
|
);
|
|
try {
|
|
renderShrinkWrappingViewport.computeMinIntrinsicHeight(0);
|
|
} on FlutterError catch (e) {
|
|
error = e;
|
|
}
|
|
expect(error, isNotNull);
|
|
expect(
|
|
error.toStringDeep(),
|
|
'FlutterError\n'
|
|
' RenderShrinkWrappingViewport does not support returning intrinsic\n'
|
|
' dimensions.\n'
|
|
' Calculating the intrinsic dimensions would require instantiating\n'
|
|
' every child of the viewport, which defeats the point of viewports\n'
|
|
' being lazy.\n'
|
|
' If you are merely trying to shrink-wrap the viewport in the main\n'
|
|
' axis direction, you should be able to achieve that effect by just\n'
|
|
' giving the viewport loose constraints, without needing to measure\n'
|
|
' its intrinsic dimensions.\n',
|
|
);
|
|
});
|
|
|
|
group('Viewport paint order', () {
|
|
final paintLog = <int>[];
|
|
|
|
Widget makeSliver(int i) {
|
|
return SliverToBoxAdapter(
|
|
key: ValueKey<int>(i),
|
|
child: CustomPaint(
|
|
painter: TestCustomPainter()..onPaint = (_, _) => paintLog.add(i),
|
|
child: Text('Item $i'),
|
|
),
|
|
);
|
|
}
|
|
|
|
testWidgets('default (firstIsTop)', (WidgetTester tester) async {
|
|
addTearDown(paintLog.clear);
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: CustomScrollView(
|
|
center: const ValueKey<int>(2),
|
|
anchor: 0.5,
|
|
slivers: List<Widget>.generate(5, makeSliver),
|
|
),
|
|
),
|
|
);
|
|
|
|
// First sliver paints last, over other slivers; last sliver paints first.
|
|
expect(paintLog, equals(<int>[4, 3, 2, 1, 0]));
|
|
});
|
|
|
|
testWidgets('lastIsTop', (WidgetTester tester) async {
|
|
addTearDown(paintLog.clear);
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: CustomScrollView(
|
|
paintOrder: SliverPaintOrder.lastIsTop,
|
|
center: const ValueKey<int>(2),
|
|
anchor: 0.5,
|
|
slivers: List<Widget>.generate(5, makeSliver),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Last sliver paints last, over other slivers; first sliver paints first.
|
|
expect(paintLog, equals(<int>[0, 1, 2, 3, 4]));
|
|
});
|
|
});
|
|
|
|
group('Viewport hit-test order', () {
|
|
Widget makeSliver(int i) {
|
|
return _AllOverlapSliver(key: ValueKey<int>(i), id: i);
|
|
}
|
|
|
|
testWidgets('default (firstIsTop)', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: CustomScrollView(
|
|
center: const ValueKey<int>(2),
|
|
anchor: 0.5,
|
|
slivers: List<Widget>.generate(5, makeSliver),
|
|
),
|
|
),
|
|
);
|
|
|
|
final HitTestResult result = tester.hitTestOnBinding(const Offset(400, 300));
|
|
expect(
|
|
result.path
|
|
.map((HitTestEntry<HitTestTarget> e) => e.target)
|
|
.map((HitTestTarget t) => t is _RenderAllOverlapSliver ? t.id : null)
|
|
.nonNulls,
|
|
equals(<int>[0, 1, 2, 3, 4]),
|
|
);
|
|
});
|
|
|
|
testWidgets('lastIsTop', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: CustomScrollView(
|
|
paintOrder: SliverPaintOrder.lastIsTop,
|
|
center: const ValueKey<int>(2),
|
|
anchor: 0.5,
|
|
slivers: List<Widget>.generate(5, makeSliver),
|
|
),
|
|
),
|
|
);
|
|
|
|
final HitTestResult result = tester.hitTestOnBinding(const Offset(400, 300));
|
|
expect(
|
|
result.path
|
|
.map((HitTestEntry<HitTestTarget> e) => e.target)
|
|
.map((HitTestTarget t) => t is _RenderAllOverlapSliver ? t.id : null)
|
|
.nonNulls,
|
|
equals(<int>[4, 3, 2, 1, 0]),
|
|
);
|
|
});
|
|
});
|
|
|
|
group('Overscrolling RenderShrinkWrappingViewport', () {
|
|
Widget buildSimpleShrinkWrap({
|
|
ScrollController? controller,
|
|
Axis scrollDirection = Axis.vertical,
|
|
ScrollPhysics? physics,
|
|
}) {
|
|
return Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: MediaQuery(
|
|
data: const MediaQueryData(),
|
|
child: ListView.builder(
|
|
controller: controller,
|
|
physics: physics,
|
|
scrollDirection: scrollDirection,
|
|
shrinkWrap: true,
|
|
itemBuilder: (BuildContext context, int index) =>
|
|
SizedBox(height: 50, width: 50, child: Text('Item $index')),
|
|
itemCount: 20,
|
|
itemExtent: 50,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget buildClippingShrinkWrap(ScrollController controller, {bool constrain = false}) {
|
|
return Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: MediaQuery(
|
|
data: const MediaQueryData(),
|
|
child: ColoredBox(
|
|
color: const Color(0xFF000000),
|
|
child: Column(
|
|
children: <Widget>[
|
|
// Translucent boxes above and below the shrinkwrapped viewport
|
|
// make it easily discernible if the viewport is not being
|
|
// clipped properly.
|
|
Opacity(
|
|
opacity: 0.5,
|
|
child: Container(height: 100, color: const Color(0xFF00B0FF)),
|
|
),
|
|
Container(
|
|
height: constrain ? 150 : null,
|
|
color: const Color(0xFFF44336),
|
|
child: ListView.builder(
|
|
controller: controller,
|
|
shrinkWrap: true,
|
|
physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()),
|
|
itemBuilder: (BuildContext context, int index) => Text('Item $index'),
|
|
itemCount: 10,
|
|
),
|
|
),
|
|
Opacity(
|
|
opacity: 0.5,
|
|
child: Container(height: 100, color: const Color(0xFF00B0FF)),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
testWidgets('constrained viewport correctly clips overflow', (WidgetTester tester) async {
|
|
// Regression test for https://github.com/flutter/flutter/issues/89717
|
|
final controller = ScrollController();
|
|
addTearDown(controller.dispose);
|
|
|
|
await tester.pumpWidget(buildClippingShrinkWrap(controller, constrain: true));
|
|
expect(controller.offset, 0.0);
|
|
expect(tester.getTopLeft(find.text('Item 0')).dy, 100.0);
|
|
expect(tester.getTopLeft(find.text('Item 9')).dy, 226.0);
|
|
|
|
// Overscroll
|
|
final TestGesture overscrollGesture = await tester.startGesture(
|
|
tester.getCenter(find.text('Item 0')),
|
|
);
|
|
await overscrollGesture.moveBy(const Offset(0, 100));
|
|
await tester.pump();
|
|
expect(controller.offset, -100.0);
|
|
expect(tester.getTopLeft(find.text('Item 0')).dy, 200.0);
|
|
await expectLater(
|
|
find.byType(Directionality),
|
|
matchesGoldenFile('shrinkwrap_clipped_constrained_overscroll.png'),
|
|
);
|
|
await overscrollGesture.up();
|
|
await tester.pumpAndSettle();
|
|
expect(controller.offset, 0.0);
|
|
expect(tester.getTopLeft(find.text('Item 0')).dy, 100.0);
|
|
expect(tester.getTopLeft(find.text('Item 9')).dy, 226.0);
|
|
});
|
|
|
|
testWidgets('correctly clips overflow without constraints', (WidgetTester tester) async {
|
|
// Regression test for https://github.com/flutter/flutter/issues/89717
|
|
final controller = ScrollController();
|
|
addTearDown(controller.dispose);
|
|
|
|
await tester.pumpWidget(buildClippingShrinkWrap(controller));
|
|
expect(controller.offset, 0.0);
|
|
expect(tester.getTopLeft(find.text('Item 0')).dy, 100.0);
|
|
expect(tester.getTopLeft(find.text('Item 9')).dy, 226.0);
|
|
|
|
// Overscroll
|
|
final TestGesture overscrollGesture = await tester.startGesture(
|
|
tester.getCenter(find.text('Item 0')),
|
|
);
|
|
await overscrollGesture.moveBy(const Offset(0, 100));
|
|
await tester.pump();
|
|
expect(controller.offset, -100.0);
|
|
expect(tester.getTopLeft(find.text('Item 0')).dy, 200.0);
|
|
await expectLater(
|
|
find.byType(Directionality),
|
|
matchesGoldenFile('shrinkwrap_clipped_overscroll.png'),
|
|
);
|
|
await overscrollGesture.up();
|
|
await tester.pumpAndSettle();
|
|
expect(controller.offset, 0.0);
|
|
expect(tester.getTopLeft(find.text('Item 0')).dy, 100.0);
|
|
expect(tester.getTopLeft(find.text('Item 9')).dy, 226.0);
|
|
});
|
|
|
|
testWidgets(
|
|
'allows overscrolling on default platforms - vertical',
|
|
(WidgetTester tester) async {
|
|
// Regression test for https://github.com/flutter/flutter/issues/10949
|
|
// Scrollables should overscroll by default on iOS and macOS
|
|
final controller = ScrollController();
|
|
addTearDown(controller.dispose);
|
|
|
|
await tester.pumpWidget(buildSimpleShrinkWrap(controller: controller));
|
|
expect(controller.offset, 0.0);
|
|
expect(tester.getTopLeft(find.text('Item 0')).dy, 0.0);
|
|
// Check overscroll at both ends
|
|
// Start
|
|
TestGesture overscrollGesture = await tester.startGesture(
|
|
tester.getCenter(find.byType(ListView)),
|
|
);
|
|
await overscrollGesture.moveBy(const Offset(0, 25));
|
|
await tester.pump();
|
|
expect(controller.offset, -25.0);
|
|
expect(tester.getTopLeft(find.text('Item 0')).dy, 25.0);
|
|
await overscrollGesture.up();
|
|
await tester.pumpAndSettle();
|
|
expect(controller.offset, 0.0);
|
|
expect(tester.getTopLeft(find.text('Item 0')).dy, 0.0);
|
|
|
|
// End
|
|
final double maxExtent = controller.position.maxScrollExtent;
|
|
controller.jumpTo(controller.position.maxScrollExtent);
|
|
await tester.pumpAndSettle();
|
|
expect(controller.offset, maxExtent);
|
|
expect(tester.getBottomLeft(find.text('Item 19')).dy, 600.0);
|
|
|
|
overscrollGesture = await tester.startGesture(tester.getCenter(find.byType(ListView)));
|
|
await overscrollGesture.moveBy(const Offset(0, -25));
|
|
await tester.pump();
|
|
expect(controller.offset, greaterThan(maxExtent));
|
|
expect(tester.getBottomLeft(find.text('Item 19')).dy, 575.0);
|
|
await overscrollGesture.up();
|
|
await tester.pumpAndSettle();
|
|
expect(controller.offset, maxExtent);
|
|
expect(tester.getBottomLeft(find.text('Item 19')).dy, 600.0);
|
|
},
|
|
variant: const TargetPlatformVariant(<TargetPlatform>{
|
|
TargetPlatform.iOS,
|
|
TargetPlatform.macOS,
|
|
}),
|
|
);
|
|
|
|
testWidgets(
|
|
'allows overscrolling on default platforms - horizontal',
|
|
(WidgetTester tester) async {
|
|
// Regression test for https://github.com/flutter/flutter/issues/10949
|
|
// Scrollables should overscroll by default on iOS and macOS
|
|
final controller = ScrollController();
|
|
addTearDown(controller.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
buildSimpleShrinkWrap(controller: controller, scrollDirection: Axis.horizontal),
|
|
);
|
|
expect(controller.offset, 0.0);
|
|
expect(tester.getTopLeft(find.text('Item 0')).dx, 0.0);
|
|
// Check overscroll at both ends
|
|
// Start
|
|
TestGesture overscrollGesture = await tester.startGesture(
|
|
tester.getCenter(find.byType(ListView)),
|
|
);
|
|
await overscrollGesture.moveBy(const Offset(25, 0));
|
|
await tester.pump();
|
|
expect(controller.offset, -25.0);
|
|
expect(tester.getTopLeft(find.text('Item 0')).dx, 25.0);
|
|
await overscrollGesture.up();
|
|
await tester.pumpAndSettle();
|
|
expect(controller.offset, 0.0);
|
|
expect(tester.getTopLeft(find.text('Item 0')).dx, 0.0);
|
|
|
|
// End
|
|
final double maxExtent = controller.position.maxScrollExtent;
|
|
controller.jumpTo(controller.position.maxScrollExtent);
|
|
await tester.pumpAndSettle();
|
|
expect(controller.offset, maxExtent);
|
|
expect(tester.getTopRight(find.text('Item 19')).dx, 800.0);
|
|
|
|
overscrollGesture = await tester.startGesture(tester.getCenter(find.byType(ListView)));
|
|
await overscrollGesture.moveBy(const Offset(-25, 0));
|
|
await tester.pump();
|
|
expect(controller.offset, greaterThan(maxExtent));
|
|
expect(tester.getTopRight(find.text('Item 19')).dx, 775.0);
|
|
await overscrollGesture.up();
|
|
await tester.pumpAndSettle();
|
|
expect(controller.offset, maxExtent);
|
|
expect(tester.getTopRight(find.text('Item 19')).dx, 800.0);
|
|
},
|
|
variant: const TargetPlatformVariant(<TargetPlatform>{
|
|
TargetPlatform.iOS,
|
|
TargetPlatform.macOS,
|
|
}),
|
|
);
|
|
|
|
testWidgets('allows overscrolling per physics - vertical', (WidgetTester tester) async {
|
|
// Regression test for https://github.com/flutter/flutter/issues/10949
|
|
// Scrollables should overscroll when the scroll physics allow
|
|
final controller = ScrollController();
|
|
addTearDown(controller.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
buildSimpleShrinkWrap(controller: controller, physics: const BouncingScrollPhysics()),
|
|
);
|
|
expect(controller.offset, 0.0);
|
|
expect(tester.getTopLeft(find.text('Item 0')).dy, 0.0);
|
|
// Check overscroll at both ends
|
|
// Start
|
|
TestGesture overscrollGesture = await tester.startGesture(
|
|
tester.getCenter(find.byType(ListView)),
|
|
);
|
|
await overscrollGesture.moveBy(const Offset(0, 25));
|
|
await tester.pump();
|
|
expect(controller.offset, -25.0);
|
|
expect(tester.getTopLeft(find.text('Item 0')).dy, 25.0);
|
|
await overscrollGesture.up();
|
|
await tester.pumpAndSettle();
|
|
expect(controller.offset, 0.0);
|
|
expect(tester.getTopLeft(find.text('Item 0')).dy, 0.0);
|
|
|
|
// End
|
|
final double maxExtent = controller.position.maxScrollExtent;
|
|
controller.jumpTo(controller.position.maxScrollExtent);
|
|
await tester.pumpAndSettle();
|
|
expect(controller.offset, maxExtent);
|
|
expect(tester.getBottomLeft(find.text('Item 19')).dy, 600.0);
|
|
|
|
overscrollGesture = await tester.startGesture(tester.getCenter(find.byType(ListView)));
|
|
await overscrollGesture.moveBy(const Offset(0, -25));
|
|
await tester.pump();
|
|
expect(controller.offset, greaterThan(maxExtent));
|
|
expect(tester.getBottomLeft(find.text('Item 19')).dy, 575.0);
|
|
await overscrollGesture.up();
|
|
await tester.pumpAndSettle();
|
|
expect(controller.offset, maxExtent);
|
|
expect(tester.getBottomLeft(find.text('Item 19')).dy, 600.0);
|
|
});
|
|
|
|
testWidgets('allows overscrolling per physics - horizontal', (WidgetTester tester) async {
|
|
// Regression test for https://github.com/flutter/flutter/issues/10949
|
|
// Scrollables should overscroll when the scroll physics allow
|
|
final controller = ScrollController();
|
|
addTearDown(controller.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
buildSimpleShrinkWrap(
|
|
controller: controller,
|
|
scrollDirection: Axis.horizontal,
|
|
physics: const BouncingScrollPhysics(),
|
|
),
|
|
);
|
|
expect(controller.offset, 0.0);
|
|
expect(tester.getTopLeft(find.text('Item 0')).dx, 0.0);
|
|
// Check overscroll at both ends
|
|
// Start
|
|
TestGesture overscrollGesture = await tester.startGesture(
|
|
tester.getCenter(find.byType(ListView)),
|
|
);
|
|
await overscrollGesture.moveBy(const Offset(25, 0));
|
|
await tester.pump();
|
|
expect(controller.offset, -25.0);
|
|
expect(tester.getTopLeft(find.text('Item 0')).dx, 25.0);
|
|
await overscrollGesture.up();
|
|
await tester.pumpAndSettle();
|
|
expect(controller.offset, 0.0);
|
|
expect(tester.getTopLeft(find.text('Item 0')).dx, 0.0);
|
|
|
|
// End
|
|
final double maxExtent = controller.position.maxScrollExtent;
|
|
controller.jumpTo(controller.position.maxScrollExtent);
|
|
await tester.pumpAndSettle();
|
|
expect(controller.offset, maxExtent);
|
|
expect(tester.getTopRight(find.text('Item 19')).dx, 800.0);
|
|
|
|
overscrollGesture = await tester.startGesture(tester.getCenter(find.byType(ListView)));
|
|
await overscrollGesture.moveBy(const Offset(-25, 0));
|
|
await tester.pump();
|
|
expect(controller.offset, greaterThan(maxExtent));
|
|
expect(tester.getTopRight(find.text('Item 19')).dx, 775.0);
|
|
await overscrollGesture.up();
|
|
await tester.pumpAndSettle();
|
|
expect(controller.offset, maxExtent);
|
|
expect(tester.getTopRight(find.text('Item 19')).dx, 800.0);
|
|
});
|
|
});
|
|
|
|
testWidgets(
|
|
'Handles infinite constraints when TargetPlatform is iOS or macOS',
|
|
(WidgetTester tester) async {
|
|
// regression test for https://github.com/flutter/flutter/issues/45866
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: MediaQuery(
|
|
data: const MediaQueryData(),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: <Widget>[
|
|
GridView(
|
|
shrinkWrap: true,
|
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
|
crossAxisCount: 3,
|
|
childAspectRatio: 3,
|
|
mainAxisSpacing: 3,
|
|
crossAxisSpacing: 3,
|
|
),
|
|
children: const <Widget>[Text('a'), Text('b'), Text('c')],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(find.text('b'), findsOneWidget);
|
|
await tester.drag(find.text('b'), const Offset(0, 200));
|
|
await tester.pumpAndSettle();
|
|
},
|
|
variant: const TargetPlatformVariant(<TargetPlatform>{
|
|
TargetPlatform.iOS,
|
|
TargetPlatform.macOS,
|
|
}),
|
|
);
|
|
|
|
testWidgets('Viewport describeApproximateClip respects clipBehavior', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
const Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: CustomScrollView(
|
|
clipBehavior: Clip.none,
|
|
slivers: <Widget>[SliverToBoxAdapter(child: SizedBox(width: 20, height: 20))],
|
|
),
|
|
),
|
|
);
|
|
RenderViewport viewport = tester.allRenderObjects.whereType<RenderViewport>().first;
|
|
expect(viewport.clipBehavior, Clip.none);
|
|
var visited = false;
|
|
viewport.visitChildren((RenderObject child) {
|
|
visited = true;
|
|
expect(viewport.describeApproximatePaintClip(child as RenderSliver), null);
|
|
});
|
|
expect(visited, true);
|
|
|
|
await tester.pumpWidget(
|
|
const Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: CustomScrollView(
|
|
slivers: <Widget>[SliverToBoxAdapter(child: SizedBox(width: 20, height: 20))],
|
|
),
|
|
),
|
|
);
|
|
viewport = tester.allRenderObjects.whereType<RenderViewport>().first;
|
|
expect(viewport.clipBehavior, Clip.hardEdge);
|
|
visited = false;
|
|
viewport.visitChildren((RenderObject child) {
|
|
visited = true;
|
|
expect(
|
|
viewport.describeApproximatePaintClip(child as RenderSliver),
|
|
Offset.zero & viewport.size,
|
|
);
|
|
});
|
|
expect(visited, true);
|
|
});
|
|
|
|
testWidgets('Shrinkwrapping viewport asserts bounded cross axis', (WidgetTester tester) async {
|
|
final errors = <FlutterErrorDetails>[];
|
|
FlutterError.onError = (FlutterErrorDetails error) => errors.add(error);
|
|
// Vertical
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: ListView(
|
|
scrollDirection: Axis.horizontal,
|
|
children: <Widget>[
|
|
ListView(shrinkWrap: true, children: const <Widget>[SizedBox.square(dimension: 500)]),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(errors, isNotEmpty);
|
|
expect(errors.first.exception, isFlutterError);
|
|
var error = errors.first.exception as FlutterError;
|
|
expect(
|
|
error.toString(),
|
|
contains('Viewports expand in the cross axis to fill their container'),
|
|
);
|
|
errors.clear();
|
|
|
|
// Horizontal
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: ListView(
|
|
children: <Widget>[
|
|
ListView(
|
|
scrollDirection: Axis.horizontal,
|
|
shrinkWrap: true,
|
|
children: const <Widget>[SizedBox.square(dimension: 500)],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(errors, isNotEmpty);
|
|
expect(errors.first.exception, isFlutterError);
|
|
error = errors.first.exception as FlutterError;
|
|
expect(
|
|
error.toString(),
|
|
contains('Viewports expand in the cross axis to fill their container'),
|
|
);
|
|
errors.clear();
|
|
|
|
// No children
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: ListView(
|
|
scrollDirection: Axis.horizontal,
|
|
children: <Widget>[ListView(shrinkWrap: true)],
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(errors, isNotEmpty);
|
|
expect(errors.first.exception, isFlutterError);
|
|
error = errors.first.exception as FlutterError;
|
|
expect(
|
|
error.toString(),
|
|
contains('Viewports expand in the cross axis to fill their container'),
|
|
);
|
|
errors.clear();
|
|
});
|
|
|
|
testWidgets('RenderViewport maxLayoutCycles depends on the number of children', (
|
|
WidgetTester tester,
|
|
) async {
|
|
Future<void> expectFlutterError({required Widget widget, required WidgetTester tester}) async {
|
|
final errors = <FlutterErrorDetails>[];
|
|
final FlutterExceptionHandler? oldHandler = FlutterError.onError;
|
|
FlutterError.onError = (FlutterErrorDetails error) => errors.add(error);
|
|
try {
|
|
await tester.pumpWidget(widget);
|
|
} finally {
|
|
FlutterError.onError = oldHandler;
|
|
}
|
|
expect(errors, isNotEmpty);
|
|
expect(errors.first.exception, isFlutterError);
|
|
}
|
|
|
|
Widget buildWidget({required int sliverCount, required int correctionsCount}) {
|
|
return Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: CustomScrollView(
|
|
slivers: List<Widget>.generate(
|
|
sliverCount,
|
|
(_) => _ScrollOffsetCorrectionSliver(correctionsCount: correctionsCount),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// 5 correction per child will pass.
|
|
await tester.pumpWidget(buildWidget(sliverCount: 30, correctionsCount: 5));
|
|
|
|
// 15 correction per child will throw exception.
|
|
await expectFlutterError(
|
|
widget: buildWidget(sliverCount: 1, correctionsCount: 15),
|
|
tester: tester,
|
|
);
|
|
});
|
|
}
|
|
|
|
class TestCustomPainter extends CustomPainter {
|
|
void Function(Canvas canvas, Size size)? onPaint;
|
|
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
if (onPaint != null) {
|
|
onPaint!(canvas, size);
|
|
}
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(covariant CustomPainter oldDelegate) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/// A sliver that overlaps with other slivers as far as possible,
|
|
/// and does nothing else.
|
|
class _AllOverlapSliver extends LeafRenderObjectWidget {
|
|
const _AllOverlapSliver({super.key, required this.id});
|
|
|
|
final int id;
|
|
|
|
@override
|
|
RenderObject createRenderObject(BuildContext context) => _RenderAllOverlapSliver(id);
|
|
}
|
|
|
|
class _RenderAllOverlapSliver extends RenderSliver {
|
|
_RenderAllOverlapSliver(this.id);
|
|
|
|
final int id;
|
|
|
|
@override
|
|
void performLayout() {
|
|
geometry = SliverGeometry(
|
|
paintExtent: constraints.remainingPaintExtent,
|
|
maxPaintExtent: constraints.remainingPaintExtent,
|
|
layoutExtent: 0.0,
|
|
);
|
|
}
|
|
|
|
@override
|
|
bool hitTest(
|
|
SliverHitTestResult result, {
|
|
required double mainAxisPosition,
|
|
required double crossAxisPosition,
|
|
}) {
|
|
if (mainAxisPosition >= 0.0 &&
|
|
mainAxisPosition < geometry!.hitTestExtent &&
|
|
crossAxisPosition >= 0.0 &&
|
|
crossAxisPosition < constraints.crossAxisExtent) {
|
|
result.add(
|
|
SliverHitTestEntry(
|
|
this,
|
|
mainAxisPosition: mainAxisPosition,
|
|
crossAxisPosition: crossAxisPosition,
|
|
),
|
|
);
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Simple sliver that applies N scroll offset corrections.
|
|
class _RenderScrollOffsetCorrectionSliver extends RenderSliver {
|
|
int _correctionCount = 0;
|
|
@override
|
|
void performLayout() {
|
|
if (_correctionCount > 0) {
|
|
--_correctionCount;
|
|
geometry = const SliverGeometry(scrollOffsetCorrection: 1.0);
|
|
return;
|
|
}
|
|
const double extent = 5;
|
|
final double paintedChildSize = calculatePaintOffset(constraints, from: 0.0, to: extent);
|
|
final double cacheExtent = calculateCacheOffset(constraints, from: 0.0, to: extent);
|
|
|
|
geometry = SliverGeometry(
|
|
scrollExtent: extent,
|
|
paintExtent: paintedChildSize,
|
|
maxPaintExtent: extent,
|
|
cacheExtent: cacheExtent,
|
|
);
|
|
}
|
|
}
|
|
|
|
class _ScrollOffsetCorrectionSliver extends SingleChildRenderObjectWidget {
|
|
const _ScrollOffsetCorrectionSliver({required this.correctionsCount});
|
|
final int correctionsCount;
|
|
|
|
@override
|
|
_RenderScrollOffsetCorrectionSliver createRenderObject(BuildContext context) {
|
|
final sliver = _RenderScrollOffsetCorrectionSliver();
|
|
sliver._correctionCount = correctionsCount;
|
|
return sliver;
|
|
}
|
|
|
|
@override
|
|
void updateRenderObject(
|
|
BuildContext context,
|
|
covariant _RenderScrollOffsetCorrectionSliver renderObject,
|
|
) {
|
|
super.updateRenderObject(context, renderObject);
|
|
renderObject.markNeedsLayout();
|
|
renderObject._correctionCount = correctionsCount;
|
|
}
|
|
}
|