Flutter的列表和表格

# 需求

#####  最近Flutter项目中遇到一个稍显复杂的页面,主要是列表和表格的综合运用,感觉有必要总结一下。简单绘制了一下原型,大致要求如下:

-  页面可上下滑动

- 点击Tab标题时,Tab栏下方区域页面进行切换,且Tab栏滑动到顶部也就是紧挨着标题栏下方时,悬浮在顶部,用户再次向上滑动时,Tab下方页面进行上滑,而Tab栏固定不变

- Tab栏下方页面为表格数据,具体有多少列需要根据返回数据动态扩展,不固定。表格可上下左右滑动,左右滑动时,每一行的标题固定不变,仅滑动表格中的数据可滑动

![原型](https://img-blog.csdnimg.cn/20210129143337661.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM0MjAyMDU0,size_16,color_FFFFFF,t_70#pic_center)

# 分析与实践

### 根据需求分析,初步设计如下:

- 页面上下滑动,这里使用CustomScrollView,除了可上下滑动,其SliverAppBar可以根据滑动距离自动显示隐藏而且可以扩展指定高度,SliverToBoxAdapter可以根据上下滑动自然滑动,这两者都可以用来装Top Content;SliverFillRemaining则是让内容不滑出屏幕,符合Tab栏滑动到顶部之后悬浮的要求:

```java

CustomScrollView(

        slivers: <Widget>[

          SliverAppBar(

            expandedHeight:100.0,

            automaticallyImplyLeading: false,

            floating: true,

            snap: true,

            toolbarHeight: 0.0,

            elevation: 0.0,

            pinned: true,

            flexibleSpace: FlexibleSpaceBar(

                centerTitle: true,

                background: Column(

                  children: [

                  // Top content

                  ...

                      ],

                    ),

          ),

        ),

        //  SliverToBoxAdapter(

        //      child: ...,

        //  ),

          SliverFillRemaining(

            child: Column(

              children: [

              // Tab content

              ...

              ],

            ),

          ),

        ],

      ),

```

如果不想把较多的内容放在SliverAppBar的background(用来放置沉浸式效果的背景图片)中,可以将内容放在SliverToBoxAdapter中。其中SliverAppBar中toolbarHeigh设置0.0,是因为该页面中标题栏是自定义的,并不是和SliverAppBar联动的,即无法实现沉浸式效果,如果不设置0.0,当顶部内容滑出屏幕,Tab栏无法置顶,距离顶部还有toolbarHeigh(默认56.0)空白区域,若使用SliverToBoxAdapter则没有这种问题,只是相比较SliverAppBar滑动效果稍微生硬了一些,但不是卡顿,不影响使用。

-  点击Tab栏标题,Tab栏下方可进行切换,这里使用TabBar和TabBarView,效果和Android中TabLayout和ViewPager一样。这里还要在CustomScrollView外层再加上DefaultTabController,因为TabBar和TabBarView一般建议使用在Scaffold中的appBar和body中,使用比较死板,而加上DefaultTabController后可根据需要灵活使用:

```java

    return DefaultTabController(

      length: tabTitles.length,

      child: CustomScrollView(

      ...

            SliverFillRemaining(

            child: Column(

              children: [

                TabBar(

        controller: _tabController,

        tabs: titles

            .map((e) => Tab(

                  child: Text(

                    e,

                    style:

                        TextStyle(fontSize: 14.0, fontWeight: FontWeight.w500),

                  ),

                ))

            .toList(),

        isScrollable: false,

        labelColor: CommonColors.color_1376ee,

        indicatorColor: CommonColors.color_1376ee,

        unselectedLabelColor: CommonColors.color_66,

      ),

            Expanded(

                    flex: 1,

                    child: Container(

                      color: CommonColors.color_white,

                      child: TabBarView(

                        controller: tabController,

                        physics: NeverScrollableScrollPhysics(),

                        children: <Widget>[

                        ...

                        ],

                      ),

                    )),

              ],

            ),

          ),

      ),

    );

```

tabTitles就是Tab栏标题数组,TabBarView用Expanded包裹是为了防止出现屏幕溢出错误,使用NeverScrollableScrollPhysics()是不让TabBarView左右滑动。

- 表格区域可上下左右活动,上下滑动用ListView,因为每一行的标题悬浮的,所以在Row使用Expanded按比列划分屏幕的宽度。左边悬浮标题区域使用ListView且禁止滑动,右边内容区域也使用ListView,同样也禁止滑动,两次禁止滑动而外侧的ListView没有禁止滑动,这样就可以保证滑动标题和滑动数据内容的时候可以做到联动统一:

```java

ListView(

      children: [

        Row(

          children: [

            // 行名

            Expanded(

                child: ListView(

              physics: NeverScrollableScrollPhysics(),

              shrinkWrap: true,

              children: titles,

            )),

            Expanded(

              flex: 3,

              child: ListView(

                physics: NeverScrollableScrollPhysics(),

                shrinkWrap: true,

                children: [

                ...

                ],

              ),

            ),

          ],

        )

      ],

    );

```

- 数据行列不确定,这里使用DataTable,可以动态扩展列数:

```java

            Expanded(

              flex: 3,

              child: ListView(

                physics: NeverScrollableScrollPhysics(),

                shrinkWrap: true,

                children: [

                  SingleChildScrollView(

                    scrollDirection: Axis.horizontal,

                    child: DataTable(

                      dividerThickness: 0.0,

                      headingTextStyle: TextStyle(

                          fontSize: 14.0,

                          color: CommonColors.text_33,

                          fontWeight: FontWeight.w600),

                      sortAscending: false,

                      showBottomBorder: false,

                      showCheckboxColumn: false,

                      headingRowHeight: 32.0,

                      dataRowHeight: 36.0,

                      columns: dataColumns,

                      // 列名

                      rows: dataRows, // 数据

                    ),

                  )

                ],

              ),

            ),

```

使用SingleChildScrollView,设置Axis.horizontal实现表格左右滑动,DataTable是Flutter专门用来展示表格数据类似于Excel,功能比较多,像排序,全选,单选,点击,上下左右翻页等具备,详细使用请自行查看。

#  全部代码如下:

```java

class TableDemo extends StatefulWidget {

  @override

  _TableDemoState createState() => _TableDemoState();

}

class _TableDemoState extends State<TableDemo>

    with SingleTickerProviderStateMixin {

  TabController _tabController;

  List<String> _tabTitles = [

    "Tab1",

    "Tab2",

    "Tab3",

  ];

  List<DataColumn> _dataColumns = [];

  List<DataRow> _dataRows = [];

  List<Widget> _rowTitles = [];

  @override

  void initState() {

    super.initState();

    _tabController = TabController(length: _tabTitles.length, vsync: this);

    for (int i = 0; i < 21; i++) {

      List<DataCell> fadeData = [];

      // 表格每行名称-地区或运营商名称

      _rowTitles.add(InkWell(

        onTap: () => onTitleTap(i),

        child: Container(

            height: 36.0,

            alignment: Alignment.center,

            padding: EdgeInsets.symmetric(horizontal: 8.0),

            child: Text(

              "RowTitle${i + 1}",

              style: TextStyle(fontSize: 14.0, color: Color(0xff333333)),

              maxLines: 1,

              overflow: TextOverflow.ellipsis,

            )),

      ));

      for (int j = 0; j < 11; j++) {

        if (i == 0) {

          // 表格每一列的名称

          _dataColumns.add(DataColumn(label: Text("ColumnTitle$j")));

        }

        fadeData.add(DataCell(Text(

          "$i$j",

          textAlign: TextAlign.center,

          style: TextStyle(fontSize: 14.0, color: Color(0xff666666)),

        )));

      }

      _dataRows.add(DataRow(cells: fadeData));

    }

    _rowTitles.insert(

        0,

        Container(

          height: 31.0,

        ));

  }

  @override

  Widget build(BuildContext context) {

    return Scaffold(

      appBar: AppBar(

        elevation: 0.0,

        brightness: Brightness.light,

        backgroundColor: Colors.white,

        centerTitle: true,

        //在标题前面显示的一个控件,在首页通常显示应用的 logo;在其他界面通常显示为返回按钮

        leading: IconButton(

            padding: const EdgeInsets.all(0.0),

            icon: Icon(

              Icons.arrow_back_ios,

              color: Colors.black,

            ),

            onPressed: () => onBack()),

        //Toolbar 中主要内容,通常显示为当前界面的标题文字

        title: Column(

          children: [

            Text("title",

                style: TextStyle(

                  fontSize: 16.0,

                  color: Colors.black38,

                )),

            Text("subtitle",

                style: TextStyle(

                  fontSize: 12.0,

                  color: Colors.black38,

                ))

          ],

        ),

        //标题右侧显示的按钮组

        actions: [

          FlatButton(

            onPressed: () => doSearch(),

            child: Container(

              alignment: Alignment.centerRight,

              margin: EdgeInsets.only(left: 22.0),

              child: Text(

                "Search",

                style: TextStyle(

                  fontSize: 14.0,

                  color: Colors.blue,

                ),

              ),

            ),

          ),

        ],

      ),

      body: DefaultTabController(

        length: _tabTitles.length,

        child: CustomScrollView(

          slivers: <Widget>[

            SliverAppBar(

              expandedHeight: 200.0,

              automaticallyImplyLeading: false,

              floating: true,

              snap: true,

              toolbarHeight: 0.0,

              elevation: 0.0,

              pinned: true,

              flexibleSpace: FlexibleSpaceBar(

                  centerTitle: true,

                  background: Container(

                    height: 200.0,

                    alignment: Alignment.center,

                    child: Text(

                      "TopContent",

                      style: TextStyle(fontSize: 18.0, color: Colors.black),

                    ),

                  )),

            ),

            SliverFillRemaining(

              child: Column(

                children: [

                  Container(

                    height: 44.0,

                    decoration: BoxDecoration(

                        border: Border(

                            bottom: BorderSide(

                          style: BorderStyle.solid,

                          color: Color(0xfff7f7f7),

                          width: 2.0,

                        ))),

                    child: TabBar(

                      controller: _tabController,

                      tabs: _tabTitles

                          .map((e) => Tab(

                                child: Text(

                                  e,

                                  style: TextStyle(

                                      fontSize: 14.0,

                                      fontWeight: FontWeight.w500),

                                ),

                              ))

                          .toList(),

                      isScrollable: false,

                      labelColor: Color(0xff1376ee),

                      indicatorColor: Color(0xff1376ee),

                      unselectedLabelColor: Color(0xff666666),

                    ),

                  ),

                  Expanded(

                      flex: 1,

                      child: Container(

                        color: Colors.white,

                        child: TabBarView(

                          controller: _tabController,

                          physics: NeverScrollableScrollPhysics(),

                          children: <Widget>[

                            ListView(

                              children: [

                                Row(

                                  children: [

                                    // 行名

                                    Expanded(

                                        child: ListView(

                                      physics: NeverScrollableScrollPhysics(),

                                      shrinkWrap: true,

                                      children: _rowTitles,

                                    )),

                                    Expanded(

                                      flex: 3,

                                      child: ListView(

                                        physics: NeverScrollableScrollPhysics(),

                                        shrinkWrap: true,

                                        children: [

                                          SingleChildScrollView(

                                            scrollDirection: Axis.horizontal,

                                            child: DataTable(

                                              dividerThickness: 0.0,

                                              headingTextStyle: TextStyle(

                                                  fontSize: 14.0,

                                                  color: Color(0xff333333),

                                                  fontWeight: FontWeight.w600),

                                              sortAscending: false,

                                              showBottomBorder: false,

                                              showCheckboxColumn: false,

                                              headingRowHeight: 32.0,

                                              dataRowHeight: 36.0,

                                              columns: _dataColumns,

                                              // 列名

                                              rows: _dataRows, // 数据

                                            ),

                                          )

                                        ],

                                      ),

                                    ),

                                  ],

                                )

                              ],

                            ),

                            Container(),

                            Container(),

                          ],

                        ),

                      )),

                ],

              ),

            ),

          ],

        ),

      ),

    );

  }

  @override

  void dispose() {

    _tabController.dispose();

    super.dispose();

  }

  // 返回事件

  onBack() {}

  // Search

  doSearch() {}

  //每一行标题的点击事件

  onTitleTap(int i) {}

}

实现起来还是比较简单的。实际中建议一个控件写一个Widget,在body中调用,而不是都写在body中,代码太长不便于查看、管理、维护。

#  效果



FlutterTableDemo
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,667评论 5 472
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,361评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,700评论 0 333
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,027评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,988评论 5 361
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,230评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,705评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,366评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,496评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,405评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,453评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,126评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,725评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,803评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,015评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,514评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,111评论 2 341

推荐阅读更多精彩内容