Flutter列表分组吸顶
2021-07-13 本文已影响0人
倪大头
先推荐官方插件:sticky_headers
下面是用Stack + CustomScrollView实现的方式:
bzpzl-e2se5.gif列表部分是CustomScrollView,吸顶的header利用Stack布局贴在CustomScrollView上层
监听CustomScrollView的滑动,和每组小列表的header位置作比较,来确定显示哪个浮动header
完整代码:
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
// viewForHeaderInSection 每个section的Header
typedef ViewForHeaderInSection = Widget Function(
BuildContext context, int section);
// heightForHeaderInSection 每个sectionHeader的高度
typedef HeightForHeaderInSection = double Function(int section);
// numberOfRowsInSection 当前section的item个数
typedef NumberOfRowsInSection = int Function(int section);
// cellForRowAtIndexPath 每个section的item
typedef CellForRowAtIndexPath = Widget Function(
BuildContext context, IndexPath indexPath);
class IndexPath {
int section;
int row;
IndexPath(this.section, this.row);
}
class SectionTableView extends StatefulWidget {
final ViewForHeaderInSection viewForHeaderInSection;
final HeightForHeaderInSection heightForHeaderInSection;
final CellForRowAtIndexPath cellForRowAtIndexPath;
final int numberOfSections; // section个数
final NumberOfRowsInSection numberOfRowsInSection;
const SectionTableView({
required this.viewForHeaderInSection,
required this.heightForHeaderInSection,
required this.cellForRowAtIndexPath,
required this.numberOfSections,
required this.numberOfRowsInSection,
});
@override
_SectionTableViewState createState() => _SectionTableViewState();
}
class _SectionTableViewState extends State<SectionTableView> {
List<Widget> slivers = []; // CustomScrollView子组件
ScrollController scrollController = ScrollController();
GlobalKey tableKey = GlobalKey(); // CustomScrollView Key
List<GlobalKey> headerKeys = []; // 所有sectionHeader的Key
var curHeaderIndex = 0.obs; // 当前悬浮的sectionHeader下标
var curHeaderTransY = 0.0.obs;
@override
void initState() {
// 准备组件
initSlivers();
scrollController.addListener(() {
// CustomScrollView 在屏幕中Y坐标
double tableY = getWidgetOffsetY(tableKey);
// print('tableY: $tableY');
for (int i = 0; i < headerKeys.length; i++) {
// i
double curHeaderY = getWidgetOffsetY(headerKeys[i]);
// i + 1
double nextHeaderY = scrollController.position.maxScrollExtent;
if ((i + 1) < headerKeys.length) {
nextHeaderY = getWidgetOffsetY(headerKeys[i + 1]);
}
// 当前section高度
double curSectionHeight = nextHeaderY - curHeaderY;
if (((tableY - curSectionHeight) <= curHeaderY) &&
(curHeaderY <= tableY)) {
// 当前header在ScrollView中的OffsetY
double curHeaderOffsetY = curHeaderY - tableY;
// 当前section剩余高度(完全上划走前)
double bottomMargin = curHeaderOffsetY + curSectionHeight;
// 当前header size
Size headerSize = getWidgetSize(headerKeys[i]) ?? Size(0, 0);
if (bottomMargin < headerSize.height) {
curHeaderTransY.value = headerSize.height - bottomMargin;
} else {
curHeaderTransY.value = 0.0;
}
// print(curHeaderTransY.value);
if (curHeaderIndex.value != i) {
curHeaderIndex.value = i;
// print('第 ${curHeaderIndex.value} 个header悬浮');
}
}
}
});
super.initState();
}
// 获取组件offset.Y
double getWidgetOffsetY(GlobalKey key) {
RenderBox? renderBox = key.currentContext?.findRenderObject() as RenderBox?;
double offsetY = renderBox!.localToGlobal(Offset.zero).dy;
return offsetY;
}
// 获取组件size
Size? getWidgetSize(GlobalKey key) {
RenderBox? renderBox = key.currentContext?.findRenderObject() as RenderBox?;
return renderBox?.size;
}
@override
void dispose() {
scrollController.dispose();
super.dispose();
}
void initSlivers() {
headerKeys.clear();
slivers.clear();
List.generate(widget.numberOfSections, (section) {
// section header
GlobalKey headerKey = GlobalKey();
Widget sectionHeader = SliverToBoxAdapter(
child: Container(
key: headerKey,
child: widget.viewForHeaderInSection(context, section),
),
);
headerKeys.add(headerKey);
slivers.add(sectionHeader);
int rowCount = widget.numberOfRowsInSection(section);
if (rowCount > 0) {
// 当前section有item
List<Widget> rows = [];
rows = List.generate(rowCount, (row) {
return SliverToBoxAdapter(
child:
widget.cellForRowAtIndexPath(context, IndexPath(section, row)),
);
});
slivers.addAll(rows);
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: _initSubviews(),
);
}
Widget _initSubviews() {
return Stack(
children: [
CustomScrollView(
key: tableKey,
shrinkWrap: true,
controller: scrollController,
slivers: slivers,
physics: ClampingScrollPhysics(), // 禁用弹簧效果
),
Obx(() {
return Positioned(
top: -curHeaderTransY.value,
child: widget.viewForHeaderInSection(context, curHeaderIndex.value),
);
}),
],
);
}
}
使用:
return Container(
margin: EdgeInsets.only(top: 200),
child: SectionTableView(
numberOfSections: 4,
viewForHeaderInSection: (BuildContext context, int section) {
return Container(
width: ScreenUtil().screenWidth,
height: 50,
color: Colors.white,
child: Center(
child: Text(
'Header $section',
style: TextStyle(
fontSize: 28.sp,
color: Colors.black,
fontWeight: FontWeight.bold,
),
),
),
);
},
heightForHeaderInSection: (int section) {
return 50;
},
numberOfRowsInSection: (int section) {
return 6;
},
cellForRowAtIndexPath: (BuildContext context, IndexPath indexPath) {
return Container(
height: 100,
color: Utils.randomColor(),
child: Center(
child: Text(
'Row ${indexPath.row}',
style: TextStyle(
fontSize: 22.sp,
color: Colors.black,
),
),
),
);
},
),
);