Flutter 中的 ExpansionPanel:示例指南

阎德宇
2023-12-01

每个移动操作系统都提供了一个内置的 UI 工具包,其中包含各种小部件,这些小部件通常不是很可定制。Flutter 带有一个灵活的小部件系统,该系统实现了 Material Design 规范并激励移动应用程序开发人员创建具有未来感的最小 UI。

与特定于平台的 UI 小部件不同,Flutter 为每个通用需求提供了许多可定制的小部件选择,因此根据您独特的设计草图构建您的 Flutter 应用程序很容易。其中之一是ExpansionPanel小部件,它可以帮助我们创建可展开/可折叠的列表。

我们可以ExpansionPanel在一个小部件中添加多个小ExpansionPanelList部件,以在我们的 Flutter 应用程序中创建可展开/可折叠的列表。这些小部件有一个展开/折叠图标按钮,供用户显示/隐藏其他内容。Flutter 开发人员通常使用单独的详细信息屏幕来显示特定列表项的大型内容片段(即显示产品详细信息)。

该ExpansionPanel小部件可帮助开发人员显示每个列表项的中小型内容段,而无需屏幕导航。在 UI/UX 规范中,此 UI 元素可以称为 Accordion、Expandable 或 Collapsible。

ExpansionPanel在本教程中,我将通过实际示例解释如何使用和自定义小部件。此外,我们会将其与ExpansionTile提供类似功能的小部件进行比较。

跳跃前进:

  • 颤振ExpansionPanel教程

    • 与ExpansionPanel_ExpansionPanelList

  • 如何自定义ExpansionPanel小部件的 UI

    • 调整ExpansionPanel的动画和触摸反馈

    • ExpansionPanel根据小部件状态自动扩展

    • 一次展开和折叠所有项目

    • 创建无线电扩展面板ExpansionPanelRadio

  • 构建嵌套扩展面板

  • ExpansionPanel对比ExpansionTile

颤振ExpansionPanel教程

让我们创建一个新的 Flutter 项目来使用这个ExpansionPanel小部件。您还可以在现有的 Flutter 项目中使用此示例代码。

如果您是 Flutter 新手,请按照 Flutter 官方安装指南安装 Flutter 开发工具。您可以在 Google Chrome、物理移动设备或模拟器/模拟器上运行即将发布的示例。在本教程中,我将使用 Chrome 预览示例应用程序。

首先,使用以下命令创建一个新的 Flutter 应用程序:

flutter create expansionpanel_example
cd expansionpanel_example

输入flutter run命令以确保一切正常。

与ExpansionPanel_ExpansionPanelList

让我们创建一个简单的指南页面,用于创建一个包含多个ExpansionPanel小部件和一个小部件的 Flutter 应用程序ExpansionPanelList。用户可以点击特定步骤来展开它并查看更多详细信息。

在大多数情况下,我们通常通过具有异步函数的后端 Web 服务将数据加载到应用程序前端,但在我们的教程中,我们将从同步函数呈现硬编码数据以ExpansionPanel快速开始。

将以下代码添加到您的main.dart文件中:

import 'package:flutter/material.dart';
​
void main() => runApp(const MyApp());
​
class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
  static const String _title = 'Flutter Tutorial';
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: _title,
      home: Scaffold(
        appBar: AppBar(title: const Text(_title)),
        body: const Steps(),
      ),
    );
  }
}
​
class Step {
  Step(
    this.title,
    this.body,
    [this.isExpanded = false]
  );
  String title;
  String body;
  bool isExpanded;
}
​
List<Step> getSteps() {
  return [
    Step('Step 0: Install Flutter', 'Install Flutter development tools according to the official documentation.'),
    Step('Step 1: Create a project', 'Open your terminal, run `flutter create <project_name>` to create a new project.'),
    Step('Step 2: Run the app', 'Change your terminal directory to the project directory, enter `flutter run`.'),
  ];
}
​
class Steps extends StatefulWidget {
  const Steps({Key? key}) : super(key: key);
  @override
  State<Steps> createState() => _StepsState();
}
​
class _StepsState extends State<Steps> {
  final List<Step> _steps = getSteps();
  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      child: Container(
        child: _renderSteps(),
      ),
    );
  }
  Widget _renderSteps() {
    return ExpansionPanelList(
      expansionCallback: (int index, bool isExpanded) {
        setState(() {
          _steps[index].isExpanded = !isExpanded;
        });
      },
      children: _steps.map<ExpansionPanel>((Step step) {
        return ExpansionPanel(
          headerBuilder: (BuildContext context, bool isExpanded) {
            return ListTile(
              title: Text(step.title),
            );
          },
          body: ListTile(
            title: Text(step.body),
          ),
          isExpanded: step.isExpanded,
        );
      }).toList(),
    );
  }
}

请注意有关上述示例代码的以下事实:

  • 小Steps部件负责在屏幕上呈现整个可展开列表

  • 同步函数将getSteps所有硬编码的步骤作为Item类的实例返回,_steps小部件状态变量将所有项目保存为 DartList

  • 我们使用

    ExpansionPanelList

    类中的两个参数:

    • children通过转换列表来设置所有ExpansionPanel实例_steps

    • expansionCallback``_steps根据最近与展开/折叠按钮的用户交互来更新列表

  • 我们使用ListTile类而不是简单地使用Text来显示样式良好的材料列表

运行上面的代码。您将看到创建 Flutter 项目的步骤,如以下预览所示:

通过添加更多步骤来测试应用程序,或者尝试使用List.generate工厂构造函数生成一些动态数据。

如果您需要从 Web 后端加载数据,可以照常包装ExpansionPanelList小部件:FutureBuilder

import 'package:flutter/material.dart';
​
void main() => runApp(const MyApp());
​
class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
  static const String _title = 'Flutter Tutorial';
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: _title,
      home: Scaffold(
        appBar: AppBar(title: const Text(_title)),
        body: const Steps(),
      ),
    );
  }
}
​
class Step {
  Step(
    this.title,
    this.body,
    [this.isExpanded = false]
  );
  String title;
  String body;
  bool isExpanded;
}
​
Future<List<Step>> getSteps() async {
  var _items = [
    Step('Step 0: Install Flutter', 'Install Flutter development tools according to the official documentation.'),
    Step('Step 1: Create a project', 'Open your terminal, run `flutter create <project_name>` to create a new project.'),
    Step('Step 2: Run the app', 'Change your terminal directory to the project directory, enter `flutter run`.'),
  ];
  return Future<List<Step>>.delayed(const Duration(seconds: 2), () => _items);
}
​
class Steps extends StatelessWidget {
  const Steps({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      child: Container(
        child: FutureBuilder<List<Step>>(
          future: getSteps(),
          builder: (BuildContext context, AsyncSnapshot<List<Step>> snapshot) {
            if(snapshot.hasData) {
              return StepList(steps: snapshot.data ?? []);
            }
            else {
              return Center(
                child: Padding(
                  padding: EdgeInsets.all(16),
                  child: CircularProgressIndicator(),
                ),
              );
            }
          }
        ),
      ),
    );
  }
}
​
class StepList extends StatefulWidget {
  final List<Step> steps;
  const StepList({Key? key, required this.steps}) : super(key: key);
  @override
  State<StepList> createState() => _StepListState(steps: steps);
}
​
class _StepListState extends State<StepList> {
  final List<Step> _steps;
  _StepListState({required List<Step> steps}) : _steps = steps;
  @override
  Widget build(BuildContext context) {
    return ExpansionPanelList(
      expansionCallback: (int index, bool isExpanded) {
        setState(() {
          _steps[index].isExpanded = !isExpanded;
        });
      },
      children: _steps.map<ExpansionPanel>((Step step) {
        return ExpansionPanel(
          headerBuilder: (BuildContext context, bool isExpanded) {
            return ListTile(
              title: Text(step.title),
            );
          },
          body: ListTile(
            title: Text(step.body),
          ),
          isExpanded: step.isExpanded,
        );
      }).toList(),
    );
  }
}

我们对之前的源代码进行了三处更新,如下所述:

  1. 通过人工延迟使getSteps函数异步,因此现在您甚至可以通过您最喜欢的网络客户端库(即Dio)从 Web 服务获取可扩展列表的数据

  2. FutureBuilder通过创建名为 的第二个小部件来包装可扩展列表,该小部件StepList使用条件渲染在人工网络延迟期间显示循环加载动画

  3. 使Steps小部件无状态,因为我们在那里的状态中不保存任何数据

运行上面的代码——你会在两秒延迟后看到可展开的列表:

使用这两种方法中的任何一种,您都可以为需要使用ExpansionPanel小部件的任何情况提供解决方案。


超过 20 万开发人员使用 LogRocket 来创造更好的数字体验了解更多 →


现在,让我们研究一下ExpansionPanel提供的功能!在接下来的示例中,我们将更新同步版本,因为与异步版本相比,它的实现最少。将第一个示例源代码main.dart再次复制到您的文件中,然后准备继续学习本教程。

如何自定义ExpansionPanel小部件的 UI

当您使用ExpansionPanelwith 时ListTile,您将获得一个用户友好的可扩展列表,如我们在前面的示例中所见。您可以根据个人喜好或应用主题对其进行自定义。例如,您更改元素的背景颜色,如下所示:

return ExpansionPanel(
  //.....
  //...
  backgroundColor: const Color(0xffeeeeff),
);

您可以更改可扩展列表的分隔线颜色,如以下代码片段所示:

return ExpansionPanelList(
  dividerColor: Colors.teal,
  //....
  //...

也可以为标题设置自定义填充。看下面的例子:

return ExpansionPanelList(
  expandedHeaderPadding: EdgeInsets.all(6),
  //....
  //...

以下_renderSteps方法实现使用上述参数应用多个 UI 自定义。

Widget _renderSteps() {
    return ExpansionPanelList(
      dividerColor: Colors.teal,
      expandedHeaderPadding: EdgeInsets.all(0),
      expansionCallback: (int index, bool isExpanded) {
        setState(() {
          _steps[index].isExpanded = !isExpanded;
        });
      },
      children: _steps.map<ExpansionPanel>((Step step) {
        return ExpansionPanel(
          backgroundColor: const Color(0xffeeeeff),
          headerBuilder: (BuildContext context, bool isExpanded) {
            return ListTile(
              title: Text(step.title),
            );
          },
          body: ListTile(
            title: Text(step.body),
          ),
          isExpanded: step.isExpanded,
        );
      }).toList(),
    );
  }

现在,您将看到一个自定义的可展开列表 UI,如以下预览所示:

调整ExpansionPanel的动画和触摸反馈

Flutter 小部件系统可让您更改ExpansionPanel动画的速度。例如,您可以通过延长动画持续时间来减慢其动画速度,如下所示:

return ExpansionPanelList(
  animationDuration: const Duration(milliseconds: 1500),
  //....
  //...

ExpansionPanel只有当用户点击右侧图标按钮时,小部件才会打开/关闭内容部分,但如果您使用以下设置,用户可以通过点击整个标题部分来执行相同的操作:

return ExpansionPanel(
  canTapOnHeader: true,
  //...
  //..

如果您的应用程序用户通常使用小屏幕设备,则此配置是一个很好的用户体验改进——他们不需要点击小展开/折叠图标按钮来激活展开/折叠操作。

ExpansionPanel根据小部件状态自动扩展

在前面的例子中,我们在isExpanded类中使用了类变量Step,但我们没有从getSteps函数中显式地为它设置一个值。我们得到的只是扩展面板一开始就塌了。

ExpansionPanel我们可以为类的参数设置一个初始值isExpanded来设置一个自动扩展的项目。使用以下同步getSteps函数实现:

List<Step> getSteps() {
  return [
    Step('Step 0: Install Flutter',
        'Install Flutter development tools according to the official documentation.',
        true),
    Step('Step 1: Create a project', 'Open your terminal, run `flutter create <project_name>` to create a new project.'),
    Step('Step 2: Run the app', 'Change your terminal directory to the project directory, enter `flutter run`.'),
  ];
}

在这里,我们在第一个列表元素中设置true了 for 。isExpanded在方法中找到以下代码行_renderSteps:

isExpanded: step.isExpanded,

上面的行isExpanded从Step实例传递到ExpansionPanel,所以现在我们可以看到第一个面板最初是自动扩展的:

同样,您甚至可以从 Web 后端控制最初打开的面板!

一次展开和折叠所有项目

您是否注意到,在某些应用程序中,我们可以通过一个按钮一次展开/折叠所有可展开的段?如果用户需要在不点击每个扩展面板的情况下阅读所有隐藏内容,此功能会很有帮助。使用以下build方法实现_StepsState:

@override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      child: Column(
        children: <Widget>[
          Padding(
            padding: EdgeInsets.all(12),
            child: ElevatedButton(
              child: const Text('Expand all'),
              onPressed: () {
                setState(() {
                  for(int i = 0; i < _steps.length; i++) {
                    _steps[i].isExpanded = true;
                  }
                });
              },
            ),
          ),
          _renderSteps()
        ],
      ),
    );
  }

在这里,我们创建了一个按钮来一次展开所有面板。setState方法调用设置isExpanded为所有列表项实例,因此true一旦您点击按钮,所有步骤都会展开如下:

isExpanded同样,您可以通过将参数设置为来实现折叠所有面板的按钮false:

_steps[i].isExpanded = false;

创建无线电扩展面板ExpansionPanelRadio

默认ExpansionPanelList小部件的行为类似于一组复选框,因此当我们点击一个面板时,该特定面板会展开,我们必须再次单击它才能折叠它。

但是,如果我们需要构建一个行为类似于一组单选按钮的可扩展列表怎么办?我们只能保持一个面板展开,就像复选框组一样。

作为一种解决方案,您可能会考虑编写一些自定义逻辑来更新_steps列表,就像我们实现展开/折叠所有功能的方式一样,但 Flutter 小部件系统实际上ExpansionPanelRadio为此提供了内置功能。


使用以下代码实现_renderSteps函数:

Widget _renderSteps() {
    return ExpansionPanelList.radio(
      children: _steps.map<ExpansionPanelRadio>((Step step) {
        return ExpansionPanelRadio(
          headerBuilder: (BuildContext context, bool isExpanded) {
            return ListTile(
              title: Text(step.title),
            );
          },
          body: ListTile(
            title: Text(step.body),
          ),
          value: step.title
        );
      }).toList(),
    );
  }

在这里,我们使用了ExpansionPanelRadio带有ExpansionPanelList.radio构造函数的小部件。小ExpansionPanelRadio部件不接受isExpanded参数ExpansionPanel;相反,它接受带有value参数的唯一值。此外,我们不需要调用setStatefrom expansionCallback,因为 Flutter 框架提供了一个内置实现,可以在用户打开另一个面板时自动折叠打开的面板。

使用上面的代码片段后,您将看到以下结果:

如果您最初需要打开特定面板,您可以使用您通过value参数添加的唯一标识符来执行此操作,如下所示:

return ExpansionPanelList.radio(
  initialOpenPanelValue: 'Step 0: Install Flutter',
  //....
  //...

请注意,这里我们使用项目标题字符串作为唯一value用于演示目的。对于生产应用程序,请确保使用更好的唯一值,例如产品标识符。

构建嵌套扩展面板

在大多数应用中,对扩展面板使用一层就足够了,例如我们之前的示例。但是当你用 Flutter 开发复杂的应用程序(即桌面应用程序)时,多多配音App,文字转语音AI合成技术,去广告解锁会员高级版!有时你需要添加嵌套的扩展面板。

Flutter 小部件系统非常灵活,可以让您创建嵌套的扩展面板。但是,我们如何定义一个模型来保存一个扩展面板的数据?

我们确实可以对Step类使用递归定义,如下所示:

class Step {
  Step(
    this.title,
    this.body,
    [this.subSteps = const <Step>[]]
  );
  String title;
  String body;
  List<Step> subSteps;
}

现在,我们可以使用subSteps列表来渲染嵌套的扩展面板集。以下示例代码向我们的 Flutter 教程应用程序添加了另一个步骤,其中包含两个子步骤:

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
  static const String _title = 'Flutter Tutorial';
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: _title,
      home: Scaffold(
        appBar: AppBar(title: const Text(_title)),
        body: const Steps(),
      ),
    );
  }
}

class Step {
  Step(
    this.title,
    this.body,
    [this.subSteps = const <Step>[]]
  );
  String title;
  String body;
  List<Step> subSteps;
}

List<Step> getSteps() {
  return [
    Step('Step 0: Install Flutter', 'Install Flutter development tools according to the official documentation.'),
    Step('Step 1: Create a project', 'Open your terminal, run `flutter create <project_name>` to create a new project.'),
    Step('Step 2: Run the app', 'Change your terminal directory to the project directory, enter `flutter run`.'),
    Step('Step 3: Build your app', 'Select a tutorial:', [
      Step('Developing a to-do app', 'Add a link to the tutorial video'),
      Step('Developing a 2-D game', 'Add a link to the tutorial video'),
    ]),
  ];
}

class Steps extends StatefulWidget {
  const Steps({Key? key}) : super(key: key);
  @override
  State<Steps> createState() => _StepsState();
}

class _StepsState extends State<Steps> {
  final List<Step> _steps = getSteps();
  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      child: Container(
          child: _renderSteps(_steps)
      ),
    );
  }

  Widget _renderSteps(List<Step> steps) {
    return ExpansionPanelList.radio(
      children: steps.map<ExpansionPanelRadio>((Step step) {
        return ExpansionPanelRadio(
          headerBuilder: (BuildContext context, bool isExpanded) {
            return ListTile(
              title: Text(step.title),
            );
          },
          body: ListTile(
            title: Text(step.body),
            subtitle: _renderSteps(step.subSteps)
          ),
          value: step.title
        );
      }).toList(),
    );
  }
}

在这里,我们_renderSteps递归调用该方法step.subSteps来渲染嵌套的扩展面板。运行上述代码后,您将看到最后一步的子步骤,如下所示:

上面的例子只渲染了两级嵌套扩展面板,但是递归方法支持的更多,那么如何修改getSteps方法源来显示三级扩展面板呢?通过将子步骤传递给开发待办事项应用程序步骤,您可以轻松添加另一个扩展级别。

ExpansionPanel对比ExpansionTile

我们已经测试了ExpansionPanel提供的所有功能。接下来,让我们将它与类似的小部件进行比较,并讨论何时需要使用它们。查看下表与 进行ExpansionPanel比较ExpansionTile:

比较因子ExpansionPanelExpansionTile
推荐的父小部件ExpansionPanelList只要ListView, Column, Drawer, 或任何可以容纳单个或多个小部件的容器类型小部件
添加内容/正文的支持方式通过参数接受单个小部件(通常是 a ListTile)body通过参数接受多个小部件(通常是ListTiles)children
预定义样式不提供标题和内容的预定义样式 - 开发人员必须使用ListTile小部件来根据 Material 规范实现扩展列表。它还呈现一个不可自定义的箭头图标。通过让开发人员设置标题和副标题来为标题提供预定义的样式,因为这个小部件作为ListTile
支持的 UI 自定义为基于展开状态的动态渲染提供 header builder 功能。无法自定义箭头图标,但默认图标 ( ExpandIcon) 符合 Material 规范。能够设置自定义扩展图标,更改图标位置,并添加前导/尾随小部件
使用异步数据源渲染像往常一样,可能有一个FutureBuilder实例像往常一样,可能有一个FutureBuilder实例

根据上面的比较,我们可以理解,ExpansionPanel它更像是一个用户可以展开/折叠的内容小部件,因此我们可以使用它来显示有关特定产品的更多详细信息,而无需导航到第二个屏幕。ExpansionPanelRadio此外,您可以通过将小部件与小部件分组并一次显示一组小部件来简化复杂的应用程序屏幕。

另一方面,ExpansionTile是一个适合创建子列表的小部件,因为您可以直接ListTile在children参数中使用多个 s。例如,您可以使用ExpansionTile小部件实现设置面板或子菜单。请参阅flutter_settings_screens实现以了解有关使用 实现设置面板的更多信息ExpansionTile。

结论

ExpansionPanel在本教程中,我们通过根据各种需求修改一个实际示例来学习如何在 Flutter 中使用小部件。您可以使用此小部件根据 Material Design 规范创建可扩展的详细信息部分。

ExpansionPanel满足添加可扩展列表的通用 UI 要求,但正如我们在比较部分中注意到的那样,与ExpansionTile. 但是,它遵循 Material 规范,因此通常我们不需要高级定制,ExpansionPanel因为它提供了一个出色的、对开发人员友好的可扩展列表设计以及ListTile.

如果您在使用ExpansionPanelor时遇到限制ExpansionTile,可以查看flutter-expandable社区包。它以更灵活的方式提供了两者的组合功能ExpansionPanel。ExpansionTile

Flutter 提供了数百个内置小部件,Flutter 开发团队努力根据开发人员的反馈改进现有的小部件,以便他们可以引入可能导致替代的、基于社区的小部件被弃用的新功能或改进。因此,使用原生小部件,如ExpansionPanel并ExpansionTile使您的应用程序稳定并符合 Material Design 规范。

尝试使用 ! 实现下一个应用原型的可扩展列表ExpansionPanel!

 类似资料: