[Flutter Package]类iOS使用方法的SectionTableView

零、获取方式

此控件的package我已经托管到了pub仓库
如果你被墙住了,也可以看国内镜像

使用方式就是在你的flutter pubspec.yaml中添加依赖:

image

然后flutter packages get更新依赖即可

一、起因

最近写demo时发现Flutter自带的ListView widget很简陋,没有分隔线,没有section/row之分,也没有sectionHeader,如果要实现一个有分割线,有section区分,有section header的ListView,耦合会非常严重:

image

https://pub.dartlang.org 上没有找到封装好的这种TableView,于是乎决定自己写一个,命名为SectionTableView

二、需求整理

本人是iOS开发,所以习惯了iOS上的UITableView的调用风格,所以在实现flutter的SectionTableView时,决定实现如下功能

  • 可以提供分割线
  • 必须提供section的数量
  • 必须提供某section内的行数(cell row)
  • 必须提供在某一section下的某一行下(indexPath)的这一行的控件(cell)
  • 可以提供某一section和头部(section header)

三、实现

为了实现这些功能,并且方便后期增加滚动功能,上下拉刷新功能,使用了StatefulWidget作为父类:

class SectionTableView extends StatefulWidget {
  final Widget divider;
  @required
  final int sectionCount;
  @required
  final RowCountInSectionCallBack numOfRowInSection;
  @required
  final CellAtIndexPathCallBack cellAtIndexPath;
  final SectionHeaderCallBack headerInSection;
  SectionTableView(
      {this.divider,
      this.sectionCount,
      this.numOfRowInSection,
      this.cellAtIndexPath,
      this.headerInSection});
  @override
  _SectionTableViewState createState() => new _SectionTableViewState();
}

接着在对应的_SectionTableViewState中的build方法中,返回ListView:

class _SectionTableViewState extends State<SectionTableView> {

  _buildCell(BuildContext context, int index) {
    //TODO: return cells/dividers/section headers
  }

  @override
  Widget build(BuildContext context) {
    return ListView.builder(itemBuilder: (context, index) {
      return _buildCell(context, index);
    });
  }
}

熟悉flutter ListView的同学知道,ListView的builder类方法,有一个itemBuilder回调函数,参数是当前的上下文,和将要渲染的行索引index,index对应想要获取的某一行控件(cell或者叫ListItem),返回非空的组件就证明这个index有值,返回null就表示列表到尽头了。
我们需要做的就是对index进行映射,判断当前index对应的控件,应该是列表里的section header,还是分隔线devider,还是某一行的真正内容cell。

出于性能的考虑,不可能每次调用 _buildCell的时候,都计算一遍index对应的section和row的位置,所以定义了一个类成员变量indexPathSearch,是数组,数组长度就是ListView所有的行,当 _buildCell 的参数index大于等于indexPathSearch的长度的时候,就返回null,表示列表内容到此为止了。
indexPathSearch里每一个元素,就是index对应的section和row(称为indexPath),index指向实际行(cell)的时候,section和row都是大于等于0的,当section大于等于0,row==-1的时候,表示这里是一个section header,当两者都等于-1的时候,表示这里是一个分割线:

    bool showDivider = false;
    bool showSectionHeader = false;
    if (widget.divider != null) {
      showDivider = true;
    }
    if (widget.headerInSection != null) {
      showSectionHeader = true;
    }

    for (int i = 0; i < widget.sectionCount; i++) {
      if (showSectionHeader) {
        indexPathSearch.add(IndexPath(section: i, row: -1));
      }
      int rows = widget.numOfRowInSection(i);
      for (int j = 0; j < rows; j++) {
        indexPathSearch.add(IndexPath(section: i, row: j));
        if (showDivider) {
          indexPathSearch.add(IndexPath(section: -1, row: -1));
        }
      }
    }

计算好了index到indexPath的映射,剩下的就好说了,在_buildCell中,提取indexPath并判断indexPath的内容,返回对应的控件:

 _buildCell(BuildContext context, int index) {
    if (index >= indexPathSearch.length) {
      return null;
    }

    IndexPath indexPath = indexPathSearch[index];
    //section header
    if (indexPath.section >= 0 && indexPath.row < 0) {
      return widget.headerInSection(indexPath.section);
    }

    if (indexPath.section < 0 && indexPath.row < 0) {
      return widget.divider;
    }

    Widget cell = widget.cellAtIndexPath(indexPath.section, indexPath.row);
    return cell;
  }

四、 源码


library flutter_section_table_view;

import 'package:flutter/material.dart';

typedef int RowCountInSectionCallBack(int section);
typedef Widget CellAtIndexPathCallBack(int section, int row);
typedef Widget SectionHeaderCallBack(int section);

class IndexPath {
  final int section;
  final int row;
  IndexPath({this.section, this.row});
}

class SectionTableView extends StatefulWidget {
  final Widget divider;
  @required
  final int sectionCount;
  @required
  final RowCountInSectionCallBack numOfRowInSection;
  @required
  final CellAtIndexPathCallBack cellAtIndexPath;
  final SectionHeaderCallBack headerInSection;
  SectionTableView(
      {this.divider,
      this.sectionCount,
      this.numOfRowInSection,
      this.cellAtIndexPath,
      this.headerInSection});
  @override
  _SectionTableViewState createState() => new _SectionTableViewState();
}

class _SectionTableViewState extends State<SectionTableView> {
  List indexPathSearch = [];

  @override
  void initState() {
    super.initState();
    bool showDivider = false;
    bool showSectionHeader = false;
    if (widget.divider != null) {
      showDivider = true;
    }
    if (widget.headerInSection != null) {
      showSectionHeader = true;
    }

    for (int i = 0; i < widget.sectionCount; i++) {
      if (showSectionHeader) {
        indexPathSearch.add(IndexPath(section: i, row: -1));
      }
      int rows = widget.numOfRowInSection(i);
      for (int j = 0; j < rows; j++) {
        indexPathSearch.add(IndexPath(section: i, row: j));
        if (showDivider) {
          indexPathSearch.add(IndexPath(section: -1, row: -1));
        }
      }
    }
  }

  @override
  void dispose() {
    super.dispose();
  }

  @override
  void didUpdateWidget(SectionTableView oldWidget) {
    super.didUpdateWidget(oldWidget);
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
  }

  _buildCell(BuildContext context, int index) {
    if (index >= indexPathSearch.length) {
      return null;
    }

    IndexPath indexPath = indexPathSearch[index];
    //section header
    if (indexPath.section >= 0 && indexPath.row < 0) {
      return widget.headerInSection(indexPath.section);
    }

    if (indexPath.section < 0 && indexPath.row < 0) {
      return widget.divider;
    }

    Widget cell = widget.cellAtIndexPath(indexPath.section, indexPath.row);
    return cell;
  }

  @override
  Widget build(BuildContext context) {
    return ListView.builder(itemBuilder: (context, index) {
      return _buildCell(context, index);
    });
  }
}


五、下一步

这是我的第一个flutter package,目前还很简陋,flutter目前尚且如此,所以大家一起改善它,
下一步将优化如下内容:

  • 支持滑动到指定indexPath [✓]
  • 支持滑动的时候,反馈滑动到的位置[✓]
  • 支持下拉刷新[✓]
  • 支持上拉加载[✓]
  • 支持左滑编辑

如果大家喜欢,请多多star我的项目GitHub

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

推荐阅读更多精彩内容

  • 当广场的人声散去, 唯有站立的路灯在对大地倾诉。 当街道的车声远去, 唯有路边的梧桐对夜空歌唱。 当小楼的灯光熄灭...
    平少伟阅读 263评论 0 1
  • 送走了王老四,卫强东大大的松了口气,只有王艾迪,一个人叹气。 她的父亲,就像狗皮膏药,沾上了就不容易扯脱,占了点小...
    三湖之春阅读 877评论 16 15
  • 走回鬼门关,生活乐无边。 爬回奈何桥,开水变佳肴。 拎起一身千古硝,爬进炉里再烧烧。
    钧涵阅读 215评论 4 2