您的当前位置:首页正文

Flutter ExpansionPanel 超级实用展开控件

2024-11-13 来源:个人技术集锦

在实际业务开发过程中,或多或少会遇到树形控件的需求。

最简单的需求比如 QQ 联系人的分组:

类似于这种,Flutter 给我们提供了相当便捷的 UI 组件 ExpansionPanel。

ExpansionPanel

看名字也能看出来,是一个"扩展面板"。

那按照惯例,我们首先打开官网,查看一下它的说明:

看说明也就能明白了,它不单独使用,只能和 ExpansionPanelList 配合使用。

那我们点进源码看一下构造函数:

ExpansionPanel({
  @required this.headerBuilder,
  @required this.body,
  this.isExpanded = false,
  this.canTapOnHeader = false,
}) : assert(headerBuilder != null),
assert(body != null),
assert(isExpanded != null),
assert(canTapOnHeader != null);
复制代码

一共有四个参数:

  • headerBuilder:header
  • body:body
  • isExpanded:是否展开
  • canTapOnHeader:header是否可以点击

看完了 ExpansionPanel 的构造函数,下面就看一下 ExpansionPanelList

ExpansionPanelList

照例先看它的介绍:

A material expansion panel list that lays out its children and animates expansions.

material 展开面板列表,用于设置其子项并为展开设置动画。

然后打开源码查看构造函数:

const ExpansionPanelList({
  Key key,
  this.children = const <ExpansionPanel>[],
  this.expansionCallback,
  this.animationDuration = kThemeAnimationDuration,
}) : assert(children != null),
assert(animationDuration != null),
_allowOnlyOnePanelOpen = false,
initialOpenPanelValue = null,
super(key: key);
复制代码

需要我们使用的也就三个参数:

  • children:不用多说,就是 ExpansionPanel
  • expansionCallback:展开回调,这里会返回点击的 index
  • animationDuration:动画的时间

基本上看完构造函数,我们也就知道该怎么去写代码了,那官方也提供给我们了一个 Demo。

官方Demo

效果如下:

来看下代码:

class Item {
  Item({
    this.expandedValue,
    this.headerValue,
    this.isExpanded = false,
  });

  String expandedValue;
  String headerValue;
  bool isExpanded;
}

List<Item> generateItems(int numberOfItems) {
  return List.generate(numberOfItems, (int index) {
    return Item(
      headerValue: 'Panel $index',
      expandedValue: 'This is item number $index',
    );
  });
}

class ExpansionPanelPage extends StatefulWidget {
  ExpansionPanelPage({Key key}) : super(key: key);

  @override
  _ExpansionPanelPageState createState() => _ExpansionPanelPageState();
}

class _ExpansionPanelPageState extends State<ExpansionPanelPage> {
  List<Item> _data = generateItems(8);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('ExpansionPanelPage'),
      ),
      body: SingleChildScrollView(
        child: Container(
          child: _buildPanel(),
        ),
      ),
    );
  }

  Widget _buildPanel() {
    return ExpansionPanelList(
      expansionCallback: (int index, bool isExpanded) {
        setState(() {
          _data[index].isExpanded = !isExpanded;
        });
      },
      children: _data.map<ExpansionPanel>((Item item) {
        return ExpansionPanel(
          headerBuilder: (BuildContext context, bool isExpanded) {
            return ListTile(
              title: Text(item.headerValue),
            );
          },
          body: ListTile(
              title: Text(item.expandedValue),
              subtitle: Text('To delete this panel, tap the trash can icon'),
              trailing: Icon(Icons.delete),
              onTap: () {
                setState(() {
                  _data.removeWhere((currentItem) => item == currentItem);
                });
              }),
          isExpanded: item.isExpanded,
        );
      }).toList(),
    );
  }
}
复制代码

从上往下看。

Item

首先定义了一个 Item 类,里面包含了:

  • expandedValue:展开的值
  • headerValue:header的值
  • isExpanded:是否已经展开

generateItems

生成指定数量的 Item

_ExpansionPanelPageState

重点来了,看build 方法:

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text('ExpansionPanelPage'),
    ),
    body: SingleChildScrollView(
      child: Container(
        child: _buildPanel(),
      ),
    ),
  );
}
复制代码

_buildPanel()方法就是根据 Item 的数量生成一个 ExpansionPanelList

那为什么要用 SingleChildScrollView 包起来?

我们先把 SingleChildScrollView 去掉来看一下效果:

发现什么都没有了,看一下log:

flutter: The following assertion was thrown during performLayout(): flutter: RenderListBody must have unlimited space along its main axis. flutter: RenderListBody does not clip or resize its children, so it must be placed in a parent that does not flutter: constrain the main axis. You probably want to put the RenderListBody inside a RenderViewport with a matching main axis.

大致意思就是说:

RenderListBody所在的主轴必须要有无线的空间,因为RenderListBody 要不断的调整children 的大小,所以必须把它放在不约束主轴的 parent 中。

在上面的gif图我们也能看出来,只有点击箭头才能展开,如果想要点击 header 也要展开的话,

使用 ExpansionPanel 的 canTapOnHeader 参数:

ExpansionPanel(
  canTapOnHeader: true,
  headerBuilder: xxx,
  body: xxx;
)
复制代码

效果如下:

body 为ListView

在我们实际业务中,可能最多的业务为展开是一个列表,那需要 body 是ListView。

其实和官方Demo差不多,需要注意的一点就是 shrinkWrap & physics 这两个字段:

return ListView.builder(
  shrinkWrap: true,
  physics: NeverScrollableScrollPhysics(),
);
复制代码

只能展开一个

有时我们也会遇到只能展开一个,点击其他的时候要关闭已经展开的。

效果如下:

代码如下,需使用 ExpansionPanelList.radio

Widget _buildPanel() {
  return ExpansionPanelList.radio(
    expansionCallback: (int index, bool isExpanded) {
      setState(() {
        _data[index].isExpanded = !isExpanded;
      });
    },
    children: _data.map<ExpansionPanel>((Item item) {
      return ExpansionPanelRadio(
        canTapOnHeader: true,
        headerBuilder: (BuildContext context, bool isExpanded) {
          return ListTile(
            title: Text(item.headerValue),
          );
        },
        body: ListTile(
          title: Text(item.expandedValue),
          subtitle: Text('To delete this panel, tap the trash can icon'),
          trailing: Icon(Icons.delete),
          onTap: () {
            setState(() {
              _data.removeWhere((currentItem) => item == currentItem);
            });
          }),
        value: item.headerValue,
      );
    }).toList(),
  );
}
复制代码

ExpansionPanelList.radio 的 children 也需要改变为:ExpansionPanelRadio

ExpansionPanelRadioExpansionPanel 的区别就是一个 value。

ExpansionPanelRadio 是用 value 来区分的,所以每一个要是唯一的。

总结

使用 ExpansionPanel 可以很轻松的实现展开效果,

而且 ExpansionPanelList 返回的是一个 MergeableMaterial,

所以想自定义UI的,也可以自己实现。

完整代码已经传至GitHub:

转载于:https://juejin.im/post/5d08a1d0f265da1bb27732ec

显示全文