Flutter入门(36):Flutter 组件之 ExpansionPanel、ExpansionPanelRadio、ExpansionPanelList 详解

1. 基本介绍

ExpansionPanel、ExpansionPanelRadio 是一种常见的折叠框。
ExpansionPanelList 是承载折叠框的一个父类控件。

2. 示例代码

代码下载地址。如果对你有帮助的话记得给个关注,代码会根据我的 Flutter 专题不断更新。

3. 属性介绍

ExpansionPanel 属性 介绍
headerBuilder @required,Header Widget 构造方法
body @required,展开部分 Widget
isExpanded 是否展开,默认为 false
canTapOnHeader 是否可以点击 header 用来展开收起,false
ExpansionPanelRadio属性 介绍
value @required 唯一标识
headerBuilder @required Header Widget 构造方法
body @required,展开部分 Widget
canTapOnHeader 是否可以点击 header 用来展开收起,false
ExpansionPanelList属性 介绍
children 子控件数组,类型为 <ExpansionPanel> 的数组
expansionCallback 点击折叠收起回调函数,(index, isExpand){},返回当前下标以及是否折叠
animationDuration 动画时间,默认为 kThemeAnimationDuration
expandedHeaderPadding 展开后 Header 的 padding,默认为 _kPanelHeaderExpandedDefaultPadding
dividerColor 分割线颜色

ExpansionPanelList.radio 属性 | 介绍
children | 子控件数组,类型为 <ExpansionPanelRadio> 的数组
expansionCallback | 点击折叠收起回调函数,(index, isExpand){},返回当前下标以及是否折叠
initialOpenPanelValue | 当前选中标识,initialOpenPanelValue == ExpansionPanelRadio.value 时,该 ExpansionPanelRadio 会默认展开
animationDuration | 动画时间,默认为 kThemeAnimationDuration
expandedHeaderPadding | 展开后 Header 的 padding,默认为 _kPanelHeaderExpandedDefaultPadding
dividerColor | 分割线颜色

4. ExpansionPanel,ExpansionPanelList 详解

4.1 代码实现

ExpansionPanel 是单个折叠框,他的效果实现还需要依托于 ExpansionPanelList,这边直接上 demo,一个简单的折叠框的实现。

import 'package:flutter/material.dart';

class FMExpansionPanelVC extends StatefulWidget{
  @override
  FMExpansionPanelState createState() => FMExpansionPanelState();
}

class FMExpansionPanelState extends State <FMExpansionPanelVC>{
  List <ExpansionPanelModel> _models = [];
  List <ExpansionPanel> _childrenForExpansionPanel = [];
  List <ExpansionPanelRadio> _childrenForExpansionPanelRadio = [];

  @override
  void initState(){
    super.initState();

    _initData();
  }

  void _initData(){
    _models.clear();
    _models.add(ExpansionPanelModel("EP1", "Title EP1", false));
    _models.add(ExpansionPanelModel("EP2", "Title EP2", false));
    _models.add(ExpansionPanelModel("EP3", "Title EP3", false));
    _models.add(ExpansionPanelModel("EP4", "Title EP4", false));
    _models.add(ExpansionPanelModel("EP5", "Title EP5", false));

    print("initDate");
  }

  void _initChildrenForExpansionPanel(){
    _childrenForExpansionPanel.clear();
    _models.forEach((model) {
      _childrenForExpansionPanel.add(_expansionPanel(model));
    });
  }

  void _initChildrenForExpansionPanelRadio(){
    _childrenForExpansionPanelRadio.clear();
    _models.forEach((model) {
      _childrenForExpansionPanelRadio.add(_expansionPanelRadio(model));
    });

    print(_childrenForExpansionPanelRadio);
  }

  Widget build(BuildContext context) {
    // TODO: implement build
    return Scaffold(
      appBar: AppBar(
        title: Text("ExpansionPanel"),
      ),
      body: SingleChildScrollView(
        child: _expansionPanelList(),
      ),
    );
  }

  ExpansionPanelList _expansionPanelList(){

    _initChildrenForExpansionPanel();

    return ExpansionPanelList(
      expansionCallback: (index, isExpand){
        _models[index].isExpanded = !_models[index].isExpanded;
        print(_models[index].isExpanded);
        setState(() {

        });
      },
      dividerColor: Colors.black,
      expandedHeaderPadding: EdgeInsets.zero,
      children: _childrenForExpansionPanel,
    );
  }

  ExpansionPanel _expansionPanel(ExpansionPanelModel model){
    return ExpansionPanel(
      headerBuilder: (context, boolValue){
        return Container(
          height: 80,
          alignment: Alignment.centerLeft,
          child: Text("${model.title}"),
        );
      },
      isExpanded: model.isExpanded,
      canTapOnHeader: true,
      body: Container(
        height: 200,
        color: Colors.red,
      ),
    );
  }

  ExpansionPanelList _expansionPanelListRadio(){

    _initChildrenForExpansionPanelRadio();

    return ExpansionPanelList.radio(
      expansionCallback: (index, isExpand){
        _models[index].isExpanded = !_models[index].isExpanded;
        print(_models[index].isExpanded);
        setState(() {

        });
      },
      dividerColor: Colors.black,
      expandedHeaderPadding: EdgeInsets.zero,
      children: _childrenForExpansionPanelRadio,
    );
  }

  ExpansionPanelRadio _expansionPanelRadio(ExpansionPanelModel model){
    return ExpansionPanelRadio(
      value: model.value,
      headerBuilder: (context, boolValue){
        return Container(
          height: 80,
          alignment: Alignment.centerLeft,
          child: Text("${model.title}"),
        );
      },
      canTapOnHeader: true,
      body: Container(
        height: 200,
        color: Colors.red,
      ),
    );
  }
}

class ExpansionPanelModel {
  var value;
  String title;
  bool isExpanded;

  ExpansionPanelModel(this.value, this.title, this.isExpanded);
}
ExpansionPanel normal.gif

4.2 ExpansionPanelList 常见属性效果

  ExpansionPanelList _expansionPanelList(){

    _initChildrenForExpansionPanel();

    return ExpansionPanelList(
      expansionCallback: (index, isExpand){
        _models[index].isExpanded = !_models[index].isExpanded;
        print(_models[index].isExpanded);
        setState(() {

        });
      },
      dividerColor: Colors.green,
      expandedHeaderPadding: EdgeInsets.all(30),
      children: _childrenForExpansionPanel,
    );
  }

我们设置 dividerColor 为 green。

dividerColor.png

我们设置 expandedHeaderPadding 为 EdgeInsets.all(30),可以看到展开后 Header 的 Text 组件的位置做出了相应改变。

expandedHeaderPadding.png

5. ExpansionPanelRadio、ExpansionPanelList.radio 详解

5.1 代码实现

相比上方 ExpansionPanel、ExpansionPanelList 其实只是换一种实现方式,他们唯一的差别是使用 Radio 方式,可以为设置默认展开其中一个 ExpansionPanelRadio。

 import 'package:flutter/material.dart';

class FMExpansionPanelVC extends StatefulWidget{
  @override
  FMExpansionPanelState createState() => FMExpansionPanelState();
}

class FMExpansionPanelState extends State <FMExpansionPanelVC>{
  List <ExpansionPanelModel> _models = [];
  List <ExpansionPanel> _childrenForExpansionPanel = [];
  List <ExpansionPanelRadio> _childrenForExpansionPanelRadio = [];

  @override
  void initState(){
    super.initState();

    _initData();
  }

  void _initData(){
    _models.clear();
    _models.add(ExpansionPanelModel("EP1", "Title EP1", false));
    _models.add(ExpansionPanelModel("EP2", "Title EP2", false));
    _models.add(ExpansionPanelModel("EP3", "Title EP3", false));
    _models.add(ExpansionPanelModel("EP4", "Title EP4", false));
    _models.add(ExpansionPanelModel("EP5", "Title EP5", false));

    print("initDate");
  }

  void _initChildrenForExpansionPanel(){
    _childrenForExpansionPanel.clear();
    _models.forEach((model) {
      _childrenForExpansionPanel.add(_expansionPanel(model));
    });
  }

  void _initChildrenForExpansionPanelRadio(){
    _childrenForExpansionPanelRadio.clear();
    _models.forEach((model) {
      _childrenForExpansionPanelRadio.add(_expansionPanelRadio(model));
    });

    print(_childrenForExpansionPanelRadio);
  }

  Widget build(BuildContext context) {
    // TODO: implement build
    return Scaffold(
      appBar: AppBar(
        title: Text("ExpansionPanel"),
      ),
      body: SingleChildScrollView(
        // child: _expansionPanelList(),
        child: _expansionPanelListRadio(),
      ),
    );
  }

  ExpansionPanelList _expansionPanelList(){

    _initChildrenForExpansionPanel();

    return ExpansionPanelList(
      expansionCallback: (index, isExpand){
        _models[index].isExpanded = !_models[index].isExpanded;
        print(_models[index].isExpanded);
        setState(() {

        });
      },
      dividerColor: Colors.black,
      expandedHeaderPadding: EdgeInsets.zero,
      children: _childrenForExpansionPanel,
    );
  }

  ExpansionPanel _expansionPanel(ExpansionPanelModel model){
    return ExpansionPanel(
      headerBuilder: (context, boolValue){
        return Container(
          height: 80,
          alignment: Alignment.centerLeft,
          child: Text("${model.title}"),
        );
      },
      isExpanded: model.isExpanded,
      canTapOnHeader: true,
      body: Container(
        height: 200,
        color: Colors.red,
      ),
    );
  }

  ExpansionPanelList _expansionPanelListRadio(){

    _initChildrenForExpansionPanelRadio();

    return ExpansionPanelList.radio(
      expansionCallback: (index, isExpand){
        _models[index].isExpanded = !_models[index].isExpanded;
        print(_models[index].isExpanded);
        setState(() {

        });
      },
      dividerColor: Colors.black,
      expandedHeaderPadding: EdgeInsets.zero,
      children: _childrenForExpansionPanelRadio,
    );
  }

  ExpansionPanelRadio _expansionPanelRadio(ExpansionPanelModel model){
    return ExpansionPanelRadio(
      value: model.value,
      headerBuilder: (context, boolValue){
        return Container(
          height: 80,
          alignment: Alignment.centerLeft,
          child: Text("${model.title}"),
        );
      },
      canTapOnHeader: true,
      body: Container(
        height: 200,
        color: Colors.red,
      ),
    );
  }
}

class ExpansionPanelModel {
  var value;
  String title;
  bool isExpanded;

  ExpansionPanelModel(this.value, this.title, this.isExpanded){

  }
}
ExpansionPanel radio.gif

5.2 设置默认展开 ExpansionPanelRadio

它的原理其实很简单,设置 ExpansionPanelRadio.value,可以是字符串,也可以是数字,作为唯一识别标识。注意每个 value 不可以重复,否则会报错。

All ExpansionPanelRadio identifier values must be unique.
'package:flutter/src/material/expansion_panel.dart':
Failed assertion: line 385 pos 14: '_allIdentifiersUnique()'

我们对示例代码进行以下改动,然后重新进入页面,此处热重载不会更新页面。

  ExpansionPanelList _expansionPanelListRadio(){

    _initChildrenForExpansionPanelRadio();

    return ExpansionPanelList.radio(
      expansionCallback: (index, isExpand){
        _models[index].isExpanded = !_models[index].isExpanded;
        setState(() {

        });
      },
      initialOpenPanelValue: _models[2].value,
      dividerColor: Colors.black,
      expandedHeaderPadding: EdgeInsets.zero,
      children: _childrenForExpansionPanelRadio,
    );
  }
ExpansionPanelRadio initialOpenPanelValue.png

6. 技术小结

  • 注意 ExpansionPanel 不可以直接作为子控件给到 body,否则会报错,这里用了 SingleChildScrollView 作为外层容器,使用 Column、ListView...等很多控件都可以。
  • 注意使用 ExpansionPanelList() 与 ExpansionPanelList.radio() 创建出来的 Widget,他们的 children 分别对应 <ExpansionPanel>,<ExpansionPanelRadio>,此处容易报错。
  • 注意 ExpansionPanelRadio.value 不可以相同,容易报错。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345