yim bdb36c50d2
Fix the incorrect position of SliverTree child nodes. (#167928)
Run the code below:
```dart
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

void main() => runApp(const MaterialApp(home: MyApp()));

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final GlobalKey _key = GlobalKey();
  final GlobalKey _childNodeKey = GlobalKey();

  late final List<TreeSliverNode<GlobalKey>> tree = [
    TreeSliverNode<GlobalKey>(_key, expanded: true, children: [
      TreeSliverNode<GlobalKey>(_childNodeKey),
    ]),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomScrollView(
        slivers: [
          TreeSliver(
            tree: tree,
            indentation: TreeSliverIndentationType.custom(40),
            treeRowExtentBuilder: (node, dimensions) => 40,
            treeNodeBuilder: (context, node, animationStyle) {
              return Container(
                key: node.content as Key,
                color: node.content == _key ? Colors.blue : Colors.green,
              );
            },
          ),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          final RenderBox rb = _childNodeKey.currentContext!.findRenderObject() as RenderBox;
          print(rb.localToGlobal(Offset.zero));
        },
        child: const Icon(Icons.add),
      ),
    );
  }
}
```
Get the screen:
![Screenshot 2025-04-28 at 22 51
50](https://github.com/user-attachments/assets/1b80a044-81ba-4338-b476-e6a726dd4c84)

When the `floatingActionButton` is clicked, it should print the position
of the green `Container`. The expected result is `Offset(40.0, 40.0)`,
but the actual result is `Offset(0.0, 40.0)`. `hitTest` also produces
incorrect results. This PR fixes the issue.



- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [ ] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- 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
2025-05-03 14:07:32 +00:00

631 lines
28 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/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
List<TreeSliverNode<String>> _setUpNodes() {
return <TreeSliverNode<String>>[
TreeSliverNode<String>('First'),
TreeSliverNode<String>(
'Second',
children: <TreeSliverNode<String>>[
TreeSliverNode<String>(
'alpha',
children: <TreeSliverNode<String>>[
TreeSliverNode<String>('uno'),
TreeSliverNode<String>('dos'),
TreeSliverNode<String>('tres'),
],
),
TreeSliverNode<String>('beta'),
TreeSliverNode<String>('kappa'),
],
),
TreeSliverNode<String>(
'Third',
expanded: true,
children: <TreeSliverNode<String>>[
TreeSliverNode<String>('gamma'),
TreeSliverNode<String>('delta'),
TreeSliverNode<String>('epsilon'),
],
),
TreeSliverNode<String>('Fourth'),
];
}
List<TreeSliverNode<String>> treeNodes = _setUpNodes();
void main() {
testWidgets('asserts proper axis directions', (WidgetTester tester) async {
final List<Object?> exceptions = <Object?>[];
final FlutterExceptionHandler? oldHandler = FlutterError.onError;
FlutterError.onError = (FlutterErrorDetails details) {
exceptions.add(details.exception);
};
addTearDown(() {
FlutterError.onError = oldHandler;
});
await tester.pumpWidget(
MaterialApp(
home: CustomScrollView(
reverse: true,
slivers: <Widget>[TreeSliver<String>(tree: treeNodes)],
),
),
);
FlutterError.onError = oldHandler;
expect(exceptions.isNotEmpty, isTrue);
expect(
exceptions[0].toString(),
contains('TreeSliver is only supported in Viewports with an AxisDirection.down.'),
);
exceptions.clear();
await tester.pumpWidget(Container());
FlutterError.onError = (FlutterErrorDetails details) {
exceptions.add(details.exception);
};
await tester.pumpWidget(
MaterialApp(
home: CustomScrollView(
scrollDirection: Axis.horizontal,
reverse: true,
slivers: <Widget>[TreeSliver<String>(tree: treeNodes)],
),
),
);
FlutterError.onError = oldHandler;
expect(exceptions.isNotEmpty, isTrue);
expect(
exceptions[0].toString(),
contains('TreeSliver is only supported in Viewports with an AxisDirection.down.'),
);
exceptions.clear();
await tester.pumpWidget(Container());
FlutterError.onError = (FlutterErrorDetails details) {
exceptions.add(details.exception);
};
await tester.pumpWidget(
MaterialApp(
home: CustomScrollView(
scrollDirection: Axis.horizontal,
slivers: <Widget>[TreeSliver<String>(tree: treeNodes)],
),
),
);
FlutterError.onError = oldHandler;
expect(exceptions.isNotEmpty, isTrue);
expect(
exceptions[0].toString(),
contains('TreeSliver is only supported in Viewports with an AxisDirection.down.'),
);
});
testWidgets('Basic layout', (WidgetTester tester) async {
treeNodes = _setUpNodes();
// Default layout, custom indentation values, row extents.
TreeSliver<String> treeSliver = TreeSliver<String>(tree: treeNodes);
await tester.pumpWidget(MaterialApp(home: CustomScrollView(slivers: <Widget>[treeSliver])));
await tester.pump();
expect(
find.byType(TreeSliver<String>),
paints
..paragraph(offset: const Offset(46.0, 8.0))
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 48.0))
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 88.0))
..paragraph(offset: const Offset(56.0, 128.0))
..paragraph(offset: const Offset(56.0, 168.0))
..paragraph(offset: const Offset(56.0, 208.0))
..paragraph(offset: const Offset(46.0, 248.0)),
);
expect(find.text('First'), findsOneWidget);
expect(tester.getRect(find.text('First')), const Rect.fromLTRB(46.0, 8.0, 286.0, 32.0));
expect(find.text('Second'), findsOneWidget);
expect(tester.getRect(find.text('Second')), const Rect.fromLTRB(46.0, 48.0, 334.0, 72.0));
expect(find.text('Third'), findsOneWidget);
expect(tester.getRect(find.text('Third')), const Rect.fromLTRB(46.0, 88.0, 286.0, 112.0));
expect(find.text('gamma'), findsOneWidget);
expect(tester.getRect(find.text('gamma')), const Rect.fromLTRB(56.0, 128.0, 296.0, 152.0));
expect(find.text('delta'), findsOneWidget);
expect(tester.getRect(find.text('delta')), const Rect.fromLTRB(56.0, 168.0, 296.0, 192.0));
expect(find.text('epsilon'), findsOneWidget);
expect(tester.getRect(find.text('epsilon')), const Rect.fromLTRB(56.0, 208.0, 392.0, 232.0));
expect(find.text('Fourth'), findsOneWidget);
expect(tester.getRect(find.text('Fourth')), const Rect.fromLTRB(46.0, 248.0, 334.0, 272.0));
treeSliver = TreeSliver<String>(tree: treeNodes, indentation: TreeSliverIndentationType.none);
await tester.pumpWidget(MaterialApp(home: CustomScrollView(slivers: <Widget>[treeSliver])));
await tester.pump();
expect(
find.byType(TreeSliver<String>),
paints
..paragraph(offset: const Offset(46.0, 8.0))
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 48.0))
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 88.0))
..paragraph(offset: const Offset(46.0, 128.0))
..paragraph(offset: const Offset(46.0, 168.0))
..paragraph(offset: const Offset(46.0, 208.0))
..paragraph(offset: const Offset(46.0, 248.0)),
);
expect(find.text('First'), findsOneWidget);
expect(tester.getRect(find.text('First')), const Rect.fromLTRB(46.0, 8.0, 286.0, 32.0));
expect(find.text('Second'), findsOneWidget);
expect(tester.getRect(find.text('Second')), const Rect.fromLTRB(46.0, 48.0, 334.0, 72.0));
expect(find.text('Third'), findsOneWidget);
expect(tester.getRect(find.text('Third')), const Rect.fromLTRB(46.0, 88.0, 286.0, 112.0));
expect(find.text('gamma'), findsOneWidget);
expect(tester.getRect(find.text('gamma')), const Rect.fromLTRB(46.0, 128.0, 286.0, 152.0));
expect(find.text('delta'), findsOneWidget);
expect(tester.getRect(find.text('delta')), const Rect.fromLTRB(46.0, 168.0, 286.0, 192.0));
expect(find.text('epsilon'), findsOneWidget);
expect(tester.getRect(find.text('epsilon')), const Rect.fromLTRB(46.0, 208.0, 382.0, 232.0));
expect(find.text('Fourth'), findsOneWidget);
expect(tester.getRect(find.text('Fourth')), const Rect.fromLTRB(46.0, 248.0, 334.0, 272.0));
treeSliver = TreeSliver<String>(
tree: treeNodes,
indentation: TreeSliverIndentationType.custom(50.0),
);
await tester.pumpWidget(MaterialApp(home: CustomScrollView(slivers: <Widget>[treeSliver])));
await tester.pump();
expect(
find.byType(TreeSliver<String>),
paints
..paragraph(offset: const Offset(46.0, 8.0))
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 48.0))
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 88.0))
..paragraph(offset: const Offset(96.0, 128.0))
..paragraph(offset: const Offset(96.0, 168.0))
..paragraph(offset: const Offset(96.0, 208.0))
..paragraph(offset: const Offset(46.0, 248.0)),
);
expect(find.text('First'), findsOneWidget);
expect(tester.getRect(find.text('First')), const Rect.fromLTRB(46.0, 8.0, 286.0, 32.0));
expect(find.text('Second'), findsOneWidget);
expect(tester.getRect(find.text('Second')), const Rect.fromLTRB(46.0, 48.0, 334.0, 72.0));
expect(find.text('Third'), findsOneWidget);
expect(tester.getRect(find.text('Third')), const Rect.fromLTRB(46.0, 88.0, 286.0, 112.0));
expect(find.text('gamma'), findsOneWidget);
expect(tester.getRect(find.text('gamma')), const Rect.fromLTRB(96.0, 128.0, 336.0, 152.0));
expect(find.text('delta'), findsOneWidget);
expect(tester.getRect(find.text('delta')), const Rect.fromLTRB(96.0, 168.0, 336.0, 192.0));
expect(find.text('epsilon'), findsOneWidget);
expect(tester.getRect(find.text('epsilon')), const Rect.fromLTRB(96.0, 208.0, 432.0, 232.0));
expect(find.text('Fourth'), findsOneWidget);
expect(tester.getRect(find.text('Fourth')), const Rect.fromLTRB(46.0, 248.0, 334.0, 272.0));
treeSliver = TreeSliver<String>(tree: treeNodes, treeRowExtentBuilder: (_, _) => 100);
await tester.pumpWidget(MaterialApp(home: CustomScrollView(slivers: <Widget>[treeSliver])));
await tester.pump();
expect(
find.byType(TreeSliver<String>),
paints
..paragraph(offset: const Offset(46.0, 26.0))
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 126.0))
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 226.0))
..paragraph(offset: const Offset(56.0, 326.0))
..paragraph(offset: const Offset(56.0, 426.0))
..paragraph(offset: const Offset(56.0, 526.0)),
);
expect(find.text('First'), findsOneWidget);
expect(tester.getRect(find.text('First')), const Rect.fromLTRB(46.0, 26.0, 286.0, 74.0));
expect(find.text('Second'), findsOneWidget);
expect(tester.getRect(find.text('Second')), const Rect.fromLTRB(46.0, 126.0, 334.0, 174.0));
expect(find.text('Third'), findsOneWidget);
expect(tester.getRect(find.text('Third')), const Rect.fromLTRB(46.0, 226.0, 286.0, 274.0));
expect(find.text('gamma'), findsOneWidget);
expect(tester.getRect(find.text('gamma')), const Rect.fromLTRB(56.0, 326.0, 296.0, 374.0));
expect(find.text('delta'), findsOneWidget);
expect(tester.getRect(find.text('delta')), const Rect.fromLTRB(56.0, 426.0, 296.0, 474.0));
expect(find.text('epsilon'), findsOneWidget);
expect(tester.getRect(find.text('epsilon')), const Rect.fromLTRB(56.0, 526.0, 392.0, 574.0));
expect(find.text('Fourth'), findsNothing);
});
testWidgets('Animating node segment', (WidgetTester tester) async {
treeNodes = _setUpNodes();
TreeSliver<String> treeSliver = TreeSliver<String>(tree: treeNodes);
await tester.pumpWidget(MaterialApp(home: CustomScrollView(slivers: <Widget>[treeSliver])));
await tester.pump();
expect(
find.byType(TreeSliver<String>),
paints
..paragraph(offset: const Offset(46.0, 8.0))
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 48.0))
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 88.0))
..paragraph(offset: const Offset(56.0, 128.0))
..paragraph(offset: const Offset(56.0, 168.0))
..paragraph(offset: const Offset(56.0, 208.0))
..paragraph(offset: const Offset(46.0, 248.0)),
);
expect(find.text('alpha'), findsNothing);
await tester.tap(find.byType(Icon).first);
await tester.pump();
expect(
find.byType(TreeSliver<String>),
paints
..paragraph(offset: const Offset(46.0, 8.0))
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 48.0))
..paragraph(offset: const Offset(56.0, 8.0)) // beta animating in
..paragraph(offset: const Offset(56.0, 48.0)) // kappa animating in
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 88.0))
..paragraph(offset: const Offset(56.0, 128.0))
..paragraph(offset: const Offset(56.0, 168.0))
..paragraph(offset: const Offset(56.0, 208.0))
..paragraph(offset: const Offset(46.0, 248.0)),
);
// New nodes have been inserted into the tree, alpha
// is not visible yet.
expect(find.text('alpha'), findsNothing);
expect(find.text('beta'), findsOneWidget);
expect(tester.getRect(find.text('beta')), const Rect.fromLTRB(56.0, 8.0, 248.0, 32.0));
expect(find.text('kappa'), findsOneWidget);
expect(tester.getRect(find.text('kappa')), const Rect.fromLTRB(56.0, 48.0, 296.0, 72.0));
// Progress the animation.
await tester.pump(const Duration(milliseconds: 50));
expect(
find.byType(TreeSliver<String>),
paints
..paragraph(offset: const Offset(46.0, 8.0))
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 48.0))
..paragraph() // alpha icon
..paragraph(offset: const Offset(56.0, 8.0)) // alpha animating in
..paragraph(offset: const Offset(56.0, 48.0)) // beta animating in
..paragraph(offset: const Offset(56.0, 88.0)) // kappa animating in
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 128.0))
..paragraph(offset: const Offset(56.0, 168.0))
..paragraph(offset: const Offset(56.0, 208.0))
..paragraph(offset: const Offset(56.0, 248.0))
..paragraph(offset: const Offset(46.0, 288.0)),
);
expect(tester.getRect(find.text('alpha')).top.floor(), 8.0);
expect(find.text('beta'), findsOneWidget);
expect(tester.getRect(find.text('beta')).top.floor(), 48.0);
expect(find.text('kappa'), findsOneWidget);
expect(tester.getRect(find.text('kappa')).top.floor(), 88.0);
// Complete the animation
await tester.pumpAndSettle();
expect(
find.byType(TreeSliver<String>),
paints
..paragraph(offset: const Offset(46.0, 8.0)) // First
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 48.0)) // Second
..paragraph() // alpha icon
..paragraph(offset: const Offset(56.0, 88.0)) // alpha
..paragraph(offset: const Offset(56.0, 128.0)) // beta
..paragraph(offset: const Offset(56.0, 168.0)) // kappa
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 208.0)) // Third
..paragraph(offset: const Offset(56.0, 248.0)) // gamma
..paragraph(offset: const Offset(56.0, 288.0)) // delta
..paragraph(offset: const Offset(56.0, 328.0)) // epsilon
..paragraph(offset: const Offset(46.0, 368.0)), // Fourth
);
expect(find.text('alpha'), findsOneWidget);
expect(tester.getRect(find.text('alpha')), const Rect.fromLTRB(56.0, 88.0, 296.0, 112.0));
expect(find.text('beta'), findsOneWidget);
expect(tester.getRect(find.text('beta')), const Rect.fromLTRB(56.0, 128.0, 248.0, 152.0));
expect(find.text('kappa'), findsOneWidget);
expect(tester.getRect(find.text('kappa')), const Rect.fromLTRB(56.0, 168.0, 296.0, 192.0));
// Customize the animation
treeSliver = TreeSliver<String>(
tree: treeNodes,
toggleAnimationStyle: const AnimationStyle(
duration: Duration(milliseconds: 500),
curve: Curves.bounceIn,
),
);
await tester.pumpWidget(MaterialApp(home: CustomScrollView(slivers: <Widget>[treeSliver])));
await tester.pump();
expect(
find.byType(TreeSliver<String>),
paints
..paragraph(offset: const Offset(46.0, 8.0)) // First
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 48.0)) // Second
..paragraph() // alpha icon
..paragraph(offset: const Offset(56.0, 88.0)) // alpha
..paragraph(offset: const Offset(56.0, 128.0)) // beta
..paragraph(offset: const Offset(56.0, 168.0)) // kappa
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 208.0)) // Third
..paragraph(offset: const Offset(56.0, 248.0)) // gamma
..paragraph(offset: const Offset(56.0, 288.0)) // delta
..paragraph(offset: const Offset(56.0, 328.0)) // epsilon
..paragraph(offset: const Offset(46.0, 368.0)), // Fourth
);
// Still visible from earlier.
expect(find.text('alpha'), findsOneWidget);
expect(tester.getRect(find.text('alpha')), const Rect.fromLTRB(56.0, 88.0, 296.0, 112.0));
// Collapse the node now
await tester.tap(find.byType(Icon).first);
await tester.pump();
await tester.pump(const Duration(milliseconds: 200));
expect(find.text('alpha'), findsOneWidget);
expect(tester.getRect(find.text('alpha')).top.floor(), -22);
expect(find.text('beta'), findsOneWidget);
expect(tester.getRect(find.text('beta')).top.floor(), 18);
expect(find.text('kappa'), findsOneWidget);
expect(tester.getRect(find.text('kappa')).top.floor(), 58);
// Progress the animation.
await tester.pump(const Duration(milliseconds: 200));
expect(find.text('alpha'), findsOneWidget);
expect(tester.getRect(find.text('alpha')).top.floor(), -25);
expect(find.text('beta'), findsOneWidget);
expect(tester.getRect(find.text('beta')).top.floor(), 15);
expect(find.text('kappa'), findsOneWidget);
expect(tester.getRect(find.text('kappa')).top.floor(), 55.0);
// Complete the animation
await tester.pumpAndSettle();
expect(
find.byType(TreeSliver<String>),
paints
..paragraph(offset: const Offset(46.0, 8.0))
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 48.0))
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 88.0))
..paragraph(offset: const Offset(56.0, 128.0))
..paragraph(offset: const Offset(56.0, 168.0))
..paragraph(offset: const Offset(56.0, 208.0))
..paragraph(offset: const Offset(46.0, 248.0)),
);
expect(find.text('alpha'), findsNothing);
// Disable the animation
treeSliver = TreeSliver<String>(
tree: treeNodes,
toggleAnimationStyle: AnimationStyle.noAnimation,
);
await tester.pumpWidget(MaterialApp(home: CustomScrollView(slivers: <Widget>[treeSliver])));
await tester.pump();
expect(
find.byType(TreeSliver<String>),
paints
..paragraph(offset: const Offset(46.0, 8.0))
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 48.0))
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 88.0))
..paragraph(offset: const Offset(56.0, 128.0))
..paragraph(offset: const Offset(56.0, 168.0))
..paragraph(offset: const Offset(56.0, 208.0))
..paragraph(offset: const Offset(46.0, 248.0)),
);
// Not in the tree.
expect(find.text('alpha'), findsNothing);
// Collapse the node now
await tester.tap(find.byType(Icon).first);
await tester.pump();
// No animating, straight to positions.
expect(
find.byType(TreeSliver<String>),
paints
..paragraph(offset: const Offset(46.0, 8.0)) // First
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 48.0)) // Second
..paragraph() // alpha icon
..paragraph(offset: const Offset(56.0, 88.0)) // alpha
..paragraph(offset: const Offset(56.0, 128.0)) // beta
..paragraph(offset: const Offset(56.0, 168.0)) // kappa
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 208.0)) // Third
..paragraph(offset: const Offset(56.0, 248.0)) // gamma
..paragraph(offset: const Offset(56.0, 288.0)) // delta
..paragraph(offset: const Offset(56.0, 328.0)) // epsilon
..paragraph(offset: const Offset(46.0, 368.0)), // Fourth
);
expect(find.text('alpha'), findsOneWidget);
expect(tester.getRect(find.text('alpha')), const Rect.fromLTRB(56.0, 88.0, 296.0, 112.0));
expect(find.text('beta'), findsOneWidget);
expect(tester.getRect(find.text('beta')), const Rect.fromLTRB(56.0, 128.0, 248.0, 152.0));
expect(find.text('kappa'), findsOneWidget);
expect(tester.getRect(find.text('kappa')), const Rect.fromLTRB(56.0, 168.0, 296.0, 192.0));
});
testWidgets('Multiple animating node segments', (WidgetTester tester) async {
treeNodes = _setUpNodes();
final TreeSliver<String> treeSliver = TreeSliver<String>(tree: treeNodes);
await tester.pumpWidget(MaterialApp(home: CustomScrollView(slivers: <Widget>[treeSliver])));
await tester.pump();
expect(
find.byType(TreeSliver<String>),
paints
..paragraph(offset: const Offset(46.0, 8.0))
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 48.0))
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 88.0))
..paragraph(offset: const Offset(56.0, 128.0))
..paragraph(offset: const Offset(56.0, 168.0))
..paragraph(offset: const Offset(56.0, 208.0))
..paragraph(offset: const Offset(46.0, 248.0)),
);
expect(find.text('Second'), findsOneWidget);
expect(find.text('alpha'), findsNothing); // Second is collapsed
expect(find.text('Third'), findsOneWidget);
expect(find.text('gamma'), findsOneWidget); // Third is expanded
expect(tester.getRect(find.text('Second')), const Rect.fromLTRB(46.0, 48.0, 334.0, 72.0));
expect(tester.getRect(find.text('Third')), const Rect.fromLTRB(46.0, 88.0, 286.0, 112.0));
expect(tester.getRect(find.text('gamma')), const Rect.fromLTRB(56.0, 128.0, 296.0, 152.0));
// Trigger two animations to run together.
// Collapse Third
await tester.tap(find.byType(Icon).last);
// Expand Second
await tester.tap(find.byType(Icon).first);
await tester.pump(const Duration(milliseconds: 15));
expect(
find.byType(TreeSliver<String>),
paints
..paragraph(offset: const Offset(46.0, 8.0))
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 48.0))
..paragraph(offset: const Offset(56.0, 8.0)) // beta entering
..paragraph(offset: const Offset(56.0, 48.0)) // kappa entering
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 88.0))
..paragraph(offset: const Offset(56.0, 128.0))
..paragraph(offset: const Offset(56.0, 168.0))
..paragraph(offset: const Offset(56.0, 208.0))
..paragraph(offset: const Offset(46.0, 248.0)),
);
// Third is collapsing
expect(tester.getRect(find.text('Third')), const Rect.fromLTRB(46.0, 88.0, 286.0, 112.0));
expect(tester.getRect(find.text('gamma')), const Rect.fromLTRB(56.0, 128.0, 296.0, 152.0));
// Second is expanding
expect(tester.getRect(find.text('Second')), const Rect.fromLTRB(46.0, 48.0, 334.0, 72.0));
// beta has been added and is animating into view.
expect(tester.getRect(find.text('beta')).top.floor(), 8.0);
await tester.pump(const Duration(milliseconds: 15));
expect(
find.byType(TreeSliver<String>),
paints
..paragraph(offset: const Offset(46.0, 8.0))
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 48.0))
..paragraph() // alpha icon animating
..paragraph(offset: const Offset(56.0, -20.0)) // alpha animating
..paragraph(offset: const Offset(56.0, 20.0)) // beta
..paragraph(offset: const Offset(56.0, 60.0)) // kappa
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 100.0)) // Third
// Children of Third are animating, but the expand and
// collapse counter each other, so their position is unchanged.
..paragraph(offset: const Offset(56.0, 128.0))
..paragraph(offset: const Offset(56.0, 168.0))
..paragraph(offset: const Offset(56.0, 208.0))
..paragraph(offset: const Offset(46.0, 248.0)),
);
// Third is still collapsing. Third is sliding down
// as Seconds's children slide in, gamma is still exiting.
expect(tester.getRect(find.text('Third')).top.floor(), 100.0);
// gamma appears to not have moved, this is because it is
// intersecting both animations, the positive offset of
// Second animation == the negative offset of Third
expect(tester.getRect(find.text('gamma')), const Rect.fromLTRB(56.0, 128.0, 296.0, 152.0));
// Second is still expanding
expect(tester.getRect(find.text('Second')), const Rect.fromLTRB(46.0, 48.0, 334.0, 72.0));
// alpha is still animating into view.
expect(tester.getRect(find.text('alpha')).top.floor(), -20.0);
// Progress the animation further
await tester.pump(const Duration(milliseconds: 15));
expect(
find.byType(TreeSliver<String>),
paints
..paragraph(offset: const Offset(46.0, 8.0))
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 48.0))
..paragraph() // alpha icon animating
..paragraph(offset: const Offset(56.0, -8.0)) // alpha animating
..paragraph(offset: const Offset(56.0, 32.0)) // beta
..paragraph(offset: const Offset(56.0, 72.0)) // kappa
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 112.0)) // Third
// Children of Third are animating, but the expand and
// collapse counter each other, so their position is unchanged.
..paragraph(offset: const Offset(56.0, 128.0))
..paragraph(offset: const Offset(56.0, 168.0))
..paragraph(offset: const Offset(56.0, 208.0))
..paragraph(offset: const Offset(46.0, 248.0)),
);
// Third is still collapsing. Third is sliding down
// as Seconds's children slide in, gamma is still exiting.
expect(tester.getRect(find.text('Third')).top.floor(), 112.0);
// gamma appears to not have moved, this is because it is
// intersecting both animations, the positive offset of
// Second animation == the negative offset of Third
expect(tester.getRect(find.text('gamma')), const Rect.fromLTRB(56.0, 128.0, 296.0, 152.0));
// Second is still expanding
expect(tester.getRect(find.text('Second')), const Rect.fromLTRB(46.0, 48.0, 334.0, 72.0));
// alpha is still animating into view.
expect(tester.getRect(find.text('alpha')).top.floor(), -8.0);
// Complete the animations
await tester.pumpAndSettle();
expect(
find.byType(TreeSliver<String>),
paints
..paragraph(offset: const Offset(46.0, 8.0))
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 48.0))
..paragraph() // Icon
..paragraph(offset: const Offset(56.0, 88.0))
..paragraph(offset: const Offset(56.0, 128.0))
..paragraph(offset: const Offset(56.0, 168.0))
..paragraph() // Icon
..paragraph(offset: const Offset(46.0, 208.0))
..paragraph(offset: const Offset(46.0, 248.0)),
);
expect(tester.getRect(find.text('Third')), const Rect.fromLTRB(46.0, 208.0, 286.0, 232.0));
// gamma has left the building
expect(find.text('gamma'), findsNothing);
expect(tester.getRect(find.text('Second')), const Rect.fromLTRB(46.0, 48.0, 334.0, 72.0));
// alpha is in place.
expect(tester.getRect(find.text('alpha')), const Rect.fromLTRB(56.0, 88.0, 296.0, 112.0));
});
testWidgets('only paints visible rows', (WidgetTester tester) async {
treeNodes = _setUpNodes();
final ScrollController scrollController = ScrollController();
addTearDown(scrollController.dispose);
treeNodes = _setUpNodes();
final TreeSliver<String> treeSliver = TreeSliver<String>(
treeRowExtentBuilder: (_, _) => 200,
tree: treeNodes,
);
await tester.pumpWidget(
MaterialApp(
home: CustomScrollView(controller: scrollController, slivers: <Widget>[treeSliver]),
),
);
await tester.pump();
expect(scrollController.position.pixels, 0.0);
expect(scrollController.position.maxScrollExtent, 800.0);
bool rowNeedsPaint(String row) {
return find.text(row).evaluate().first.renderObject!.debugNeedsPaint;
}
expect(rowNeedsPaint('First'), isFalse);
expect(rowNeedsPaint('Second'), isFalse);
expect(rowNeedsPaint('Third'), isFalse);
expect(find.text('gamma'), findsNothing); // Not visible
// Change the scroll offset
scrollController.jumpTo(200);
await tester.pump();
expect(find.text('First'), findsNothing);
expect(rowNeedsPaint('Second'), isFalse);
expect(rowNeedsPaint('Third'), isFalse);
expect(rowNeedsPaint('gamma'), isFalse); // Now visible
});
}