在实际业务开发过程当中,或多或少会遇到树形控件的需求。html
最简单的需求好比 QQ 联系人的分组:git
相似于这种,Flutter 给咱们提供了至关便捷的 UI 组件 ExpansionPanel。github
看名字也能看出来,是一个"扩展面板"。api
那按照惯例,咱们首先打开官网,查看一下它的说明:app
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.ide
Expansion panels are only intended to be used as children for ExpansionPanelList.函数
一个material 扩展面板。它有一个 header 和一个 body ,能够展开或折叠。面板的 body 仅在展开时可见。动画
扩展面板仅用做于 ExpansionPanelList。ui
看说明也就能明白了,它不单独使用,只能和 ExpansionPanelList
配合使用。this
那咱们点进源码看一下构造函数:
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);
复制代码
一共有四个参数:
看完了 ExpansionPanel
的构造函数,下面就看一下 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);
复制代码
须要咱们使用的也就三个参数:
基本上看完构造函数,咱们也就知道该怎么去写代码了,那官方也提供给咱们了一个 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 类,里面包含了:
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。
其实和官方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
。
ExpansionPanelRadio
和 ExpansionPanel
的区别就是一个 value。
ExpansionPanelRadio
是用 value 来区分的,因此每个要是惟一的。
使用 ExpansionPanel 能够很轻松的实现展开效果,
并且 ExpansionPanelList 返回的是一个 MergeableMaterial,
因此想自定义UI的,也能够本身实现。
完整代码已经传至GitHub:github.com/wanglu1209/…