From 8a255a73d17fa8bdd160c26704de8c1cbf5a5da0 Mon Sep 17 00:00:00 2001 From: baumths Date: Fri, 3 May 2024 12:25:23 -0300 Subject: [PATCH] experiment came out great the only gotcha is when a node is expanded while it is collapsing; all descendants are shown twice temporarily. --- example/lib/src/examples.dart | 6 +- example/lib/src/examples/animated.dart | 192 +++++++++++++++++++++++++ 2 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 example/lib/src/examples/animated.dart diff --git a/example/lib/src/examples.dart b/example/lib/src/examples.dart index 40c2998..4d29760 100644 --- a/example/lib/src/examples.dart +++ b/example/lib/src/examples.dart @@ -1,9 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:flutter_fancy_tree_view/flutter_fancy_tree_view.dart'; +import 'package:flutter_fancy_tree_view/flutter_fancy_tree_view.dart' + hide AnimatedTreeView; import 'package:path_drawing/path_drawing.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'examples/animated.dart' show AnimatedTreeView; import 'examples/drag_and_drop.dart' show DragAndDropTreeView; import 'examples/filterable.dart' show FilterableTreeView; import 'examples/lazy_loading.dart' show LazyLoadingTreeView; @@ -33,6 +35,7 @@ class SelectedExampleNotifier extends ValueNotifier { } enum Example { + animated('Animated', Icon(Icons.animation)), dragAndDrop('Drag and Drop', Icon(Icons.move_down_rounded)), filterable('Filterable', Icon(Icons.manage_search_rounded)), lazyLoading('Lazy Loading', Icon(Icons.hourglass_top_rounded)), @@ -58,6 +61,7 @@ class ExamplesView extends StatelessWidget { child: TreeIndentGuideScope( key: Key(selectedExample.title), child: switch (selectedExample) { + Example.animated => const AnimatedTreeView(), Example.dragAndDrop => const DragAndDropTreeView(), Example.filterable => const FilterableTreeView(), Example.lazyLoading => const LazyLoadingTreeView(), diff --git a/example/lib/src/examples/animated.dart b/example/lib/src/examples/animated.dart new file mode 100644 index 0000000..af8b72e --- /dev/null +++ b/example/lib/src/examples/animated.dart @@ -0,0 +1,192 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_fancy_tree_view/flutter_fancy_tree_view.dart' + hide SliverAnimatedTree, AnimatedTreeView; + +import '../tree_data.dart' show generateTreeNodes; + +class Node { + Node({required this.title}) : children = []; + + final String title; + final List children; +} + +class AnimatedTreeView extends StatefulWidget { + const AnimatedTreeView({super.key}); + + @override + State createState() => _AnimatedTreeViewState(); +} + +class _AnimatedTreeViewState extends State { + late final TreeController treeController; + late final Node root = Node(title: 'A portion of the world'); + + @override + void initState() { + super.initState(); + generateTreeNodes(root, (Node parent, String title) { + final child = Node(title: title); + parent.children.add(child); + return child; + }); + + treeController = TreeController( + roots: root.children, + childrenProvider: (Node node) => node.children, + ); + } + + @override + void dispose() { + treeController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return CustomScrollView( + slivers: [ + SliverAnimatedTree( + treeController: treeController, + duration: Durations.long2, + nodeBuilder: (BuildContext context, TreeEntry entry) { + return TreeIndentation( + entry: entry, + child: Row( + children: [ + FolderButton( + key: Key('FolderButton#${entry.node.title}'), + isOpen: entry.isExpanded, + onPressed: () => treeController.toggleExpansion(entry.node), + ), + Flexible(child: Text(entry.node.title)), + ], + ), + ); + }, + ), + ], + ); + } +} + +class SliverAnimatedTree extends StatefulWidget { + const SliverAnimatedTree({ + super.key, + required this.treeController, + required this.nodeBuilder, + this.duration = Durations.medium2, + this.transitionBuilder = defaultTreeTransitionBuilder, + }); + + final TreeController treeController; + final TreeNodeBuilder nodeBuilder; + final Duration duration; + final TreeTransitionBuilder transitionBuilder; + + @override + State> createState() => _SliverAnimatedTreeState(); +} + +class _SliverAnimatedTreeState + extends State> { + final GlobalKey _listKey = + GlobalKey(); + + late Map> _nodeToEntry = >{}; + List> _flatTree = const []; + + void _createFlatTree() { + final Map> newEntries = >{}; + final List> flatTree = >[]; + + widget.treeController.depthFirstTraversal(onTraverse: (TreeEntry entry) { + flatTree.add(entry); + newEntries[entry.node] = entry; + }); + + _flatTree = flatTree; + _nodeToEntry = newEntries; + } + + void _updateFlatTree() { + if (widget.duration == Duration.zero) { + setState(_createFlatTree); + return; + } + + final Map> oldEntries = >{..._nodeToEntry}; + final Map> newEntries = >{}; + final List indicesAnimatingIn = []; + final List> flatTree = >[]; + + widget.treeController.depthFirstTraversal(onTraverse: (TreeEntry entry) { + flatTree.add(entry); + newEntries[entry.node] = entry; + + if (oldEntries.remove(entry.node) == null) { + indicesAnimatingIn.add(entry.index); + } + }); + + for (final TreeEntry entry in oldEntries.values.toList().reversed) { + _listKey.currentState?.removeItem( + duration: widget.duration, + entry.index, + (BuildContext context, Animation animation) { + return widget.transitionBuilder( + context, + widget.nodeBuilder(context, entry), + animation, + ); + }, + ); + } + + setState(() { + _flatTree = flatTree; + _nodeToEntry = newEntries; + }); + + for (final int index in indicesAnimatingIn) { + _listKey.currentState?.insertItem(index, duration: widget.duration); + } + } + + void _rebuild() => _updateFlatTree(); + + @override + void initState() { + super.initState(); + widget.treeController.addListener(_rebuild); + _createFlatTree(); + } + + @override + void dispose() { + _flatTree = const []; + _nodeToEntry = const {}; + widget.treeController.removeListener(_rebuild); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SliverAnimatedList( + key: _listKey, + initialItemCount: _flatTree.length, + itemBuilder: ( + BuildContext context, + int index, + Animation animation, + ) { + return widget.transitionBuilder( + context, + widget.nodeBuilder(context, _flatTree[index]), + animation, + ); + }, + ); + } +}