在實際業務開發過程當中,或多或少會遇到樹形控件的需求。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/…