Flutter优雅且灵活的带有动画效果的树形视图组件arborio的使用

发布于 1周前 作者 gougou168 最后一次编辑是 5天前 来自 Flutter

Flutter优雅且灵活的带有动画效果的树形视图组件arborio的使用

Arborio简介

Arborio 是一个优雅且灵活的带有动画效果的树形视图组件,可以在Flutter中展示层级数据。以下是Arborio的一些特点:

  • 🌳 支持无限嵌套的层级数据展示
  • ✨ 展开/折叠操作时具有平滑动画效果
  • 🎨 节点和展开器外观完全可定制
  • 🔑 提供全局键支持以实现程序化控制
  • 🎯 支持泛型,类型安全
  • 📱 响应式设计,移动友好

你可以在这里查看实时示例应用

基本用法

下面是一个简单的例子,展示了如何创建一个树形视图:

import 'package:arborio/tree_view.dart';
import 'package:flutter/material.dart';

enum ElementType { file, folder }

// 定义你的数据类型
class FileSystemElement {
  FileSystemElement(this.name, this.type);
  final String name;
  final ElementType type;
}

// 创建树节点
final nodes = [
  TreeNode<FileSystemElement>(
    const Key('root'),
    FileSystemElement('Documents', ElementType.folder),
    [
      TreeNode<FileSystemElement>(
        const Key('child1'),
        FileSystemElement('report.pdf', ElementType.file),
      ),
    ],
  ),
];

void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) => MaterialApp(
        home: Scaffold(
          body: TreeView<FileSystemElement>(
            nodes: nodes,
            builder: (context, node, isSelected, animation, select) => Row(
              children: [
                Icon(
                  node.data.type == ElementType.folder
                      ? Icons.folder
                      : Icons.file_copy,
                ),
                Text(node.data.name),
              ],
            ),
            expanderBuilder: (context, isExpanded, animation) =>
                RotationTransition(
              turns: animation,
              child: const Icon(Icons.chevron_right),
            ),
          ),
        ),
        debugShowCheckedModeBanner: false,
      );
}

使用TreeViewKey

TreeViewKey允许对树形视图进行程序化控制:

// 创建一个键
const treeViewKey = TreeViewKey<FileSystemElement>();

// 在你的TreeView中使用它
TreeView<FileSystemElement>(
  key: treeViewKey,
  nodes: nodes,
  builder: (context, node, isSelected, animation, select) {
    // ... 节点构建器实现
  },
  expanderBuilder: (context, isExpanded, animation) {
    return RotationTransition(
      turns: animation,
      child: const Icon(Icons.chevron_right),
    );
  },
)

节点管理

你可以动态地添加或移除节点,下面的例子展示了如何通过状态管理来实现这一点:

// 添加新节点
FloatingActionButton(
  onPressed: () => setState(() {
    nodes.add(
      TreeNode(
        const Key('newnode'),
        FileSystemElement('New Folder', ElementType.folder),
      ),
    );
  }),
  child: const Icon(Icons.add),
),

// 展开/折叠所有节点
FloatingActionButton(
  onPressed: () => setState(() {
    treeViewKey.currentState?.expandAll();
  }),
  child: const Icon(Icons.expand),
),

FloatingActionButton(
  onPressed: () => setState(() {
    treeViewKey.currentState?.collapseAll();
  }),
  child: const Icon(Icons.compress),
),

处理节点事件

你可以监听节点的展开/折叠以及选择变化事件:

TreeView<FileSystemElement>(
  onExpansionChanged: (node, expanded) {
    print('Node ${node.data.name} is now ${expanded ? 'expanded' : 'collapsed'}');
  },
  onSelectionChanged: (node) {
    print('Selected node: ${node.data.name}');
  },
  expanderBuilder: (context, isExpanded, animation) {
    return RotationTransition(
      turns: animation,
      child: const Icon(Icons.chevron_right),
    );
  },
)

自定义节点外观

builder参数提供了对节点外观的全面控制,并包括一个动画变量,以便你能够响应随时间的变化:

builder: (context, node, isSelected, animation, select) {
  return InkWell(
    onTap: () => select(node),
    child: Container(
      color: isSelected ? Colors.blue.withOpacity(0.1) : null,
      padding: const EdgeInsets.all(8),
      child: Row(
        children: [
          if (node.data.type == ElementType.folder)
            RotationTransition(
              turns: animation,
              child: const Icon(Icons.folder),
            )
          else
            const Icon(Icons.file_copy),
          const SizedBox(width: 8),
          Text(node.data.name),
        ],
      ),
    ),
  );
}

更多功能示例

以下是一个更复杂的示例,展示了更多高级功能,如自定义动画曲线、扩展图标选择等:

import 'dart:ui';

import 'package:arborio/tree_view.dart';
import 'package:flutter/material.dart';
import 'package:path/path.dart' as path;

enum ElementType { file, folder }

class FileSystemElement {
  FileSystemElement(this.name, this.type);

  final String name;
  final ElementType type;
}

List<TreeNode<FileSystemElement>> fileTree() => [
      TreeNode<FileSystemElement>(
        const Key('Projects'),
        FileSystemElement('Projects', ElementType.folder),
        [
          TreeNode<FileSystemElement>(
            const Key('FlutterApp'),
            FileSystemElement('FlutterApp', ElementType.folder),
            [
              TreeNode<FileSystemElement>(
                const Key('lib'),
                FileSystemElement('lib', ElementType.folder),
                [
                  TreeNode<FileSystemElement>(
                    const Key('main.dart'),
                    FileSystemElement('main.dart', ElementType.file),
                  ),
                  TreeNode<FileSystemElement>(
                    const Key('app.dart'),
                    FileSystemElement('app.dart', ElementType.file),
                  ),
                ],
              ),
              TreeNode<FileSystemElement>(
                const Key('assets'),
                FileSystemElement('assets', ElementType.folder),
                [
                  TreeNode<FileSystemElement>(
                    const Key('logo.png'),
                    FileSystemElement('logo.png', ElementType.file),
                  ),
                  TreeNode<FileSystemElement>(
                    const Key('data.json'),
                    FileSystemElement('data.json', ElementType.file),
                  ),
                ],
              ),
            ],
          ),
          TreeNode<FileSystemElement>(
            const Key('PythonScripts'),
            FileSystemElement('PythonScripts', ElementType.folder),
            [
              TreeNode<FileSystemElement>(
                const Key('script.py'),
                FileSystemElement('script.py', ElementType.file),
              ),
            ],
          ),
        ],
      ),
      TreeNode<FileSystemElement>(
        const Key('Documents'),
        FileSystemElement('Documents', ElementType.folder),
        [
          TreeNode<FileSystemElement>(
            const Key('Resume.docx'),
            FileSystemElement('Resume.docx', ElementType.file),
          ),
          TreeNode<FileSystemElement>(
            const Key('Budget.xlsx'),
            FileSystemElement('Budget.xlsx', ElementType.file),
          ),
        ],
      ),
      TreeNode<FileSystemElement>(
        const Key('Music'),
        FileSystemElement('Music', ElementType.folder),
        [
          TreeNode<FileSystemElement>(
            const Key('Favorites'),
            FileSystemElement('Favorites', ElementType.folder),
            [
              TreeNode<FileSystemElement>(
                const Key('song1.mp3'),
                FileSystemElement('song1.mp3', ElementType.file),
              ),
              TreeNode<FileSystemElement>(
                const Key('song2.mp3'),
                FileSystemElement('song2.mp3', ElementType.file),
              ),
            ],
          ),
        ],
      ),
      TreeNode<FileSystemElement>(
        const Key('Photos'),
        FileSystemElement('Photos', ElementType.folder),
        [
          TreeNode<FileSystemElement>(
            const Key('Vacation'),
            FileSystemElement('Vacation', ElementType.folder),
            [
              TreeNode<FileSystemElement>(
                const Key('image1.jpg'),
                FileSystemElement('image1.jpg', ElementType.file),
              ),
              TreeNode<FileSystemElement>(
                const Key('image2.jpg'),
                FileSystemElement('image2.jpg', ElementType.file),
              ),
            ],
          ),
          TreeNode<FileSystemElement>(
            const Key('Family'),
            FileSystemElement('Family', ElementType.folder),
            [
              TreeNode<FileSystemElement>(
                const Key('photo1.jpg'),
                FileSystemElement('photo1.jpg', ElementType.file),
              ),
            ],
          ),
        ],
      ),
    ];

const defaultExpander = Icon(Icons.chevron_right);

const arrowRight = Icon(Icons.arrow_right);

const doubleArrow = Icon(Icons.double_arrow);

const fingerPointer = Text(
  '👉',
  style: TextStyle(fontSize: 16),
);

void main() {
  runApp(const MyApp());
}

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

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

class _MyAppState extends State<MyApp> {
  final treeViewKey = const TreeViewKey<FileSystemElement>();
  String _selectedCurve = 'easeInOut';
  Widget _expander = defaultExpander;
  int _animationDuration = 500;
  final textEditingController = TextEditingController(text: '500');
  TreeNode<FileSystemElement>? _selectedNode;
  final List<TreeNode<FileSystemElement>> _fileTree = fileTree();

  @override
  Widget build(BuildContext context) => MaterialApp(
        theme: ThemeData(
          useMaterial3: true,
          colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF44AD4D)),
        ),
        debugShowCheckedModeBanner: false,
        home: Scaffold(
          backgroundColor: const Color(0xFFFEFCE5),
          appBar: PreferredSize(
            preferredSize: const Size(
              double.infinity,
              56,
            ),
            child: ClipRRect(
              child: BackdropFilter(
                filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
                child: AppBar(
                  backgroundColor: Colors.black.withAlpha(51),
                  title: Text(_title()),
                  elevation: 3,
                ),
              ),
            ),
          ),
          body: Stack(
            children: [
              Opacity(
                opacity: .025,
                child: Image.asset(
                  'assets/images/arborio_transparent.png',
                  fit: BoxFit.scaleDown,
                  width: double.infinity,
                  height: double.infinity,
                ),
              ),
              _treeView(),
              Positioned(
                bottom: 0,
                left: 0,
                right: 0,
                child: _bottomPane(),
              ),
            ],
          ),
        ),
      );

  ClipRRect _bottomPane() => ClipRRect(
        child: BackdropFilter(
          filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
          child: Container(
            height: 90,
            color: Colors.black.withAlpha(26),
            width: double.infinity,
            child: SingleChildScrollView(
              scrollDirection: Axis.horizontal,
              child: Row(
                children: [
                  const SizedBox(width: 16),
                  _durationField(),
                  const SizedBox(width: 16),
                  _dropDownsRow(),
                  const SizedBox(width: 16),
                  _buttonRow(),
                ],
              ),
            ),
          ),
        ),
      );

  Widget _durationField() => Center(
        child: SizedBox(
          width: 200,
          child: TextField(
            keyboardType: TextInputType.number,
            decoration: InputDecoration(
              border: OutlineInputBorder(
                borderRadius: BorderRadius.circular(4),
              ),
              labelText: 'Animation Duration (ms)',
            ),
            onChanged: (v) {
              setState(() {
                _animationDuration = int.tryParse(v) ?? 500;
              });
            },
            controller: textEditingController,
          ),
        ),
      );

  String _title() => 'Arborio Sample${_selectedNode != null ? ' - '
      '${_selectedNode!.data.name}' : ''}';

  Widget _dropDownsRow() => Row(
        children: [
          DropdownMenu<Widget>(
            label: const Text('Expander'),
            onSelected: (v) => setState(() => _expander = v ?? _expander),
            initialSelection: _expander,
            dropdownMenuEntries: const [
              DropdownMenuEntry(value: fingerPointer, label: '👉'),
              DropdownMenuEntry(
                value: defaultExpander,
                label: 'Chevron Right Icon',
              ),
              DropdownMenuEntry(
                value: arrowRight,
                label: 'Arrow Right',
              ),
              DropdownMenuEntry(
                value: doubleArrow,
                label: 'Double Arrow',
              ),
            ],
          ),
          const SizedBox(width: 16),
          DropdownMenu<String>(
            label: const Text('Animation Curve'),
            onSelected: (v) => _selectedCurve = v ?? _selectedCurve,
            initialSelection: _selectedCurve,
            dropdownMenuEntries: const [
              DropdownMenuEntry(value: 'bounceIn', label: 'bounceIn'),
              DropdownMenuEntry(value: 'bounceInOut', label: 'bounceInOut'),
              DropdownMenuEntry(value: 'bounceOut', label: 'bounceOut'),
              DropdownMenuEntry(value: 'ease', label: 'ease'),
              DropdownMenuEntry(value: 'easeIn', label: 'easeIn'),
              DropdownMenuEntry(value: 'easeInBack', label: 'easeInBack'),
              DropdownMenuEntry(value: 'easeInCirc', label: 'easeInCirc'),
              DropdownMenuEntry(value: 'easeInExpo', label: 'easeInExpo'),
              DropdownMenuEntry(value: 'easeInOut', label: 'easeInOut'),
              DropdownMenuEntry(
                value: 'easeInOutBack',
                label: 'easeInOutBack',
              ),
              DropdownMenuEntry(
                value: 'easeInOutCirc',
                label: 'easeInOutCirc',
              ),
              DropdownMenuEntry(
                value: 'easeInOutExpo',
                label: 'easeInOutExpo',
              ),
              DropdownMenuEntry(
                value: 'easeInOutQuad',
                label: 'easeInOutQuad',
              ),
              DropdownMenuEntry(
                value: 'easeInOutQuart',
                label: 'easeInOutQuart',
              ),
              DropdownMenuEntry(
                value: 'easeInOutQuint',
                label: 'easeInOutQuint',
              ),
              DropdownMenuEntry(
                value: 'easeInOutSine',
                label: 'easeInOutSine',
              ),
              DropdownMenuEntry(value: 'easeInQuad', label: 'easeInQuad'),
              DropdownMenuEntry(value: 'easeInQuart', label: 'easeInQuart'),
              DropdownMenuEntry(value: 'easeInQuint', label: 'easeInQuint'),
              DropdownMenuEntry(value: 'easeInSine', label: 'easeInSine'),
              DropdownMenuEntry(value: 'easeOut', label: 'easeOut'),
              DropdownMenuEntry(value: 'easeOutBack', label: 'easeOutBack'),
              DropdownMenuEntry(value: 'easeOutCirc', label: 'easeOutCirc'),
              DropdownMenuEntry(value: 'easeOutExpo', label: 'easeOutExpo'),
              DropdownMenuEntry(value: 'easeOutQuad', label: 'easeOutQuad'),
              DropdownMenuEntry(value: 'easeOutQuart', label: 'easeOutQuart'),
              DropdownMenuEntry(value: 'easeOutQuint', label: 'easeOutQuint'),
              DropdownMenuEntry(value: 'easeOutSine', label: 'easeOutSine'),
              DropdownMenuEntry(value: 'elasticIn', label: 'elasticIn'),
              DropdownMenuEntry(value: 'elasticInOut', label: 'elasticInOut'),
              DropdownMenuEntry(value: 'elasticOut', label: 'elasticOut'),
              DropdownMenuEntry(value: 'linear', label: 'linear'),
            ],
          ),
        ],
      );

  Row _buttonRow() => Row(
        children: [
          FloatingActionButton(
            tooltip: 'Add',
            onPressed: () => setState(
              () => _fileTree.add(
                TreeNode(
                  const Key('newnode'),
                  FileSystemElement(
                    'New Folder',
                    ElementType.folder,
                  ),
                ),
              ),
            ),
            child: const Icon(Icons.add),
          ),
          const SizedBox(width: 16),
          FloatingActionButton(
            tooltip: 'Expand All',
            onPressed: () =>
                setState(() => treeViewKey.currentState!.expandAll()),
            child: const Icon(Icons.expand),
          ),
          const SizedBox(width: 16),
          FloatingActionButton(
            tooltip: 'Collapse All',
            onPressed: () =>
                setState(() => treeViewKey.currentState!.collapseAll()),
            child: const Icon(Icons.compress),
          ),
        ],
      );

  TreeView<FileSystemElement> _treeView() => TreeView(
        onSelectionChanged: (node) => setState(() => _selectedNode = node),
        key: treeViewKey,
        animationDuration: Duration(milliseconds: _animationDuration),
        animationCurve: switch (_selectedCurve) {
          ('bounceIn') => Curves.bounceIn,
          ('bounceInOut') => Curves.bounceInOut,
          ('bounceOut') => Curves.bounceOut,
          ('easeInCirc') => Curves.easeInCirc,
          ('easeInOutExpo') => Curves.easeInOutExpo,
          ('elasticInOut') => Curves.elasticInOut,
          ('easeInOut') => Curves.easeInOut,
          ('easeOutCirc') => Curves.easeOutCirc,
          ('elasticOut') => Curves.elasticOut,
          ('elasticIn') => Curves.elasticIn,
          ('easeIn') => Curves.easeIn,
          ('ease') => Curves.ease,
          ('easeInBack') => Curves.easeInBack,
          ('easeOutBack') => Curves.easeOutBack,
          ('easeInOutBack') => Curves.easeInOutBack,
          ('easeInSine') => Curves.easeInSine,
          ('easeOutSine') => Curves.easeOutSine,
          ('easeInOutSine') => Curves.easeInOutSine,
          ('easeInQuad') => Curves.easeInQuad,
          ('easeOutQuad') => Curves.easeOutQuad,
          ('easeInOutQuad') => Curves.easeInOutQuad,
          ('easeInQuart') => Curves.easeInQuart,
          ('easeOutQuart') => Curves.easeOutQuart,
          ('easeInOutQuart') => Curves.easeInOutQuart,
          ('easeInQuint') => Curves.easeInQuint,
          ('easeOutQuint') => Curves.easeOutQuint,
          ('easeInOutQuint') => Curves.easeInOutQuint,
          ('easeInExpo') => Curves.easeInExpo,
          ('easeOutExpo') => Curves.easeOutExpo,
          ('linear') => Curves.linear,
          _ => Curves.easeInOut,
        },
        builder: (
          context,
          node,
          isSelected,
          expansionAnimation,
          select,
        ) =>
            switch (node.data.type) {
          (ElementType.file) => InkWell(
              onTap: () => select(node),
              // ignore: use_decorated_box
              child: Container(
                margin: const EdgeInsets.symmetric(horizontal: 8),
                decoration: BoxDecoration(
                  gradient: isSelected
                      ? LinearGradient(
                          colors: [
                            Theme.of(context)
                                .colorScheme
                                .primary
                                .withAlpha(77),
                            Theme.of(context)
                                .colorScheme
                                .primary
                                .withAlpha(26),
                          ],
                          begin: Alignment.topLeft,
                          end: Alignment.bottomRight,
                        )
                      : null,
                  borderRadius: BorderRadius.circular(4),
                ),
                child: Padding(
                  padding: const EdgeInsets.all(8),
                  child: Row(
                    children: [
                      Image.asset(
                        switch (path.extension(node.data.name).toLowerCase()) {
                          ('.mp3') => 'assets/images/music.png',
                          ('.py') => 'assets/images/python.png',
                          ('.jpg') => 'assets/images/image.png',
                          ('.png') => 'assets/images/image.png',
                          ('.dart') => 'assets/images/dart.png',
                          ('.json') => 'assets/images/json.png',
                          (_) => 'assets/images/file.png'
                        },
                        width: 32,
                        height: 32,
                      ),
                      const SizedBox(width: 16),
                      Text(node.data.name),
                    ],
                  ),
                ),
              ),
            ),
          (ElementType.folder) => Row(
              children: [
                RotationTransition(
                  turns: expansionAnimation,
                  child: Image.asset(
                    'assets/images/folder.png',
                    width: 32,
                    height: 32,
                  ),
                ),
                const SizedBox(width: 16),
                Text(node.data.name),
              ],
            ),
        },
        nodes: _fileTree,
        expanderBuilder: (context, node, animationValue) => RotationTransition(
          turns: animationValue,
          child: _expander,
        ),
      );
}

这个更完整的示例展示了如何结合多种功能,使树形视图更加丰富和互动。希望这些信息能帮助你更好地理解和使用Arborio插件!


更多关于Flutter优雅且灵活的带有动画效果的树形视图组件arborio的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter优雅且灵活的带有动画效果的树形视图组件arborio的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


探索和使用Flutter中未知功能插件arborio时,我们需要先确保已经将该插件添加到项目中。由于arborio可能是一个不太常见的插件,这里假设它已经在pub.dev上有发布,或者你已经从某个私有源获取到了这个插件。以下是如何在Flutter项目中集成并使用该插件的一个基本示例。

1. 添加依赖

首先,在你的pubspec.yaml文件中添加arborio插件的依赖:

dependencies:
  flutter:
    sdk: flutter
  arborio: ^x.y.z  # 替换为实际的版本号

然后运行flutter pub get来安装依赖。

2. 导入插件

在你需要使用arborio插件的Dart文件中导入它:

import 'package:arborio/arborio.dart';

3. 使用插件功能

由于arborio的具体功能未知,这里将展示一个假设的使用场景。假设arborio提供了一个用于处理某种数据的API,我们可以这样使用它:

import 'package:flutter/material.dart';
import 'package:arborio/arborio.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Arborio Plugin Demo'),
        ),
        body: ArborioDemo(),
      ),
    );
  }
}

class ArborioDemo extends StatefulWidget {
  @override
  _ArborioDemoState createState() => _ArborioDemoState();
}

class _ArborioDemoState extends State<ArborioDemo> {
  String result = '';

  @override
  void initState() {
    super.initState();
    // 假设arborio有一个名为processData的方法
    _processData();
  }

  Future<void> _processData() async {
    try {
      // 假设这个方法接受一个字符串参数并返回一个处理后的结果
      String data = 'example data';
      String processedData = await Arborio.processData(data);
      
      // 更新UI
      setState(() {
        result = processedData;
      });
    } catch (e) {
      // 处理错误
      print('Error processing data: $e');
      setState(() {
        result = 'Error processing data';
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Text('Processed Data:'),
          Text(result),
        ],
      ),
    );
  }
}

在这个示例中,我们假设arborio插件有一个静态方法processData,它接受一个字符串参数并返回一个处理后的结果。我们在initState方法中调用这个方法,并在UI中显示结果。

注意事项

  • 插件文档:务必查阅arborio插件的官方文档或源代码,以了解它的具体功能和使用方法。
  • 错误处理:在实际应用中,应添加更详细的错误处理逻辑。
  • 权限:如果arborio插件需要特定的系统权限(如访问存储、相机等),请确保在AndroidManifest.xmlInfo.plist中声明这些权限,并在运行时请求权限。

由于arborio是一个未知插件,上述代码仅为一个假设性的示例。在实际使用中,你需要根据插件的实际API和功能进行调整。

回到顶部