From 27e06569a157b9a59ed36a640ae2b3e6852223ed Mon Sep 17 00:00:00 2001 From: "auto-submit[bot]" <98614782+auto-submit[bot]@users.noreply.github.com> Date: Wed, 5 Jun 2024 17:17:19 +0000 Subject: [PATCH] Reverts "TreeSliver & associated classes (#147171)" (#149754) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reverts: flutter/flutter#147171 Initiated by: QuncCccccc Reason for reverting: tree is red due to a test in this PR Original PR Author: Piinks Reviewed By: {QuncCccccc, TahaTesser, bleroux} This change reverts the following previous change: **FYI for Reviewers:** Much of the API surface matches that of the 2D TreeView in https://github.com/flutter/packages/pull/6592. If it changes here, it should change there, and vice versa. 📜 [Design Document](https://docs.google.com/document/d/1-aFI7VjkF9yMkWpP94J8T_JREDS-M3bOak26PVehUYg/edit?usp=sharing) This adds classes and associated callbacks and controllers for TreeSliver. Core components: - TreeSliver - RenderTreeSliver - TreeSliverNode - TreeSliverController - TreeSliverStateMixin - TreeSliverIndentationType Fixes https://github.com/flutter/flutter/issues/114299 https://github.com/flutter/flutter/assets/16964204/3facd095-7262-4068-aa33-d713e2deca99 https://github.com/flutter/flutter/assets/16964204/f851ae30-8e71-45c7-82a4-9606986a5872 --- .../api/lib/widgets/sliver/sliver_tree.0.dart | 104 -- .../api/lib/widgets/sliver/sliver_tree.1.dart | 189 ---- .../widgets/sliver/sliver_tree.0_test.dart | 21 - .../widgets/sliver/sliver_tree.1_test.dart | 21 - packages/flutter/lib/rendering.dart | 1 - .../rendering/sliver_multi_box_adaptor.dart | 1 - .../lib/src/rendering/sliver_tree.dart | 404 ------- .../flutter/lib/src/widgets/sliver_tree.dart | 997 ------------------ packages/flutter/lib/widgets.dart | 1 - .../test/rendering/sliver_tree_test.dart | 860 --------------- .../test/widgets/sliver_tree_test.dart | 727 ------------- 11 files changed, 3326 deletions(-) delete mode 100644 examples/api/lib/widgets/sliver/sliver_tree.0.dart delete mode 100644 examples/api/lib/widgets/sliver/sliver_tree.1.dart delete mode 100644 examples/api/test/widgets/sliver/sliver_tree.0_test.dart delete mode 100644 examples/api/test/widgets/sliver/sliver_tree.1_test.dart delete mode 100644 packages/flutter/lib/src/rendering/sliver_tree.dart delete mode 100644 packages/flutter/lib/src/widgets/sliver_tree.dart delete mode 100644 packages/flutter/test/rendering/sliver_tree_test.dart delete mode 100644 packages/flutter/test/widgets/sliver_tree_test.dart diff --git a/examples/api/lib/widgets/sliver/sliver_tree.0.dart b/examples/api/lib/widgets/sliver/sliver_tree.0.dart deleted file mode 100644 index 914815752d..0000000000 --- a/examples/api/lib/widgets/sliver/sliver_tree.0.dart +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/material.dart'; - -/// Flutter code sample for [TreeSliver]. - -void main() => runApp(const TreeSliverExampleApp()); - -class TreeSliverExampleApp extends StatelessWidget { - const TreeSliverExampleApp({super.key}); - - @override - Widget build(BuildContext context) { - return const MaterialApp( - home: TreeSliverExample(), - ); - } -} - -class TreeSliverExample extends StatefulWidget { - const TreeSliverExample({super.key}); - - @override - State createState() => _TreeSliverExampleState(); -} - -class _TreeSliverExampleState extends State { - TreeSliverNode? _selectedNode; - final TreeSliverController controller = TreeSliverController(); - final List> _tree = >[ - TreeSliverNode('First'), - TreeSliverNode( - 'Second', - children: >[ - TreeSliverNode( - 'alpha', - children: >[ - TreeSliverNode('uno'), - TreeSliverNode('dos'), - TreeSliverNode('tres'), - ], - ), - TreeSliverNode('beta'), - TreeSliverNode('kappa'), - ], - ), - TreeSliverNode( - 'Third', - expanded: true, - children: >[ - TreeSliverNode('gamma'), - TreeSliverNode('delta'), - TreeSliverNode('epsilon'), - ], - ), - TreeSliverNode('Fourth'), - ]; - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('TreeSliver Demo'), - ), - body: CustomScrollView( - slivers: [ - TreeSliver( - tree: _tree, - controller: controller, - treeNodeBuilder: ( - BuildContext context, - TreeSliverNode node, - AnimationStyle animationStyle, - ) { - Widget child = GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () { - setState(() { - controller.toggleNode(node); - _selectedNode = node as TreeSliverNode; - }); - }, - child: TreeSliver.defaultTreeNodeBuilder( - context, - node, - animationStyle, - ), - ); - if (_selectedNode == node as TreeSliverNode) { - child = ColoredBox( - color: Colors.purple[100]!, - child: child, - ); - } - return child; - }, - ), - ], - ), - ); - } -} diff --git a/examples/api/lib/widgets/sliver/sliver_tree.1.dart b/examples/api/lib/widgets/sliver/sliver_tree.1.dart deleted file mode 100644 index b5d8b783f3..0000000000 --- a/examples/api/lib/widgets/sliver/sliver_tree.1.dart +++ /dev/null @@ -1,189 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; - -/// Flutter code sample for [TreeSliver]. - -void main() => runApp(const TreeSliverExampleApp()); - -class TreeSliverExampleApp extends StatelessWidget { - const TreeSliverExampleApp({super.key}); - - @override - Widget build(BuildContext context) { - return const MaterialApp( - home: TreeSliverExample(), - ); - } -} - -class TreeSliverExample extends StatefulWidget { - const TreeSliverExample({super.key}); - - @override - State createState() => _TreeSliverExampleState(); -} - -class _TreeSliverExampleState extends State { - TreeSliverNode? _selectedNode; - final List> tree = >[ - TreeSliverNode('README.md'), - TreeSliverNode('analysis_options.yaml'), - TreeSliverNode( - 'lib', - children: >[ - TreeSliverNode( - 'src', - children: >[ - TreeSliverNode( - 'widgets', - children: >[ - TreeSliverNode('about.dart.dart'), - TreeSliverNode('app.dart'), - TreeSliverNode('basic.dart'), - TreeSliverNode('constants.dart'), - ], - ), - ], - ), - TreeSliverNode('widgets.dart'), - ], - ), - TreeSliverNode('pubspec.lock'), - TreeSliverNode('pubspec.yaml'), - TreeSliverNode( - 'test', - children: >[ - TreeSliverNode( - 'widgets', - children: >[ - TreeSliverNode('about_test.dart'), - TreeSliverNode('app_test.dart'), - TreeSliverNode('basic_test.dart'), - TreeSliverNode('constants_test.dart'), - ], - ), - ], - ), - ]; - - Widget _treeNodeBuilder( - BuildContext context, - TreeSliverNode node, - AnimationStyle toggleAnimationStyle, - ) { - final bool isParentNode = node.children.isNotEmpty; - final BorderSide border = BorderSide( - width: 2, - color: Colors.purple[300]!, - ); - return TreeSliver.wrapChildToToggleNode( - node: node, - child: Row( - children: [ - // Custom indentation - SizedBox(width: 10.0 * node.depth! + 8.0), - DecoratedBox( - decoration: BoxDecoration( - border: node.parent != null - ? Border(left: border, bottom: border) - : null, - ), - child: const SizedBox(height: 50.0, width: 20.0), - ), - // Leading icon for parent nodes - if (isParentNode) - DecoratedBox( - decoration: BoxDecoration(border: Border.all()), - child: SizedBox.square( - dimension: 20.0, - child: Icon( - node.isExpanded ? Icons.remove : Icons.add, - size: 14, - ), - ), - ), - // Spacer - const SizedBox(width: 8.0), - // Content - Text(node.content.toString()), - ], - ), - ); - } - - Widget _getTree() { - return DecoratedSliver( - decoration: BoxDecoration( border: Border.all()), - sliver: TreeSliver( - tree: tree, - onNodeToggle: (TreeSliverNode node) { - setState(() { - _selectedNode = node as TreeSliverNode; - }); - }, - treeNodeBuilder: _treeNodeBuilder, - treeRowExtentBuilder: ( - TreeSliverNode node, - SliverLayoutDimensions layoutDimensions, - ) { - // This gives more space to parent nodes. - return node.children.isNotEmpty ? 60.0 : 50.0; - }, - // No internal indentation, the custom treeNodeBuilder applies its - // own indentation to decorate in the indented space. - indentation: TreeSliverIndentationType.none, - ), - ); - } - - @override - Widget build(BuildContext context) { - // This example is assumes the full screen is available. - final Size screenSize = MediaQuery.sizeOf(context); - final List selectedChildren = []; - if (_selectedNode != null) { - selectedChildren.addAll([ - const Spacer(), - Icon( - _selectedNode!.children.isEmpty - ? Icons.file_open_outlined - : Icons.folder_outlined, - ), - const SizedBox(height: 16.0), - Text(_selectedNode!.content), - const Spacer(), - ]); - } - return Scaffold( - body: Row(children: [ - SizedBox( - width: screenSize.width / 2, - height: double.infinity, - child: CustomScrollView( - slivers: [ - _getTree(), - ], - ), - ), - DecoratedBox( - decoration: BoxDecoration( - border: Border.all(), - ), - child: SizedBox( - width: screenSize.width / 2, - height: double.infinity, - child: Center( - child: Column( - children: selectedChildren, - ), - ), - ), - ), - ]), - ); - } -} diff --git a/examples/api/test/widgets/sliver/sliver_tree.0_test.dart b/examples/api/test/widgets/sliver/sliver_tree.0_test.dart deleted file mode 100644 index 46d77c06f3..0000000000 --- a/examples/api/test/widgets/sliver/sliver_tree.0_test.dart +++ /dev/null @@ -1,21 +0,0 @@ -// 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_api_samples/widgets/sliver/sliver_tree.0.dart' - as example; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - testWidgets('Can toggle nodes in TreeSliver', (WidgetTester tester) async { - await tester.pumpWidget( - const example.TreeSliverExampleApp(), - ); - expect(find.text('Second'), findsOneWidget); - expect(find.text('alpha'), findsNothing); - // Toggle tree node. - await tester.tap(find.text('Second')); - await tester.pumpAndSettle(); - expect(find.text('alpha'), findsOneWidget); - }); -} diff --git a/examples/api/test/widgets/sliver/sliver_tree.1_test.dart b/examples/api/test/widgets/sliver/sliver_tree.1_test.dart deleted file mode 100644 index d54f839c6b..0000000000 --- a/examples/api/test/widgets/sliver/sliver_tree.1_test.dart +++ /dev/null @@ -1,21 +0,0 @@ -// 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_api_samples/widgets/sliver/sliver_tree.1.dart' - as example; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - testWidgets('Can toggle nodes in TreeSliver', (WidgetTester tester) async { - await tester.pumpWidget( - const example.TreeSliverExampleApp(), - ); - expect(find.text('lib'), findsOneWidget); - expect(find.text('src'), findsNothing); - // Toggle tree node. - await tester.tap(find.text('lib')); - await tester.pumpAndSettle(); - expect(find.text('src'), findsOneWidget); - }); -} diff --git a/packages/flutter/lib/rendering.dart b/packages/flutter/lib/rendering.dart index dcf36cca13..20705ce17c 100644 --- a/packages/flutter/lib/rendering.dart +++ b/packages/flutter/lib/rendering.dart @@ -67,7 +67,6 @@ export 'src/rendering/sliver_list.dart'; export 'src/rendering/sliver_multi_box_adaptor.dart'; export 'src/rendering/sliver_padding.dart'; export 'src/rendering/sliver_persistent_header.dart'; -export 'src/rendering/sliver_tree.dart'; export 'src/rendering/stack.dart'; export 'src/rendering/table.dart'; export 'src/rendering/table_border.dart'; diff --git a/packages/flutter/lib/src/rendering/sliver_multi_box_adaptor.dart b/packages/flutter/lib/src/rendering/sliver_multi_box_adaptor.dart index 31d163d32d..4812ce726d 100644 --- a/packages/flutter/lib/src/rendering/sliver_multi_box_adaptor.dart +++ b/packages/flutter/lib/src/rendering/sliver_multi_box_adaptor.dart @@ -8,7 +8,6 @@ import 'package:vector_math/vector_math_64.dart'; import 'box.dart'; import 'object.dart'; import 'sliver.dart'; -import 'sliver_fixed_extent_list.dart'; /// A delegate used by [RenderSliverMultiBoxAdaptor] to manage its children. /// diff --git a/packages/flutter/lib/src/rendering/sliver_tree.dart b/packages/flutter/lib/src/rendering/sliver_tree.dart deleted file mode 100644 index 11f9d5fa98..0000000000 --- a/packages/flutter/lib/src/rendering/sliver_tree.dart +++ /dev/null @@ -1,404 +0,0 @@ -// 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 'dart:math' as math; - -import 'package:flutter/foundation.dart'; - -import 'box.dart'; -import 'layer.dart'; -import 'object.dart'; -import 'sliver.dart'; -import 'sliver_fixed_extent_list.dart'; -import 'sliver_multi_box_adaptor.dart'; - -/// Represents the animation of the children of a parent [TreeSliverNode] that -/// are animating into or out of view. -/// -/// The `fromIndex` and `toIndex` identify the animating children following -/// the parent, with the `value` representing the status of the current -/// animation. The value of `toIndex` is inclusive, meaning the child at that -/// index is included in the animating segment. -/// -/// Provided to [RenderTreeSliver] as part of -/// [RenderTreeSliver.activeAnimations] by [TreeSliver] to properly offset -/// animating children. -typedef TreeSliverNodesAnimation = ({ - int fromIndex, - int toIndex, - double value, -}); - -/// Used to pass information down to [RenderTreeSliver]. -class TreeSliverNodeParentData extends SliverMultiBoxAdaptorParentData { - /// The depth of the node, used by [RenderTreeSliver] to offset children by - /// by the [TreeSliverIndentationType]. - int depth = 0; -} - -/// The style of indentation for [TreeSliverNode]s in a [TreeSliver], as -/// handled by [RenderTreeSliver]. -/// -/// {@template flutter.rendering.TreeSliverIndentationType} -/// By default, the indentation is handled by [RenderTreeSliver]. Child nodes -/// are offset by the indentation specified by -/// [TreeSliverIndentationType.value] in the cross axis of the viewport. This -/// means the space allotted to the indentation will not be part of the space -/// made available to the widget returned by [TreeSliver.treeNodeBuilder]. -/// -/// Alternatively, the indentation can be implemented in -/// [TreeSliver.treeNodeBuilder], with the depth of the given tree row -/// accessed by [TreeSliverNode.depth]. This allows for more customization in -/// building tree rows, such as filling the indented area with decorations or -/// ink effects. -/// -/// {@tool dartpad} -/// This example shows a highly customized [TreeSliver] configured to -/// [TreeSliverIndentationType.none]. This allows the indentation to be handled -/// by the developer in [TreeSliver.treeNodeBuilder], where a decoration is -/// used to fill the indented space. -/// -/// ** See code in examples/api/lib/widgets/sliver/sliver_tree.1.dart ** -/// {@end-tool} -/// -/// {@endtemplate} -class TreeSliverIndentationType { - const TreeSliverIndentationType._internal(double value) : _value = value; - - /// The number of pixels by which [TreeSliverNode]s will be offset according - /// to their [TreeSliverNode.depth]. - double get value => _value; - final double _value; - - /// The default indentation of child [TreeSliverNode]s in a [TreeSliver]. - /// - /// Child nodes will be offset by 10 pixels for each level in the tree. - static const TreeSliverIndentationType standard = TreeSliverIndentationType._internal(10.0); - - /// Configures no offsetting of child nodes in a [TreeSliver]. - /// - /// Useful if the indentation is implemented in the - /// [TreeSliver.treeNodeBuilder] instead for more customization options. - /// - /// Child nodes will not be offset in the tree. - static const TreeSliverIndentationType none = TreeSliverIndentationType._internal(0.0); - - /// Configures a custom offset for indenting child nodes in a - /// [TreeSliver]. - /// - /// Child nodes will be offset by the provided number of pixels in the tree. - /// The [value] must be a non negative number. - static TreeSliverIndentationType custom(double value) { - assert(value >= 0.0); - return TreeSliverIndentationType._internal(value); - } -} - -// Used during paint to delineate animating portions of the tree. -typedef _PaintSegment = ({int leadingIndex, int trailingIndex}); - -/// A sliver that places multiple [TreeSliverNode]s in a linear array along the -/// main axis, while staggering nodes that are animating into and out of view. -/// -/// The extent of each child node is determined by the [itemExtentBuilder]. -/// -/// See also: -/// -/// * [TreeSliver], the widget that creates and manages this render -/// object. -class RenderTreeSliver extends RenderSliverVariedExtentList { - /// Creates the render object that lays out the [TreeSliverNode]s of a - /// [TreeSliver]. - RenderTreeSliver({ - required super.childManager, - required super.itemExtentBuilder, - required Map activeAnimations, - required double indentation, - }) : _activeAnimations = activeAnimations, - _indentation = indentation; - - // TODO(Piinks): There are some opportunities to cache even further as far as - // extents and layout offsets when using itemExtentBuilder from the super - // class as we do here. I want to yak shave that in a separate change. - - /// The currently active [TreeSliverNode] animations. - /// - /// Since the index of animating nodes can change at any time, the unique key - /// is used to track an animation of nodes across frames. - Map get activeAnimations => _activeAnimations; - Map _activeAnimations; - set activeAnimations(Map value) { - if (_activeAnimations == value) { - return; - } - _activeAnimations = value; - markNeedsLayout(); - } - - /// The number of pixels by which child nodes will be offset in the cross axis - /// based on their [TreeSliverNodeParentData.depth]. - /// - /// If zero, can alternatively offset children in - /// [TreeSliver.treeNodeBuilder] for more options to customize the - /// indented space. - double get indentation => _indentation; - double _indentation; - set indentation(double value) { - if (_indentation == value) { - return; - } - assert(indentation >= 0.0); - _indentation = value; - markNeedsLayout(); - } - - // Maps the index of parents to the animation key of their children. - final Map _animationLeadingIndices = {}; - // Maps the key of child node animations to the fixed distance they are - // traversing during the animation. Determined at the start of the animation. - final Map _animationOffsets = {}; - void _updateAnimationCache() { - _animationLeadingIndices.clear(); - _activeAnimations.forEach((UniqueKey key, TreeSliverNodesAnimation animation) { - _animationLeadingIndices[animation.fromIndex - 1] = key; - }); - // Remove any stored offsets or clip layers that are no longer actively - // animating. - _animationOffsets.removeWhere((UniqueKey key, _) => !_activeAnimations.keys.contains(key)); - _clipHandles.removeWhere((UniqueKey key, LayerHandle handle) { - if (!_activeAnimations.keys.contains(key)) { - handle.layer = null; - return true; - } - return false; - }); - } - - @override - void setupParentData(RenderBox child) { - if (child.parentData is! TreeSliverNodeParentData) { - child.parentData = TreeSliverNodeParentData(); - } - } - - @override - void dispose() { - _clipHandles.removeWhere((UniqueKey key, LayerHandle handle) { - handle.layer = null; - return true; - }); - super.dispose(); - } - - // TODO(Piinks): This should be made a public getter on the super class. - // Multiple subclasses are making use of it now, yak shave that refactor - // separately. - late SliverLayoutDimensions _currentLayoutDimensions; - - @override - void performLayout() { - assert( - constraints.axisDirection == AxisDirection.down, - 'TreeSliver is only supported in Viewports with an AxisDirection.down. ' - 'The current axis direction is: ${constraints.axisDirection}.', - ); - _updateAnimationCache(); - _currentLayoutDimensions = SliverLayoutDimensions( - scrollOffset: constraints.scrollOffset, - precedingScrollExtent: constraints.precedingScrollExtent, - viewportMainAxisExtent: constraints.viewportMainAxisExtent, - crossAxisExtent: constraints.crossAxisExtent, - ); - super.performLayout(); - } - - @override - int getMinChildIndexForScrollOffset(double scrollOffset, double itemExtent) { - // itemExtent is deprecated in the super class, we ignore it because we use - // the builder anyways. - return _getChildIndexForScrollOffset(scrollOffset); - } - - @override - int getMaxChildIndexForScrollOffset(double scrollOffset, double itemExtent) { - // itemExtent is deprecated in the super class, we ignore it because we use - // the builder anyways. - return _getChildIndexForScrollOffset(scrollOffset); - } - - int _getChildIndexForScrollOffset(double scrollOffset) { - if (scrollOffset == 0.0) { - return 0; - } - double position = 0.0; - int index = 0; - double totalAnimationOffset = 0.0; - double? itemExtent; - final int? childCount = childManager.estimatedChildCount; - while (position < scrollOffset) { - if (childCount != null && index > childCount - 1) { - break; - } - - itemExtent = itemExtentBuilder(index, _currentLayoutDimensions); - if (itemExtent == null) { - break; - } - if (_animationLeadingIndices.keys.contains(index)) { - final UniqueKey animationKey = _animationLeadingIndices[index]!; - if (_animationOffsets[animationKey] == null) { - // We have not computed the distance this block is traversing over the - // lifetime of the animation. - _computeAnimationOffsetFor(animationKey, position); - } - // We add the offset accounting for the animation value. - totalAnimationOffset += _animationOffsets[animationKey]! * (1 - _activeAnimations[animationKey]!.value); - } - position += itemExtent - totalAnimationOffset; - ++index; - } - return index - 1; - } - - void _computeAnimationOffsetFor(UniqueKey key, double position) { - assert(_activeAnimations[key] != null); - final double targetPosition = constraints.scrollOffset + constraints.remainingCacheExtent; - double currentPosition = position; - final int startingIndex = _activeAnimations[key]!.fromIndex; - final int lastIndex = _activeAnimations[key]!.toIndex; - int currentIndex = startingIndex; - double totalAnimatingOffset = 0.0; - // We animate only a portion of children that would be visible/in the cache - // extent, unless all children would fit on the screen. - while (currentIndex <= lastIndex && currentPosition < targetPosition) { - final double itemExtent = itemExtentBuilder(currentIndex, _currentLayoutDimensions)!; - totalAnimatingOffset += itemExtent; - currentPosition += itemExtent; - currentIndex++; - } - // For the life of this animation, which affects all children following - // startingIndex (regardless of if they are a child of the triggering - // parent), they will be offset by totalAnimatingOffset * the - // animation value. This is because even though more children can be - // scrolled into view, the same distance must be maintained for a smooth - // animation. - _animationOffsets[key] = totalAnimatingOffset; - } - - @override - double indexToLayoutOffset(double itemExtent, int index) { - // itemExtent is deprecated in the super class, we ignore it because we use - // the builder anyways. - double position = 0.0; - int currentIndex = 0; - double totalAnimationOffset = 0.0; - double? itemExtent; - final int? childCount = childManager.estimatedChildCount; - while (currentIndex < index) { - if (childCount != null && currentIndex > childCount - 1) { - break; - } - - itemExtent = itemExtentBuilder(currentIndex, _currentLayoutDimensions); - if (itemExtent == null) { - break; - } - if (_animationLeadingIndices.keys.contains(currentIndex)) { - final UniqueKey animationKey = _animationLeadingIndices[currentIndex]!; - assert(_animationOffsets[animationKey] != null); - // We add the offset accounting for the animation value. - totalAnimationOffset += _animationOffsets[animationKey]! * (1 - _activeAnimations[animationKey]!.value); - } - position += itemExtent; - currentIndex++; - } - return position - totalAnimationOffset; - } - - final Map> _clipHandles = >{}; - - @override - void paint(PaintingContext context, Offset offset) { - if (firstChild == null) { - return; - } - - RenderBox? nextChild = firstChild; - void paintUpTo( - int index, - RenderBox? startWith, - PaintingContext context, - Offset offset, - ) { - RenderBox? child = startWith; - while (child != null && indexOf(child) <= index) { - final double mainAxisDelta = childMainAxisPosition(child); - final TreeSliverNodeParentData parentData = child.parentData! as TreeSliverNodeParentData; - final Offset childOffset = Offset( - parentData.depth * indentation, - parentData.layoutOffset!, - ); - - // If the child's visible interval (mainAxisDelta, mainAxisDelta + paintExtentOf(child)) - // does not intersect the paint extent interval (0, constraints.remainingPaintExtent), it's hidden. - if (mainAxisDelta < constraints.remainingPaintExtent && mainAxisDelta + paintExtentOf(child) > 0) { - context.paintChild(child, childOffset); - } - child = childAfter(child); - } - nextChild = child; - } - if (_animationLeadingIndices.isEmpty) { - // There are no animations running. - paintUpTo(indexOf(lastChild!), firstChild, context, offset); - return; - } - - // We are animating. - // Separate animating segments to clip for any overlap. - int leadingIndex = indexOf(firstChild!); - final List animationIndices = _animationLeadingIndices.keys.toList()..sort(); - final List<_PaintSegment> paintSegments = <_PaintSegment>[]; - while (animationIndices.isNotEmpty) { - final int trailingIndex = animationIndices.removeAt(0); - paintSegments.add((leadingIndex: leadingIndex, trailingIndex: trailingIndex)); - leadingIndex = trailingIndex + 1; - } - paintSegments.add((leadingIndex: leadingIndex, trailingIndex: indexOf(lastChild!))); - - // Paint, clipping for all but the first segment. - paintUpTo(paintSegments.removeAt(0).trailingIndex, nextChild, context, offset); - // Paint the rest with clip layers. - while (paintSegments.isNotEmpty) { - final _PaintSegment segment = paintSegments.removeAt(0); - - // Rect is calculated by the trailing edge of the parent (preceding - // leadingIndex), and the trailing edge of the trailing index. We cannot - // rely on the leading edge of the leading index, because it is currently - // moving. - final int parentIndex = math.max(segment.leadingIndex - 1, 0); - final double leadingOffset = indexToLayoutOffset(0.0, parentIndex) - + (parentIndex == 0 ? 0.0 : itemExtentBuilder(parentIndex, _currentLayoutDimensions)!); - final double trailingOffset = indexToLayoutOffset(0.0, segment.trailingIndex) - + itemExtentBuilder(segment.trailingIndex, _currentLayoutDimensions)!; - final Rect rect = Rect.fromPoints( - Offset(0.0, leadingOffset), - Offset(constraints.crossAxisExtent, trailingOffset), - ); - // We use the same animation key to keep track of the clip layer, unless - // this is the odd man out segment. - final UniqueKey key = _animationLeadingIndices[parentIndex]!; - _clipHandles[key] ??= LayerHandle(); - _clipHandles[key]!.layer = context.pushClipRect( - needsCompositing, - offset, - rect, - (PaintingContext context, Offset offset) { - paintUpTo(segment.trailingIndex, nextChild, context, offset); - }, - oldLayer: _clipHandles[key]!.layer, - ); - } - } -} diff --git a/packages/flutter/lib/src/widgets/sliver_tree.dart b/packages/flutter/lib/src/widgets/sliver_tree.dart deleted file mode 100644 index 9232a23890..0000000000 --- a/packages/flutter/lib/src/widgets/sliver_tree.dart +++ /dev/null @@ -1,997 +0,0 @@ -// 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/rendering.dart'; - -import 'basic.dart'; -import 'framework.dart'; -import 'gesture_detector.dart'; -import 'icon.dart'; -import 'icon_data.dart'; -import 'implicit_animations.dart'; -import 'scroll_delegate.dart'; -import 'sliver.dart'; -import 'text.dart'; -import 'ticker_provider.dart'; - -const double _kDefaultRowExtent = 40.0; - -/// A data structure for configuring children of a [TreeSliver]. -/// -/// A [TreeSliverNode.content] can be of any type [T], but must correspond with -/// the same type of the [TreeSliver]. -/// -/// The values returned by [depth], [parent] and [isExpanded] getters are -/// managed by the [TreeSliver]'s state. -class TreeSliverNode { - /// Creates a [TreeSliverNode] instance for use in a [TreeSliver]. - TreeSliverNode( - T content, { - List>? children, - bool expanded = false, - }) : _expanded = children != null && children.isNotEmpty && expanded, - _content = content, - _children = children ?? >[]; - - /// The subject matter of the node. - /// - /// Must correspond with the type of [TreeSliver]. - T get content => _content; - final T _content; - - /// Other [TreeSliverNode]s that this node will be [parent] to. - /// - /// Modifying the children of nodes in a [TreeSliver] will cause the tree to be - /// rebuilt so that newly added active nodes are reflected in the tree. - List> get children => _children; - final List> _children; - - /// Whether or not this node is expanded in the tree. - /// - /// Cannot be expanded if there are no children. - bool get isExpanded => _expanded; - bool _expanded; - - /// The number of parent nodes between this node and the root of the tree. - int? get depth => _depth; - int? _depth; - - /// The parent [TreeSliverNode] of this node. - TreeSliverNode? get parent => _parent; - TreeSliverNode? _parent; - - @override - String toString() { - return 'TreeSliverNode: $content, depth: ${depth == 0 ? 'root' : depth}, ' - '${children.isEmpty ? 'leaf' : 'parent, expanded: $isExpanded'}'; - } -} - -/// Signature for a function that creates a [Widget] to represent the given -/// [TreeSliverNode] in the [TreeSliver]. -/// -/// Used by [TreeSliver.treeNodeBuilder] to build rows on demand for the -/// tree. -typedef TreeSliverNodeBuilder = Widget Function( - BuildContext context, - TreeSliverNode node, - AnimationStyle animationStyle, -); - -/// Signature for a function that returns an extent for the given -/// [TreeSliverNode] in the [TreeSliver]. -/// -/// Used by [TreeSliver.treeRowExtentBuilder] to size rows on demand in the -/// tree. The provided [SliverLayoutDimensions] provide information about the -/// current scroll state and [Viewport] dimensions. -/// -/// See also: -/// -/// * [SliverVariedExtentList], which uses a similar item extent builder for -/// dynamic child sizing in the list. -typedef TreeSliverRowExtentBuilder = double Function( - TreeSliverNode node, - SliverLayoutDimensions dimensions, -); - -/// Signature for a function that is called when a [TreeSliverNode] is toggled, -/// changing its expanded state. -/// -/// See also: -/// -/// * [TreeSliver.onNodeToggle], for controlling node expansion -/// programmatically. -typedef TreeSliverNodeCallback = void Function(TreeSliverNode node); - -/// A mixin for classes implementing a tree structure as expected by a -/// [TreeSliverController]. -/// -/// Used by [TreeSliver] to implement an interface for the -/// [TreeSliverController]. -/// -/// This allows the [TreeSliverController] to be used in other widgets that -/// implement this interface. -/// -/// The type [T] correlates to the type of [TreeSliver] and [TreeSliverNode], -/// representing the type of [TreeSliverNode.content]. -mixin TreeSliverStateMixin { - /// Returns whether or not the given [TreeSliverNode] is expanded. - bool isExpanded(TreeSliverNode node); - - /// Returns whether or not the given [TreeSliverNode] is enclosed within its - /// parent [TreeSliverNode]. - /// - /// If the [TreeSliverNode.parent] [isExpanded] (and all its parents are - /// expanded), or this is a root node, the given node is active and this - /// method will return true. This does not reflect whether or not the node is - /// visible in the [Viewport]. - bool isActive(TreeSliverNode node); - - /// Switches the given [TreeSliverNode]s expanded state. - /// - /// May trigger an animation to reveal or hide the node's children based on - /// the [TreeSliver.toggleAnimationStyle]. - /// - /// If the node does not have any children, nothing will happen. - void toggleNode(TreeSliverNode node); - - /// Closes all parent [TreeSliverNode]s in the tree. - void collapseAll(); - - /// Expands all parent [TreeSliverNode]s in the tree. - void expandAll(); - - /// Retrieves the [TreeSliverNode] containing the associated content, if it - /// exists. - /// - /// If no node exists, this will return null. This does not reflect whether - /// or not a node [isActive], or if it is visible in the viewport. - TreeSliverNode? getNodeFor(T content); - - /// Returns the current row index of the given [TreeSliverNode]. - /// - /// If the node is not currently active in the tree, meaning its parent is - /// collapsed, this will return null. - int? getActiveIndexFor(TreeSliverNode node); -} - -/// Enables control over the [TreeSliverNode]s of a [TreeSliver]. -/// -/// It can be useful to expand or collapse nodes of the tree -/// programmatically, for example to reconfigure an existing node -/// based on a system event. To do so, create a [TreeSliver] -/// with a [TreeSliverController] that's owned by a stateful widget -/// or look up the tree's automatically created [TreeSliverController] -/// with [TreeSliverController.of] -/// -/// The controller's methods to expand or collapse nodes cause the -/// the [TreeSliver] to rebuild, so they may not be called from -/// a build method. -class TreeSliverController { - /// Create a controller to be used with [TreeSliver.controller]. - TreeSliverController(); - - TreeSliverStateMixin? _state; - - /// Whether the given [TreeSliverNode] built with this controller is in an - /// expanded state. - /// - /// See also: - /// - /// * [expandNode], which expands a given [TreeSliverNode]. - /// * [collapseNode], which collapses a given [TreeSliverNode]. - /// * [TreeSliver.controller] to create a TreeSliver with a controller. - bool isExpanded(TreeSliverNode node) { - assert(_state != null); - return _state!.isExpanded(node); - } - - /// Whether or not the given [TreeSliverNode] is enclosed within its parent - /// [TreeSliverNode]. - /// - /// If the [TreeSliverNode.parent] [isExpanded], or this is a root node, the - /// given node is active and this method will return true. This does not - /// reflect whether or not the node is visible in the [Viewport]. - bool isActive(TreeSliverNode node) { - assert(_state != null); - return _state!.isActive(node); - } - - /// Returns the [TreeSliverNode] containing the associated content, if it - /// exists. - /// - /// If no node exists, this will return null. This does not reflect whether - /// or not a node [isActive], or if it is currently visible in the viewport. - TreeSliverNode? getNodeFor(Object? content) { - assert(_state != null); - return _state!.getNodeFor(content); - } - - /// Switches the given [TreeSliverNode]s expanded state. - /// - /// May trigger an animation to reveal or hide the node's children based on - /// the [TreeSliver.toggleAnimationStyle]. - /// - /// If the node does not have any children, nothing will happen. - void toggleNode(TreeSliverNode node) { - assert(_state != null); - return _state!.toggleNode(node); - } - - /// Expands the [TreeSliverNode] that was built with this controller. - /// - /// If the node is already in the expanded state (see [isExpanded]), calling - /// this method has no effect. - /// - /// Calling this method may cause the [TreeSliver] to rebuild, so it may - /// not be called from a build method. - /// - /// Calling this method will trigger the [TreeSliver.onNodeToggle] - /// callback. - /// - /// See also: - /// - /// * [collapseNode], which collapses the [TreeSliverNode]. - /// * [isExpanded] to check whether the tile is expanded. - /// * [TreeSliver.controller] to create a TreeSliver with a controller. - void expandNode(TreeSliverNode node) { - assert(_state != null); - if (!node.isExpanded) { - _state!.toggleNode(node); - } - } - - /// Expands all parent [TreeSliverNode]s in the tree. - void expandAll() { - assert(_state != null); - _state!.expandAll(); - } - - /// Closes all parent [TreeSliverNode]s in the tree. - void collapseAll() { - assert(_state != null); - _state!.collapseAll(); - } - - /// Collapses the [TreeSliverNode] that was built with this controller. - /// - /// If the node is already in the collapsed state (see [isExpanded]), calling - /// this method has no effect. - /// - /// Calling this method may cause the [TreeSliver] to rebuild, so it may - /// not be called from a build method. - /// - /// Calling this method will trigger the [TreeSliver.onNodeToggle] - /// callback. - /// - /// See also: - /// - /// * [expandNode], which expands the tile. - /// * [isExpanded] to check whether the tile is expanded. - /// * [TreeSliver.controller] to create a TreeSliver with a controller. - void collapseNode(TreeSliverNode node) { - assert(_state != null); - if (node.isExpanded) { - _state!.toggleNode(node); - } - } - - /// Returns the current row index of the given [TreeSliverNode]. - /// - /// If the node is not currently active in the tree, meaning its parent is - /// collapsed, this will return null. - int? getActiveIndexFor(TreeSliverNode node) { - assert(_state != null); - return _state!.getActiveIndexFor(node); - } - - /// Finds the [TreeSliverController] for the closest [TreeSliver] instance - /// that encloses the given context. - /// - /// If no [TreeSliver] encloses the given context, calling this - /// method will cause an assert in debug mode, and throw an - /// exception in release mode. - /// - /// To return null if there is no [TreeSliver] use [maybeOf] instead. - /// - /// Typical usage of the [TreeSliverController.of] function is to call it - /// from within the `build` method of a descendant of a [TreeSliver]. - /// - /// When the [TreeSliver] is actually created in the same `build` - /// function as the callback that refers to the controller, then the - /// `context` argument to the `build` function can't be used to find - /// the [TreeSliverController] (since it's "above" the widget - /// being returned in the widget tree). In cases like that you can - /// add a [Builder] widget, which provides a new scope with a - /// [BuildContext] that is "under" the [TreeSliver]. - static TreeSliverController of(BuildContext context) { - final _TreeSliverState? result = - context.findAncestorStateOfType<_TreeSliverState>(); - if (result != null) { - return result.controller; - } - throw FlutterError.fromParts([ - ErrorSummary( - 'TreeController.of() called with a context that does not contain a ' - 'TreeSliver.', - ), - ErrorDescription( - 'No TreeSliver ancestor could be found starting from the context that ' - 'was passed to TreeController.of(). ' - 'This usually happens when the context provided is from the same ' - 'StatefulWidget as that whose build function actually creates the ' - 'TreeSliver widget being sought.', - ), - ErrorHint( - 'There are several ways to avoid this problem. The simplest is to use ' - 'a Builder to get a context that is "under" the TreeSliver.', - ), - ErrorHint( - 'A more efficient solution is to split your build function into ' - 'several widgets. This introduces a new context from which you can ' - 'obtain the TreeSliver. In this solution, you would have an outer ' - 'widget that creates the TreeSliver populated by instances of your new ' - 'inner widgets, and then in these inner widgets you would use ' - 'TreeController.of().', - ), - context.describeElement('The context used was'), - ]); - } - - /// Finds the [TreeSliver] from the closest instance of this class that - /// encloses the given context and returns its [TreeSliverController]. - /// - /// If no [TreeSliver] encloses the given context then return null. - /// To throw an exception instead, use [of] instead of this function. - /// - /// See also: - /// - /// * [of], a similar function to this one that throws if no [TreeSliver] - /// encloses the given context. Also includes some sample code in its - /// documentation. - static TreeSliverController? maybeOf(BuildContext context) { - return context.findAncestorStateOfType<_TreeSliverState>()?.controller; - } -} - -int _kDefaultSemanticIndexCallback(Widget _, int localIndex) => localIndex; - -/// A widget that displays [TreeSliverNode]s that expand and collapse in a -/// vertically and horizontally scrolling [Viewport]. -/// -/// The type [T] correlates to the type of [TreeSliver] and [TreeSliverNode], -/// representing the type of [TreeSliverNode.content]. -/// -/// The rows of the tree are laid out on demand by the [Viewport]'s render -/// object, using [TreeSliver.treeNodeBuilder]. This will only be called for the -/// nodes that are visible, or within the [Viewport.cacheExtent]. -/// -/// The [TreeSliver.treeNodeBuilder] returns the [Widget] that represents the -/// given [TreeSliverNode]. -/// -/// The [TreeSliver.treeRowExtentBuilder] returns a double representing the -/// extent of a given node in the main axis. -/// -/// Providing a [TreeSliverController] will enable querying and controlling the -/// state of nodes in the tree. -/// -/// A [TreeSliver] only supports a vertical axis direction of -/// [AxisDirection.down] and a horizontal axis direction of -/// [AxisDirection.right]. -/// -///{@tool dartpad} -/// This example uses a [TreeSliver] to display nodes, highlighting nodes as -/// they are selected. -/// -/// ** See code in examples/api/lib/widgets/sliver/sliver_tree.0.dart ** -/// {@end-tool} -/// -/// {@tool dartpad} -/// This example shows a highly customized [TreeSliver] configured to -/// [TreeSliverIndentationType.none]. This allows the indentation to be handled -/// by the developer in [TreeSliver.treeNodeBuilder], where a decoration is -/// used to fill the indented space. -/// -/// ** See code in examples/api/lib/widgets/sliver/sliver_tree.1.dart ** -/// {@end-tool} -class TreeSliver extends StatefulWidget { - /// Creates an instance of a [TreeSliver] for displaying [TreeSliverNode]s - /// that animate expanding and collapsing of nodes. - const TreeSliver({ - super.key, - required this.tree, - this.treeNodeBuilder = TreeSliver.defaultTreeNodeBuilder, - this.treeRowExtentBuilder = TreeSliver.defaultTreeRowExtentBuilder, - this.controller, - this.onNodeToggle, - this.toggleAnimationStyle, - this.indentation = TreeSliverIndentationType.standard, - this.addAutomaticKeepAlives = true, - this.addRepaintBoundaries = true, - this.addSemanticIndexes = true, - this.semanticIndexCallback = _kDefaultSemanticIndexCallback, - this.semanticIndexOffset = 0, - this.findChildIndexCallback, - }); - - /// The list of [TreeSliverNode]s that may be displayed in the [TreeSliver]. - /// - /// Beyond root nodes, whether or not a given [TreeSliverNode] is displayed - /// depends on the [TreeSliverNode.isExpanded] value of its parent. The - /// [TreeSliver] will set the [TreeSliverNode.parent] and - /// [TreeSliverNode.depth] as nodes are built on demand to ensure the - /// integrity of the tree. - final List> tree; - - /// Called to build and entry of the [TreeSliver] for the given node. - /// - /// By default, if this is unset, the [TreeSliver.defaultTreeNodeBuilder] - /// is used. - final TreeSliverNodeBuilder treeNodeBuilder; - - /// Called to calculate the extent of the widget built for the given - /// [TreeSliverNode]. - /// - /// By default, if this is unset, the - /// [TreeSliver.defaultTreeRowExtentBuilder] is used. - /// - /// See also: - /// - /// * [SliverVariedExtentList.itemExtentBuilder], a very similar method that - /// allows users to dynamically compute extents on demand. - final TreeSliverRowExtentBuilder treeRowExtentBuilder; - - /// If provided, the controller can be used to expand and collapse - /// [TreeSliverNode]s, or lookup information about the current state of the - /// [TreeSliver]. - final TreeSliverController? controller; - - /// Called when a [TreeSliverNode] expands or collapses. - /// - /// This will not be called if a [TreeSliverNode] does not have any children. - final TreeSliverNodeCallback? onNodeToggle; - - /// The default [AnimationStyle] for expanding and collapsing nodes in the - /// [TreeSliver]. - /// - /// The default [AnimationStyle.duration] uses - /// [TreeSliver.defaultAnimationDuration], which is 150 milliseconds. - /// - /// The default [AnimationStyle.curve] uses [TreeSliver.defaultAnimationCurve], - /// which is [Curves.linear]. - /// - /// To disable the tree animation, use [AnimationStyle.noAnimation]. - final AnimationStyle? toggleAnimationStyle; - - /// The number of pixels children will be offset by in the cross axis based on - /// their [TreeSliverNode.depth]. - /// - /// {@macro flutter.rendering.TreeSliverIndentationType} - final TreeSliverIndentationType indentation; - - /// {@macro flutter.widgets.SliverChildBuilderDelegate.addAutomaticKeepAlives} - final bool addAutomaticKeepAlives; - - /// {@macro flutter.widgets.SliverChildBuilderDelegate.addRepaintBoundaries} - final bool addRepaintBoundaries; - - /// {@macro flutter.widgets.SliverChildBuilderDelegate.addSemanticIndexes} - final bool addSemanticIndexes; - - /// {@macro flutter.widgets.SliverChildBuilderDelegate.semanticIndexCallback} - final SemanticIndexCallback semanticIndexCallback; - - /// {@macro flutter.widgets.SliverChildBuilderDelegate.semanticIndexOffset} - final int semanticIndexOffset; - - /// {@macro flutter.widgets.SliverChildBuilderDelegate.findChildIndexCallback} - final int? Function(Key)? findChildIndexCallback; - - /// The default [AnimationStyle] used for node expand and collapse animations, - /// when one has not been provided in [toggleAnimationStyle]. - static AnimationStyle defaultToggleAnimationStyle = AnimationStyle( - curve: defaultAnimationCurve, - duration: defaultAnimationDuration, - ); - - /// A default of [Curves.linear], which is used in the tree's expanding and - /// collapsing node animation. - static const Curve defaultAnimationCurve = Curves.linear; - - /// A default [Duration] of 150 milliseconds, which is used in the tree's - /// expanding and collapsing node animation. - static const Duration defaultAnimationDuration = Duration(milliseconds: 150); - - /// A wrapper method for triggering the expansion or collapse of a - /// [TreeSliverNode]. - /// - /// Used as part of [TreeSliver.defaultTreeNodeBuilder] to wrap the leading - /// icon of parent [TreeSliverNode]s such that tapping on it triggers the - /// animation. - /// - /// If defining your own [TreeSliver.treeNodeBuilder], this method can be used - /// to wrap any part, or all, of the returned widget in order to trigger the - /// change in state for the node. - static Widget wrapChildToToggleNode({ - required TreeSliverNode node, - required Widget child, - }) { - return Builder(builder: (BuildContext context) { - return GestureDetector( - onTap: () { - TreeSliverController.of(context).toggleNode(node); - }, - child: child, - ); - }); - } - - /// Returns the fixed default extent for rows in the tree, which is 40 pixels. - /// - /// Used by [TreeSliver.treeRowExtentBuilder]. - static double defaultTreeRowExtentBuilder( - TreeSliverNode node, - SliverLayoutDimensions dimensions, - ) { - return _kDefaultRowExtent; - } - - /// Returns the default tree row for a given [TreeSliverNode]. - /// - /// Used by [TreeSliver.treeNodeBuilder]. - /// - /// This will return a [Row] containing the [toString] of - /// [TreeSliverNode.content]. If the [TreeSliverNode] is a parent of - /// additional nodes, a arrow icon will precede the content, and will trigger - /// an expand and collapse animation when tapped. - static Widget defaultTreeNodeBuilder( - BuildContext context, - TreeSliverNode node, - AnimationStyle toggleAnimationStyle - ) { - final Duration animationDuration = toggleAnimationStyle.duration - ?? TreeSliver.defaultAnimationDuration; - final Curve animationCurve = toggleAnimationStyle.curve - ?? TreeSliver.defaultAnimationCurve; - final int index = TreeSliverController.of(context).getActiveIndexFor(node)!; - return Padding( - padding: const EdgeInsets.all(8.0), - child: Row(children: [ - // Icon for parent nodes - TreeSliver.wrapChildToToggleNode( - node: node, - child: SizedBox.square( - dimension: 30.0, - child: node.children.isNotEmpty - ? AnimatedRotation( - key: ValueKey(index), - turns: node.isExpanded ? 0.25 : 0.0, - duration: animationDuration, - curve: animationCurve, - // Renders a unicode right-facing arrow. > - child: const Icon(IconData(0x25BA), size: 14), - ) - : null, - ), - ), - // Spacer - const SizedBox(width: 8.0), - // Content - Text(node.content.toString()), - ]), - ); - } - - @override - State> createState() => _TreeSliverState(); -} - -// Used in _SliverTreeState for code simplicity. -typedef _AnimationRecord = ({ - AnimationController controller, - CurvedAnimation animation, - UniqueKey key, -}); - -class _TreeSliverState extends State> with TickerProviderStateMixin, TreeSliverStateMixin { - TreeSliverController get controller => _treeController!; - TreeSliverController? _treeController; - - final List> _activeNodes = >[]; - bool _shouldUnpackNode(TreeSliverNode node) { - if (node.children.isEmpty) { - // No children to unpack. - return false; - } - if (_currentAnimationForParent[node] != null) { - // Whether expanding or collapsing, the child nodes are still active, so - // unpack. - return true; - } - // If we are not animating, respect node.isExpanded. - return node.isExpanded; - } - void _unpackActiveNodes({ - int depth = 0, - List>? nodes, - TreeSliverNode? parent, - }) { - if (nodes == null) { - _activeNodes.clear(); - nodes = widget.tree; - } - for (final TreeSliverNode node in nodes) { - node._depth = depth; - node._parent = parent; - _activeNodes.add(node); - if (_shouldUnpackNode(node)) { - _unpackActiveNodes( - depth: depth + 1, - nodes: node.children, - parent: node, - ); - } - } - } - - final Map, _AnimationRecord> _currentAnimationForParent = , _AnimationRecord>{}; - final Map _activeAnimations = {}; - - @override - void initState() { - _unpackActiveNodes(); - assert( - widget.controller?._state == null, - 'The provided TreeSliverController is already associated with another ' - 'TreeSliver. A TreeSliverController can only be associated with one ' - 'TreeSliver.', - ); - _treeController = widget.controller ?? TreeSliverController(); - _treeController!._state = this; - super.initState(); - } - - @override - void didUpdateWidget(TreeSliver oldWidget) { - super.didUpdateWidget(oldWidget); - // Internal or provided, there is always a tree controller. - assert(_treeController != null); - if (oldWidget.controller == null && widget.controller != null) { - // A new tree controller has been provided, update and dispose of the - // internally generated one. - _treeController!._state = null; - _treeController = widget.controller; - _treeController!._state = this; - } else if (oldWidget.controller != null && widget.controller == null) { - // A tree controller had been provided, but was removed. We need to create - // one internally. - assert(oldWidget.controller == _treeController); - oldWidget.controller!._state = null; - _treeController = TreeSliverController(); - _treeController!._state = this; - } else if (oldWidget.controller != widget.controller) { - assert(oldWidget.controller != null); - assert(widget.controller != null); - assert(oldWidget.controller == _treeController); - // The tree is still being provided a controller, but it has changed. Just - // update it. - _treeController!._state = null; - _treeController = widget.controller; - _treeController!._state = this; - } - // Internal or provided, there is always a tree controller. - assert(_treeController != null); - assert(_treeController!._state != null); - _unpackActiveNodes(); - } - - @override - void dispose() { - _treeController!._state = null; - for (final _AnimationRecord record in _currentAnimationForParent.values) { - record.animation.dispose(); - record.controller.dispose(); - } - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return _SliverTree( - itemCount: _activeNodes.length, - activeAnimations: _activeAnimations, - itemBuilder: (BuildContext context, int index) { - final TreeSliverNode node = _activeNodes[index]; - Widget child = widget.treeNodeBuilder( - context, - node, - widget.toggleAnimationStyle ?? TreeSliver.defaultToggleAnimationStyle, - ); - - if (widget.addRepaintBoundaries) { - child = RepaintBoundary(child: child); - } - if (widget.addSemanticIndexes) { - final int? semanticIndex = widget.semanticIndexCallback(child, index); - if (semanticIndex != null) { - child = IndexedSemantics( - index: semanticIndex + widget.semanticIndexOffset, - child: child, - ); - } - } - - return _TreeNodeParentDataWidget( - depth: node.depth!, - child: child, - ); - }, - itemExtentBuilder: (int index, SliverLayoutDimensions dimensions) { - return widget.treeRowExtentBuilder(_activeNodes[index], dimensions); - }, - addAutomaticKeepAlives: widget.addAutomaticKeepAlives, - findChildIndexCallback: widget.findChildIndexCallback, - indentation: widget.indentation.value, - ); - } - - // TreeStateMixin Implementation - - @override - bool isExpanded(TreeSliverNode node) { - return _getNode(node.content, widget.tree)?.isExpanded ?? false; - } - - @override - bool isActive(TreeSliverNode node) => _activeNodes.contains(node); - - @override - TreeSliverNode? getNodeFor(T content) => _getNode(content, widget.tree); - TreeSliverNode? _getNode(T content, List> tree) { - final List> nextDepth = >[]; - for (final TreeSliverNode node in tree) { - if (node.content == content) { - return node; - } - if (node.children.isNotEmpty) { - nextDepth.addAll(node.children); - } - } - if (nextDepth.isNotEmpty) { - return _getNode(content, nextDepth); - } - return null; - } - - @override - int? getActiveIndexFor(TreeSliverNode node) { - if (_activeNodes.contains(node)) { - return _activeNodes.indexOf(node); - } - return null; - } - - @override - void expandAll() { - final List> activeNodesToExpand = >[]; - _expandAll(widget.tree, activeNodesToExpand); - activeNodesToExpand.reversed.forEach(toggleNode); - } - void _expandAll( - List> tree, - List> activeNodesToExpand, - ) { - for (final TreeSliverNode node in tree) { - if (node.children.isNotEmpty) { - // This is a parent node. - // Expand all the children, and their children. - _expandAll(node.children, activeNodesToExpand); - if (!node.isExpanded) { - // The node itself needs to be expanded. - if (_activeNodes.contains(node)) { - // This is an active node in the tree, add to - // the list to toggle once all hidden nodes - // have been handled. - activeNodesToExpand.add(node); - } else { - // This is a hidden node. Update its expanded state. - node._expanded = true; - } - } - } - } - } - - @override - void collapseAll() { - final List> activeNodesToCollapse = >[]; - _collapseAll(widget.tree, activeNodesToCollapse); - activeNodesToCollapse.reversed.forEach(toggleNode); - } - void _collapseAll( - List> tree, - List> activeNodesToCollapse, - ) { - for (final TreeSliverNode node in tree) { - if (node.children.isNotEmpty) { - // This is a parent node. - // Collapse all the children, and their children. - _collapseAll(node.children, activeNodesToCollapse); - if (node.isExpanded) { - // The node itself needs to be collapsed. - if (_activeNodes.contains(node)) { - // This is an active node in the tree, add to - // the list to toggle once all hidden nodes - // have been handled. - activeNodesToCollapse.add(node); - } else { - // This is a hidden node. Update its expanded state. - node._expanded = false; - } - } - } - } - } - - void _updateActiveAnimations() { - // The indexes of various child node animations can change constantly based - // on more nodes being expanded or collapsed. Compile the indexes and their - // animations keys each time we build with an updated active node list. - _activeAnimations.clear(); - for (final TreeSliverNode node in _currentAnimationForParent.keys) { - final _AnimationRecord animationRecord = _currentAnimationForParent[node]!; - final int leadingChildIndex = _activeNodes.indexOf(node) + 1; - final TreeSliverNodesAnimation animatingChildren = ( - fromIndex: leadingChildIndex, - toIndex: leadingChildIndex + node.children.length - 1, - value: animationRecord.animation.value, - ); - _activeAnimations[animationRecord.key] = animatingChildren; - } - } - - @override - void toggleNode(TreeSliverNode node) { - assert(_activeNodes.contains(node)); - if (node.children.isEmpty) { - // No state to change. - return; - } - setState(() { - node._expanded = !node._expanded; - if (widget.onNodeToggle != null) { - widget.onNodeToggle!(node); - } - final AnimationController controller = _currentAnimationForParent[node]?.controller - ?? AnimationController( - value: node._expanded ? 0.0 : 1.0, - vsync: this, - duration: widget.toggleAnimationStyle?.duration - ?? TreeSliver.defaultAnimationDuration, - )..addStatusListener((AnimationStatus status) { - switch (status) { - case AnimationStatus.dismissed: - case AnimationStatus.completed: - _currentAnimationForParent[node]!.controller.dispose(); - _currentAnimationForParent.remove(node); - _updateActiveAnimations(); - case AnimationStatus.forward: - case AnimationStatus.reverse: - } - })..addListener(() { - setState((){ - _updateActiveAnimations(); - }); - }); - - switch (controller.status) { - case AnimationStatus.forward: - case AnimationStatus.reverse: - // We're interrupting an animation already in progress. - controller.stop(); - case AnimationStatus.dismissed: - case AnimationStatus.completed: - } - - final CurvedAnimation newAnimation = CurvedAnimation( - parent: controller, - curve: widget.toggleAnimationStyle?.curve ?? TreeSliver.defaultAnimationCurve, - ); - _currentAnimationForParent[node] = ( - controller: controller, - animation: newAnimation, - // This key helps us keep track of the lifetime of this animation in the - // render object, since the indexes can change at any time. - key: UniqueKey(), - ); - switch (node._expanded) { - case true: - // Expanding - _unpackActiveNodes(); - controller.forward(); - case false: - // Collapsing - controller.reverse().then((_) { - _unpackActiveNodes(); - }); - } - }); - } -} - -class _TreeNodeParentDataWidget extends ParentDataWidget { - const _TreeNodeParentDataWidget({ - required this.depth, - required super.child, - }) : assert(depth >= 0); - - final int depth; - - @override - void applyParentData(RenderObject renderObject) { - final TreeSliverNodeParentData parentData = renderObject.parentData! as TreeSliverNodeParentData; - bool needsLayout = false; - - if (parentData.depth != depth) { - assert(depth >= 0); - parentData.depth = depth; - needsLayout = true; - } - - if (needsLayout) { - renderObject.parent?.markNeedsLayout(); - } - } - - @override - Type get debugTypicalAncestorWidgetClass => _SliverTree; - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(IntProperty('depth', depth)); - } -} - -class _SliverTree extends SliverVariedExtentList { - _SliverTree({ - required NullableIndexedWidgetBuilder itemBuilder, - required super.itemExtentBuilder, - required this.activeAnimations, - required this.indentation, - ChildIndexGetter? findChildIndexCallback, - required int itemCount, - bool addAutomaticKeepAlives = true, - }) : super(delegate: SliverChildBuilderDelegate( - itemBuilder, - findChildIndexCallback: findChildIndexCallback, - childCount: itemCount, - addAutomaticKeepAlives: addAutomaticKeepAlives, - addRepaintBoundaries: false, // Added in the _SliverTreeState - addSemanticIndexes: false, // Added in the _SliverTreeState - )); - - final Map activeAnimations; - final double indentation; - - @override - RenderTreeSliver createRenderObject(BuildContext context) { - final SliverMultiBoxAdaptorElement element = context as SliverMultiBoxAdaptorElement; - return RenderTreeSliver( - itemExtentBuilder: itemExtentBuilder, - activeAnimations: activeAnimations, - indentation: indentation, - childManager: element, - ); - } - - @override - void updateRenderObject(BuildContext context, RenderTreeSliver renderObject) { - renderObject - ..itemExtentBuilder = itemExtentBuilder - ..activeAnimations = activeAnimations - ..indentation = indentation; - } -} diff --git a/packages/flutter/lib/widgets.dart b/packages/flutter/lib/widgets.dart index bcee293968..a85573de44 100644 --- a/packages/flutter/lib/widgets.dart +++ b/packages/flutter/lib/widgets.dart @@ -137,7 +137,6 @@ export 'src/widgets/sliver_fill.dart'; export 'src/widgets/sliver_layout_builder.dart'; export 'src/widgets/sliver_persistent_header.dart'; export 'src/widgets/sliver_prototype_extent_list.dart'; -export 'src/widgets/sliver_tree.dart'; export 'src/widgets/slotted_render_object_widget.dart'; export 'src/widgets/snapshot_widget.dart'; export 'src/widgets/spacer.dart'; diff --git a/packages/flutter/test/rendering/sliver_tree_test.dart b/packages/flutter/test/rendering/sliver_tree_test.dart deleted file mode 100644 index a38981b42e..0000000000 --- a/packages/flutter/test/rendering/sliver_tree_test.dart +++ /dev/null @@ -1,860 +0,0 @@ -// 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> _setUpNodes() { - return >[ - TreeSliverNode('First'), - TreeSliverNode( - 'Second', - children: >[ - TreeSliverNode( - 'alpha', - children: >[ - TreeSliverNode('uno'), - TreeSliverNode('dos'), - TreeSliverNode('tres'), - ], - ), - TreeSliverNode('beta'), - TreeSliverNode('kappa'), - ], - ), - TreeSliverNode( - 'Third', - expanded: true, - children: >[ - TreeSliverNode('gamma'), - TreeSliverNode('delta'), - TreeSliverNode('epsilon'), - ], - ), - TreeSliverNode('Fourth'), - ]; -} - -List> treeNodes = _setUpNodes(); - -void main() { - testWidgets('asserts proper axis directions', (WidgetTester tester) async { - final List exceptions = []; - 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: [ - TreeSliver(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: [ - TreeSliver(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: [ - TreeSliver(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 treeSliver = TreeSliver( - tree: treeNodes, - ); - await tester.pumpWidget(MaterialApp( - home: CustomScrollView( - slivers: [ treeSliver ], - ), - )); - await tester.pump(); - expect( - find.byType(TreeSliver), - 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(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( - tree: treeNodes, - indentation: TreeSliverIndentationType.none, - ); - await tester.pumpWidget(MaterialApp( - home: CustomScrollView( - slivers: [ treeSliver ], - ), - )); - await tester.pump(); - expect( - find.byType(TreeSliver), - 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( - tree: treeNodes, - indentation: TreeSliverIndentationType.custom(50.0), - ); - await tester.pumpWidget(MaterialApp( - home: CustomScrollView( - slivers: [ treeSliver ], - ), - )); - await tester.pump(); - expect( - find.byType(TreeSliver), - 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(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( - tree: treeNodes, - treeRowExtentBuilder: (_, __) => 100, - ); - await tester.pumpWidget(MaterialApp( - home: CustomScrollView( - slivers: [ treeSliver ], - ), - )); - await tester.pump(); - expect( - find.byType(TreeSliver), - 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(46.0, 326.0, 286.0, 374.0), - ); - expect(find.text('delta'), findsOneWidget); - expect( - tester.getRect(find.text('delta')), - const Rect.fromLTRB(46.0, 426.0, 286.0, 474.0), - ); - expect(find.text('epsilon'), findsOneWidget); - expect( - tester.getRect(find.text('epsilon')), - const Rect.fromLTRB(46.0, 526.0, 382.0, 574.0), - ); - expect(find.text('Fourth'), findsNothing); - }); - - testWidgets('Animating node segment', (WidgetTester tester) async { - treeNodes = _setUpNodes(); - TreeSliver treeSliver = TreeSliver(tree: treeNodes); - await tester.pumpWidget(MaterialApp( - home: CustomScrollView( - slivers: [ treeSliver ], - ), - )); - await tester.pump(); - expect( - find.byType(TreeSliver), - 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), - 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(46.0, 8.0, 238.0, 32.0), - ); - expect(find.text('kappa'), findsOneWidget); - expect( - tester.getRect(find.text('kappa')), - const Rect.fromLTRB(46.0, 48.0, 286.0, 72.0), - ); - // Progress the animation. - await tester.pump(const Duration(milliseconds: 50)); - expect( - find.byType(TreeSliver), - 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), - 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(46.0, 88.0, 286.0, 112.0), - ); - expect(find.text('beta'), findsOneWidget); - expect( - tester.getRect(find.text('beta')), - const Rect.fromLTRB(46.0, 128.0, 238.0, 152.0), - ); - expect(find.text('kappa'), findsOneWidget); - expect( - tester.getRect(find.text('kappa')), - const Rect.fromLTRB(46.0, 168.0, 286.0, 192.0), - ); - - // Customize the animation - treeSliver = TreeSliver( - tree: treeNodes, - toggleAnimationStyle: AnimationStyle( - duration: const Duration(milliseconds: 500), - curve: Curves.bounceIn, - ), - ); - await tester.pumpWidget(MaterialApp( - home: CustomScrollView( - slivers: [ treeSliver ], - ), - )); - await tester.pump(); - expect( - find.byType(TreeSliver), - 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(46.0, 88.0, 286.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), - 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( - tree: treeNodes, - toggleAnimationStyle: AnimationStyle.noAnimation, - ); - await tester.pumpWidget(MaterialApp( - home: CustomScrollView( - slivers: [ treeSliver ], - ), - )); - await tester.pump(); - expect( - find.byType(TreeSliver), - 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), - 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(46.0, 88.0, 286.0, 112.0), - ); - expect(find.text('beta'), findsOneWidget); - expect( - tester.getRect(find.text('beta')), - const Rect.fromLTRB(46.0, 128.0, 238.0, 152.0), - ); - expect(find.text('kappa'), findsOneWidget); - expect( - tester.getRect(find.text('kappa')), - const Rect.fromLTRB(46.0, 168.0, 286.0, 192.0), - ); - }); - - testWidgets('Multiple animating node segments', (WidgetTester tester) async { - treeNodes = _setUpNodes(); - final TreeSliver treeSliver = TreeSliver(tree: treeNodes); - await tester.pumpWidget(MaterialApp( - home: CustomScrollView( - slivers: [ treeSliver ], - ), - )); - await tester.pump(); - expect( - find.byType(TreeSliver), - 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(46.0, 128.0, 286.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), - 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(46.0, 128.0, 286.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), - 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 naimating - ..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(46.0, 128.0, 286.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), - 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(46.0, 128.0, 286.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), - 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(46.0, 88.0, 286.0, 112.0), - ); - }); - - testWidgets('only paints visible rows', (WidgetTester tester) async { - treeNodes = _setUpNodes(); - final ScrollController scrollController = ScrollController(); - addTearDown(scrollController.dispose); - treeNodes = _setUpNodes(); - final TreeSliver treeSliver = TreeSliver( - treeRowExtentBuilder: (_, __) => 200, - tree: treeNodes, - ); - await tester.pumpWidget(MaterialApp( - home: CustomScrollView( - controller: scrollController, - slivers: [ 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 - }); -} diff --git a/packages/flutter/test/widgets/sliver_tree_test.dart b/packages/flutter/test/widgets/sliver_tree_test.dart deleted file mode 100644 index a10277c796..0000000000 --- a/packages/flutter/test/widgets/sliver_tree_test.dart +++ /dev/null @@ -1,727 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter_test/flutter_test.dart'; - -List> simpleNodeSet = >[ - TreeSliverNode('Root 0'), - TreeSliverNode( - 'Root 1', - expanded: true, - children: >[ - TreeSliverNode('Child 1:0'), - TreeSliverNode('Child 1:1'), - ], - ), - TreeSliverNode( - 'Root 2', - children: >[ - TreeSliverNode('Child 2:0'), - TreeSliverNode('Child 2:1'), - ], - ), - TreeSliverNode('Root 3'), -]; - -void main() { - group('TreeSliverNode', () { - test('getters, toString', () { - final List> children = >[ - TreeSliverNode('child'), - ]; - final TreeSliverNode node = TreeSliverNode( - 'parent', - children: children, - expanded: true, - ); - expect(node.content, 'parent'); - expect(node.children, children); - expect(node.isExpanded, isTrue); - expect(node.children.first.content, 'child'); - expect(node.children.first.children.isEmpty, isTrue); - expect(node.children.first.isExpanded, isFalse); - // Set by TreeSliver when built for tree integrity - expect(node.depth, isNull); - expect(node.parent, isNull); - expect(node.children.first.depth, isNull); - expect(node.children.first.parent, isNull); - - expect( - node.toString(), - 'TreeSliverNode: parent, depth: null, parent, expanded: true', - ); - expect( - node.children.first.toString(), - 'TreeSliverNode: child, depth: null, leaf', - ); - }); - - testWidgets('TreeSliverNode sets ups parent and depth properties', (WidgetTester tester) async { - final List> children = >[ - TreeSliverNode('child'), - ]; - final TreeSliverNode node = TreeSliverNode( - 'parent', - children: children, - expanded: true, - ); - await tester.pumpWidget(MaterialApp( - home: CustomScrollView( - slivers: [ - TreeSliver( - tree: >[node], - ), - ], - ) - )); - expect(node.content, 'parent'); - expect(node.children, children); - expect(node.isExpanded, isTrue); - expect(node.children.first.content, 'child'); - expect(node.children.first.children.isEmpty, isTrue); - expect(node.children.first.isExpanded, isFalse); - // Set by TreeSliver when built for tree integrity - expect(node.depth, 0); - expect(node.parent, isNull); - expect(node.children.first.depth, 1); - expect(node.children.first.parent, node); - - expect( - node.toString(), - 'TreeSliverNode: parent, depth: root, parent, expanded: true', - ); - expect( - node.children.first.toString(), - 'TreeSliverNode: child, depth: 1, leaf', - ); - }); - }); - - group('TreeController', () { - setUp(() { - // Reset node conditions for each test. - simpleNodeSet = >[ - TreeSliverNode('Root 0'), - TreeSliverNode( - 'Root 1', - expanded: true, - children: >[ - TreeSliverNode('Child 1:0'), - TreeSliverNode('Child 1:1'), - ], - ), - TreeSliverNode( - 'Root 2', - children: >[ - TreeSliverNode('Child 2:0'), - TreeSliverNode('Child 2:1'), - ], - ), - TreeSliverNode('Root 3'), - ]; - }); - testWidgets('Can set controller on TreeSliver', (WidgetTester tester) async { - final TreeSliverController controller = TreeSliverController(); - TreeSliverController? returnedController; - await tester.pumpWidget(MaterialApp( - home: CustomScrollView( - slivers: [ - TreeSliver( - tree: simpleNodeSet, - controller: controller, - treeNodeBuilder: ( - BuildContext context, - TreeSliverNode node, - AnimationStyle toggleAnimationStyle, - ) { - returnedController ??= TreeSliverController.of(context); - return TreeSliver.defaultTreeNodeBuilder( - context, - node, - toggleAnimationStyle, - ); - }, - ), - ], - ), - )); - expect(controller, returnedController); - }); - - testWidgets('Can get default controller on TreeSliver', (WidgetTester tester) async { - TreeSliverController? returnedController; - await tester.pumpWidget(MaterialApp( - home: CustomScrollView( - slivers: [ - TreeSliver( - tree: simpleNodeSet, - treeNodeBuilder: ( - BuildContext context, - TreeSliverNode node, - AnimationStyle toggleAnimationStyle, - ) { - returnedController ??= TreeSliverController.maybeOf(context); - return TreeSliver.defaultTreeNodeBuilder( - context, - node, - toggleAnimationStyle, - ); - }, - ), - ], - ), - )); - expect(returnedController, isNotNull); - }); - - testWidgets('Can get node for TreeSliverNode.content', (WidgetTester tester) async { - final TreeSliverController controller = TreeSliverController(); - await tester.pumpWidget(MaterialApp( - home: CustomScrollView( - slivers: [ - TreeSliver( - tree: simpleNodeSet, - controller: controller, - ), - ], - ), - )); - - expect(controller.getNodeFor('Root 0'), simpleNodeSet[0]); - }); - - testWidgets('Can get isExpanded for a node', (WidgetTester tester) async { - final TreeSliverController controller = TreeSliverController(); - await tester.pumpWidget(MaterialApp( - home: CustomScrollView( - slivers: [ - TreeSliver( - tree: simpleNodeSet, - controller: controller, - ), - ], - ), - )); - expect( - controller.isExpanded(simpleNodeSet[0]), - isFalse, - ); - expect( - controller.isExpanded(simpleNodeSet[1]), - isTrue, - ); - }); - - testWidgets('Can get isActive for a node', (WidgetTester tester) async { - final TreeSliverController controller = TreeSliverController(); - await tester.pumpWidget(MaterialApp( - home: CustomScrollView( - slivers: [ - TreeSliver( - tree: simpleNodeSet, - controller: controller, - ), - ], - ), - )); - expect( - controller.isActive(simpleNodeSet[0]), - isTrue, - ); - expect( - controller.isActive(simpleNodeSet[1]), - isTrue, - ); - // The parent 'Root 2' is not expanded, so its children are not active. - expect( - controller.isExpanded(simpleNodeSet[2]), - isFalse, - ); - expect( - controller.isActive(simpleNodeSet[2].children[0]), - isFalse, - ); - }); - - testWidgets('Can toggleNode, to collapse or expand', (WidgetTester tester) async { - final TreeSliverController controller = TreeSliverController(); - await tester.pumpWidget(MaterialApp( - home: CustomScrollView( - slivers: [ - TreeSliver( - tree: simpleNodeSet, - controller: controller, - ), - ], - ), - )); - - // The parent 'Root 2' is not expanded, so its children are not active. - expect( - controller.isExpanded(simpleNodeSet[2]), - isFalse, - ); - expect( - controller.isActive(simpleNodeSet[2].children[0]), - isFalse, - ); - // Toggle 'Root 2' to expand it - controller.toggleNode(simpleNodeSet[2]); - expect( - controller.isExpanded(simpleNodeSet[2]), - isTrue, - ); - expect( - controller.isActive(simpleNodeSet[2].children[0]), - isTrue, - ); - - // The parent 'Root 1' is expanded, so its children are active. - expect( - controller.isExpanded(simpleNodeSet[1]), - isTrue, - ); - expect( - controller.isActive(simpleNodeSet[1].children[0]), - isTrue, - ); - // Collapse 'Root 1' - controller.toggleNode(simpleNodeSet[1]); - expect( - controller.isExpanded(simpleNodeSet[1]), - isFalse, - ); - expect( - controller.isActive(simpleNodeSet[1].children[0]), - isTrue, - ); - // Nodes are not removed from the active list until the collapse animation - // completes. The parent expansion state also updates. - await tester.pumpAndSettle(); - expect( - controller.isExpanded(simpleNodeSet[1]), - isFalse, - ); - expect( - controller.isActive(simpleNodeSet[1].children[0]), - isFalse, - ); - }); - - testWidgets('Can expandNode, then collapseAll', - (WidgetTester tester) async { - final TreeSliverController controller = TreeSliverController(); - await tester.pumpWidget(MaterialApp( - home: CustomScrollView( - slivers: [ - TreeSliver( - tree: simpleNodeSet, - controller: controller, - ), - ], - ), - )); - - // The parent 'Root 2' is not expanded, so its children are not active. - expect( - controller.isExpanded(simpleNodeSet[2]), - isFalse, - ); - expect( - controller.isActive(simpleNodeSet[2].children[0]), - isFalse, - ); - // Expand 'Root 2' - controller.expandNode(simpleNodeSet[2]); - expect( - controller.isExpanded(simpleNodeSet[2]), - isTrue, - ); - expect( - controller.isActive(simpleNodeSet[2].children[0]), - isTrue, - ); - - // Both parents from our simple node set are expanded. - // 'Root 1' - expect(controller.isExpanded(simpleNodeSet[1]), isTrue); - // 'Root 2' - expect(controller.isExpanded(simpleNodeSet[2]), isTrue); - // Collapse both. - controller.collapseAll(); - await tester.pumpAndSettle(); - // Both parents from our simple node set have collapsed. - // 'Root 1' - expect(controller.isExpanded(simpleNodeSet[1]), isFalse); - // 'Root 2' - expect(controller.isExpanded(simpleNodeSet[2]), isFalse); - }); - - testWidgets('Can collapseNode, then expandAll', (WidgetTester tester) async { - final TreeSliverController controller = TreeSliverController(); - await tester.pumpWidget(MaterialApp( - home: CustomScrollView( - slivers: [ - TreeSliver( - tree: simpleNodeSet, - controller: controller, - ), - ], - ), - )); - - // The parent 'Root 1' is expanded, so its children are active. - expect( - controller.isExpanded(simpleNodeSet[1]), - isTrue, - ); - expect( - controller.isActive(simpleNodeSet[1].children[0]), - isTrue, - ); - // Collapse 'Root 1' - controller.collapseNode(simpleNodeSet[1]); - expect( - controller.isExpanded(simpleNodeSet[1]), - isFalse, - ); - expect( - controller.isActive(simpleNodeSet[1].children[0]), - isTrue, - ); - // Nodes are not removed from the active list until the collapse animation - // completes. - await tester.pumpAndSettle(); - expect( - controller.isActive(simpleNodeSet[1].children[0]), - isFalse, - ); - - // Both parents from our simple node set are collapsed. - // 'Root 1' - expect(controller.isExpanded(simpleNodeSet[1]), isFalse); - // 'Root 2' - expect(controller.isExpanded(simpleNodeSet[2]), isFalse); - // Expand both. - controller.expandAll(); - // Both parents from our simple node set are expanded. - // 'Root 1' - expect(controller.isExpanded(simpleNodeSet[1]), isTrue); - // 'Root 2' - expect(controller.isExpanded(simpleNodeSet[2]), isTrue); - }); - }); - - test('TreeSliverIndentationType values are properly reflected', () { - double value = TreeSliverIndentationType.standard.value; - expect(value, 10.0); - - value = TreeSliverIndentationType.none.value; - expect(value, 0.0); - - value = TreeSliverIndentationType.custom(50.0).value; - expect(value, 50.0); - }); - - testWidgets('.toggleNodeWith, onNodeToggle', (WidgetTester tester) async { - final TreeSliverController controller = TreeSliverController(); - // The default node builder wraps the leading icon with toggleNodeWith. - bool toggled = false; - TreeSliverNode? toggledNode; - await tester.pumpWidget(MaterialApp( - home: CustomScrollView( - slivers: [ - TreeSliver( - tree: simpleNodeSet, - controller: controller, - onNodeToggle: (TreeSliverNode node) { - toggled = true; - toggledNode = node as TreeSliverNode; - }, - ), - ], - ), - )); - expect(controller.isExpanded(simpleNodeSet[1]), isTrue); - await tester.tap(find.byType(Icon).first); - await tester.pump(); - expect(controller.isExpanded(simpleNodeSet[1]), isFalse); - expect(toggled, isTrue); - expect(toggledNode, simpleNodeSet[1]); - await tester.pumpAndSettle(); - expect(controller.isExpanded(simpleNodeSet[1]), isFalse); - toggled = false; - toggledNode = null; - // Use toggleNodeWith to make the whole row trigger the node state. - await tester.pumpWidget(MaterialApp( - home: CustomScrollView( - slivers: [ - TreeSliver( - tree: simpleNodeSet, - controller: controller, - onNodeToggle: (TreeSliverNode node) { - toggled = true; - toggledNode = node as TreeSliverNode; - }, - treeNodeBuilder: ( - BuildContext context, - TreeSliverNode node, - AnimationStyle toggleAnimationStyle, - ) { - final Duration animationDuration = - toggleAnimationStyle.duration ?? TreeSliver.defaultAnimationDuration; - final Curve animationCurve = - toggleAnimationStyle.curve ?? TreeSliver.defaultAnimationCurve; - // This makes the whole row trigger toggling. - return TreeSliver.wrapChildToToggleNode( - node: node, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Row(children: [ - // Icon for parent nodes - SizedBox.square( - dimension: 30.0, - child: node.children.isNotEmpty - ? AnimatedRotation( - turns: node.isExpanded ? 0.25 : 0.0, - duration: animationDuration, - curve: animationCurve, - child: const Icon(IconData(0x25BA), size: 14), - ) - : null, - ), - // Spacer - const SizedBox(width: 8.0), - // Content - Text(node.content.toString()), - ]), - ), - ); - }, - ), - ], - ), - )); - // Still collapsed from earlier - expect(controller.isExpanded(simpleNodeSet[1]), isFalse); - // Tapping on the text instead of the Icon. - await tester.tap(find.text('Root 1')); - await tester.pump(); - expect(controller.isExpanded(simpleNodeSet[1]), isTrue); - expect(toggled, isTrue); - expect(toggledNode, simpleNodeSet[1]); - }); - - testWidgets('AnimationStyle is piped through to node builder', (WidgetTester tester) async { - AnimationStyle? style; - await tester.pumpWidget(MaterialApp( - home: CustomScrollView( - slivers: [ - TreeSliver( - tree: simpleNodeSet, - treeNodeBuilder: ( - BuildContext context, - TreeSliverNode node, - AnimationStyle toggleAnimationStyle, - ) { - style ??= toggleAnimationStyle; - return Text(node.content.toString()); - }, - ), - ], - ), - )); - // Default - expect(style, TreeSliver.defaultToggleAnimationStyle); - - await tester.pumpWidget(MaterialApp( - home: CustomScrollView( - slivers: [ - TreeSliver( - tree: simpleNodeSet, - toggleAnimationStyle: AnimationStyle.noAnimation, - treeNodeBuilder: ( - BuildContext context, - TreeSliverNode node, - AnimationStyle toggleAnimationStyle, - ) { - style = toggleAnimationStyle; - return Text(node.content.toString()); - }, - ), - ], - ), - )); - expect(style, isNotNull); - expect(style!.curve, isNull); - expect(style!.duration, Duration.zero); - style = null; - - await tester.pumpWidget(MaterialApp( - home: CustomScrollView( - slivers: [ - TreeSliver( - tree: simpleNodeSet, - toggleAnimationStyle: AnimationStyle( - curve: Curves.easeIn, - duration: const Duration(milliseconds: 200), - ), - treeNodeBuilder: ( - BuildContext context, - TreeSliverNode node, - AnimationStyle toggleAnimationStyle, - ) { - style ??= toggleAnimationStyle; - return Text(node.content.toString()); - }, - ), - ], - ), - )); - expect(style, isNotNull); - expect(style!.curve, Curves.easeIn); - expect(style!.duration, const Duration(milliseconds: 200)); - }); - - testWidgets('Adding more root TreeViewNodes are reflected in the tree', (WidgetTester tester) async { - simpleNodeSet = >[ - TreeSliverNode('Root 0'), - TreeSliverNode( - 'Root 1', - expanded: true, - children: >[ - TreeSliverNode('Child 1:0'), - TreeSliverNode('Child 1:1'), - ], - ), - TreeSliverNode( - 'Root 2', - children: >[ - TreeSliverNode('Child 2:0'), - TreeSliverNode('Child 2:1'), - ], - ), - TreeSliverNode('Root 3'), - ]; - final TreeSliverController controller = TreeSliverController(); - await tester.pumpWidget(MaterialApp( - home: StatefulBuilder( - builder: (BuildContext context, StateSetter setState) { - return Scaffold( - body: CustomScrollView( - slivers: [ - TreeSliver( - tree: simpleNodeSet, - controller: controller, - ), - ], - ), - floatingActionButton: FloatingActionButton( - onPressed: () { - setState(() { - simpleNodeSet.add(TreeSliverNode('Added root')); - }); - }, - ), - ); - }, - ), - )); - await tester.pump(); - - expect(find.text('Root 0'), findsOneWidget); - expect(find.text('Root 1'), findsOneWidget); - expect(find.text('Child 1:0'), findsOneWidget); - expect(find.text('Child 1:1'), findsOneWidget); - expect(find.text('Root 2'), findsOneWidget); - expect(find.text('Child 2:0'), findsNothing); - expect(find.text('Child 2:1'), findsNothing); - expect(find.text('Root 3'), findsOneWidget); - expect(find.text('Added root'), findsNothing); - - await tester.tap(find.byType(FloatingActionButton)); - await tester.pump(); - - expect(find.text('Root 0'), findsOneWidget); - expect(find.text('Root 1'), findsOneWidget); - expect(find.text('Child 1:0'), findsOneWidget); - expect(find.text('Child 1:1'), findsOneWidget); - expect(find.text('Root 2'), findsOneWidget); - expect(find.text('Child 2:0'), findsNothing); - expect(find.text('Child 2:1'), findsNothing); - expect(find.text('Root 3'), findsOneWidget); - // Node was added - expect(find.text('Added root'), findsOneWidget); - }); - - testWidgets('Adding more TreeViewNodes below the root are reflected in the tree', (WidgetTester tester) async { - simpleNodeSet = >[ - TreeSliverNode('Root 0'), - TreeSliverNode( - 'Root 1', - expanded: true, - children: >[ - TreeSliverNode('Child 1:0'), - TreeSliverNode('Child 1:1'), - ], - ), - TreeSliverNode( - 'Root 2', - children: >[ - TreeSliverNode('Child 2:0'), - TreeSliverNode('Child 2:1'), - ], - ), - TreeSliverNode('Root 3'), - ]; - final TreeSliverController controller = TreeSliverController(); - await tester.pumpWidget(MaterialApp( - home: StatefulBuilder( - builder: (BuildContext context, StateSetter setState) { - return Scaffold( - body: CustomScrollView( - slivers: [ - TreeSliver( - tree: simpleNodeSet, - controller: controller, - ), - ], - ), - floatingActionButton: FloatingActionButton( - onPressed: () { - setState(() { - simpleNodeSet[1].children.add(TreeSliverNode('Added child')); - }); - }, - ), - ); - }, - ), - )); - await tester.pump(); - expect(find.text('Root 0'), findsOneWidget); - expect(find.text('Root 1'), findsOneWidget); - expect(find.text('Child 1:0'), findsOneWidget); - expect(find.text('Child 1:1'), findsOneWidget); - expect(find.text('Added child'), findsNothing); - expect(find.text('Root 2'), findsOneWidget); - expect(find.text('Child 2:0'), findsNothing); - expect(find.text('Child 2:1'), findsNothing); - expect(find.text('Root 3'), findsOneWidget); - await tester.tap(find.byType(FloatingActionButton)); - await tester.pump(); - expect(find.text('Root 0'), findsOneWidget); - expect(find.text('Root 1'), findsOneWidget); - expect(find.text('Child 1:0'), findsOneWidget); - expect(find.text('Child 1:1'), findsOneWidget); - // Child node was added - expect(find.text('Added child'), findsOneWidget); - expect(find.text('Root 2'), findsOneWidget); - expect(find.text('Child 2:0'), findsNothing); - expect(find.text('Child 2:1'), findsNothing); - expect(find.text('Root 3'), findsOneWidget); - }); -}