为了账号安全,请及时绑定邮箱和手机立即绑定

Flutter ExpansionPanel 超级实用展开控件

标签:
Html5

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

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

图片描述

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

ExpansionPanel

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

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

A material expansion panel. It has a header and a body and can be either expanded or collapsed. The body of the panel is only visible when it is expanded.

Expansion panels are only intended to be used as children for ExpansionPanelList.

一个material 扩展面板。它有一个 header 和一个 body ,可以展开或折叠。面板的 body 仅在展开时可见。

扩展面板仅用作于 ExpansionPanelList。

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

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

1.  `ExpansionPanel({`
    
2.   `@required  this.headerBuilder,`
    
3.   `@required  this.body,`
    
4.   `this.isExpanded =  false,`
    
5.   `this.canTapOnHeader =  false,`
    
6.  `})  :  assert(headerBuilder !=  null),`
    
7.  `assert(body !=  null),`
    
8.  `assert(isExpanded !=  null),`
    
9.  `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 展开面板列表,用于设置其子项并为展开设置动画。

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

1.  `const  ExpansionPanelList({`
    
2.   `Key key,`
    
3.   `this.children =  const  <ExpansionPanel>[],`
    
4.   `this.expansionCallback,`
    
5.   `this.animationDuration = kThemeAnimationDuration,`
    
6.  `})  :  assert(children !=  null),`
    
7.  `assert(animationDuration !=  null),`
    
8.  `_allowOnlyOnePanelOpen =  false,`
    
9.  `initialOpenPanelValue =  null,`
    
10.  `super(key: key);`
    

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

  • children:不用多说,就是 ExpansionPanel

  • expansionCallback:展开回调,这里会返回点击的 index

  • animationDuration:动画的时间

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

官方Demo

效果如下:

图片描述

来看下代码:

1.  `class  Item  {`
    
2.   `Item({`
    
3.   `this.expandedValue,`
    
4.   `this.headerValue,`
    
5.   `this.isExpanded =  false,`
    
6.   `});`
    
7.    
    
8.   `String expandedValue;`
    
9.   `String headerValue;`
    
10.   `bool isExpanded;`
    
11.  `}`
    
12.    
    
13.  `List<Item> generateItems(int numberOfItems)  {`
    
14.   `return  List.generate(numberOfItems,  (int index)  {`
    
15.   `return  Item(`
    
16.   `headerValue:  'Panel $index',`
    
17.   `expandedValue:  'This is item number $index',`
    
18.   `);`
    
19.   `});`
    
20.  `}`
    
21.    
    
22.  `class  ExpansionPanelPage  extends  StatefulWidget  {`
    
23.   `ExpansionPanelPage({Key key})  :  super(key: key);`
    
24.    
    
25.   `@override`
    
26.   `_ExpansionPanelPageState createState()  =>  _ExpansionPanelPageState();`
    
27.  `}`
    
28.    
    
29.  `class  _ExpansionPanelPageState  extends  State<ExpansionPanelPage>  {`
    
30.   `List<Item> _data = generateItems(8);`
    
31.    
    
32.   `@override`
    
33.   `Widget build(BuildContext context)  {`
    
34.   `return  Scaffold(`
    
35.   `appBar:  AppBar(`
    
36.   `title:  Text('ExpansionPanelPage'),`
    
37.   `),`
    
38.   `body:  SingleChildScrollView(`
    
39.   `child:  Container(`
    
40.   `child: _buildPanel(),`
    
41.   `),`
    
42.   `),`
    
43.   `);`
    
44.   `}`
    
45.    
    
46.   `Widget _buildPanel()  {`
    
47.   `return  ExpansionPanelList(`
    
48.   `expansionCallback:  (int index,  bool isExpanded)  {`
    
49.   `setState(()  {`
    
50.   `_data[index].isExpanded =  !isExpanded;`
    
51.   `});`
    
52.   `},`
    
53.   `children: _data.map<ExpansionPanel>((Item item)  {`
    
54.   `return  ExpansionPanel(`
    
55.   `headerBuilder:  (BuildContext context,  bool isExpanded)  {`
    
56.   `return  ListTile(`
    
57.   `title:  Text(item.headerValue),`
    
58.   `);`
    
59.   `},`
    
60.   `body:  ListTile(`
    
61.   `title:  Text(item.expandedValue),`
    
62.   `subtitle:  Text('To delete this panel, tap the trash can icon'),`
    
63.   `trailing:  Icon(Icons.delete),`
    
64.   `onTap:  ()  {`
    
65.   `setState(()  {`
    
66.   `_data.removeWhere((currentItem)  => item == currentItem);`
    
67.   `});`
    
68.   `}),`
    
69.   `isExpanded: item.isExpanded,`
    
70.   `);`
    
71.   `}).toList(),`
    
72.   `);`
    
73.   `}`
    
74.  `}`

从上往下看。

Item

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

  • expandedValue:展开的值

  • headerValue:header的值

  • isExpanded:是否已经展开

generateItems

生成指定数量的 Item

_ExpansionPanelPageState

重点来了,看build 方法:

1.  `@override`
    
2.  `Widget build(BuildContext context)  {`
    
3.   `return  Scaffold(`
    
4.   `appBar:  AppBar(`
    
5.   `title:  Text('ExpansionPanelPage'),`
    
6.   `),`
    
7.   `body:  SingleChildScrollView(`
    
8.   `child:  Container(`
    
9.   `child: _buildPanel(),`
    
10.   `),`
    
11.   `),`
    
12.   `);`
    
13.  `}`

_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 参数:

1.  `ExpansionPanel(`
    
2.   `canTapOnHeader:  true,`
    
3.   `headerBuilder: xxx,`
    
4.   `body: xxx;`
    
5.  `)`

效果如下:

图片描述

body is ListView

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

图片描述

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

1.  `return  ListView.builder(`
    
2.   `shrinkWrap:  true,`
    
3.   `physics:  NeverScrollableScrollPhysics(),`
    
4.  `);`
    

只能展开一个

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

效果如下:

图片描述

代码如下,需使用 ExpansionPanelList.radio

1.  `Widget _buildPanel()  {`
    
2.   `return  ExpansionPanelList.radio(`
    
3.   `expansionCallback:  (int index,  bool isExpanded)  {`
    
4.   `setState(()  {`
    
5.   `_data[index].isExpanded =  !isExpanded;`
    
6.   `});`
    
7.   `},`
    
8.   `children: _data.map<ExpansionPanel>((Item item)  {`
    
9.   `return  ExpansionPanelRadio(`
    
10.   `canTapOnHeader:  true,`
    
11.   `headerBuilder:  (BuildContext context,  bool isExpanded)  {`
    
12.   `return  ListTile(`
    
13.   `title:  Text(item.headerValue),`
    
14.   `);`
    
15.   `},`
    
16.   `body:  ListTile(`
    
17.   `title:  Text(item.expandedValue),`
    
18.   `subtitle:  Text('To delete this panel, tap the trash can icon'),`
    
19.   `trailing:  Icon(Icons.delete),`
    
20.   `onTap:  ()  {`
    
21.   `setState(()  {`
    
22.   `_data.removeWhere((currentItem)  => item == currentItem);`
    
23.   `});`
    
24.   `}),`
    
25.   `value: item.headerValue,`
    
26.   `);`
    
27.   `}).toList(),`
    
28.   `);`
    
29.  `}`
    

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

ExpansionPanelRadioExpansionPanel 的区别就是一个 value。

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

总结

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

而且 ExpansionPanelList 返回的是一个 MergeableMaterial,

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

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
移动开发工程师
手记
粉丝
30
获赞与收藏
45

关注作者,订阅最新文章

阅读免费教程

  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消