零, 获取方式
此控件的 package 我已经托管到了 pub 仓库 https://pub.dartlang.org/packages/flutter_section_table_view 如果你被墙住了, 也可以看国内镜像 https://pub.flutter-io.cn/packages/flutter_section_table_view
使用方式就是在你的 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<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 目前尚且如此, 所以大家一起改善它, 下一步将优化如下内容:
支持 section footer
支持下拉刷新
支持上拉加载
支持左滑编辑(这个好像有点复杂)
来源: https://juejin.im/post/5b72988651882560fd234502