零、获取方式
此控件的package我已经托管到了pub仓库
如果你被墙住了,也可以看国内镜像
使用方式就是在你的flutter pubspec.yaml中添加依赖:
然后flutter packages get更新依赖即可
一、起因
最近写demo时发现Flutter自带的ListView widget很简陋,没有分隔线,没有section/row之分,也没有sectionHeader,如果要实现一个有分割线,有section区分,有section header的ListView,耦合会非常严重:
在 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 {
_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 {
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