最近app想要类似于ios的那种tableview,区头悬停的效果。最后找到了一个大神的方案。比较不错,UI效果也达到了理想效果。
具体的实现代码:
主要是gsy大神的这个类:(地址:https://github.com/CarGuo/gsy_flutter_demo)
引入的头文件
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
准备好数据源:
Map<String, List<String>> moreItemSectionList = {
'拜访一区': ["远程拜访", "远程订单", "店前拜访提醒"],
'拜访二区': ["远程拜访", "远程订单", "店前拜访提醒", "1-进入门店", "2-生动化执行"],
'拜访三区': ["远程拜访", "远程订单", "店前拜访提醒", "1-进入门店", "2-生动化执行", "3-店铺检查"],
'拜访四区': ["远程拜访"],
'拜访五区': ["远程拜访", "远程订单", "店前拜访提醒", "1-进入门店", "2-生动化执行", "3-店铺检查"],
'拜访六区': ["1-进入门店", "2-生动化执行", "3-店铺检查"],
'拜访⑦区': ["远程拜访", "远程订单", "店前拜访提醒", "1-进入门店", "2-生动化执行", "3-店铺检查"],
'拜访⑧区': ["3-店铺检查"],
'拜访⑨区': ["远程拜访", "远程订单", "1-进入门店", "2-生动化执行", "3-店铺检查"],
};
实现方法:
final random = math.Random();
const stickHeader = 50.0;
class StickSliverListDemoPage extends StatefulWidget {
//整理数据
final List<ExpendedModel?> dataList =
List.generate(moreItemSectionList.length, (index) {
final List _titles = moreItemSectionList.keys.toList();
String titlekey = _titles[index];
List cellList = moreItemSectionList[titlekey] as List;
return ExpendedModel(false, cellList, titlekey);
});
@override
_StickSliverListDemoPageState createState() =>
_StickSliverListDemoPageState();
}
class _StickSliverListDemoPageState extends State<StickSliverListDemoPage> {
int _titleIndex = 0;
bool _showTitleTopButton = false;
ScrollController _scrollController = new ScrollController();
final GlobalKey scrollKey = GlobalKey();
@override
void initState() {
super.initState();
Log.i('数据----->$widget.dataList');
_scrollController.addListener(scrollChanged);
}
@override
void dispose() {
super.dispose();
_scrollController.removeListener(scrollChanged);
}
scrollChanged() {
if (widget.dataList.length == 0) {
return;
}
var item = widget.dataList.lastWhere((item) {
if (item!.globalKey.currentContext == null) {
return false;
}
///获取 renderBox
RenderSliver? renderSliver =
item.globalKey.currentContext!.findRenderObject() as RenderSliver?;
if (renderSliver == null) {
return false;
}
return renderSliver.constraints.scrollOffset > 0;
}, orElse: () {
return null;
});
if (item == null) {
return;
}
Log.i('----->$item');
int currentIndex = widget.dataList.indexOf(item);
if (currentIndex != _titleIndex) {
setState(() {
_titleIndex = currentIndex;
});
}
var needTopButton = _scrollController.position.pixels > 0;
if (needTopButton != _showTitleTopButton) {
setState(() {
_showTitleTopButton = needTopButton;
});
}
}
@override
Widget build(BuildContext context) {
Log.i('数据----->$_titleIndex');
return Scaffold(
appBar: AppBar(
title: new Text("分区列表"),
),
body: Stack(
children: <Widget>[
Container(
child: CustomScrollView(
key: scrollKey,
controller: _scrollController,
physics: const ClampingScrollPhysics(),
slivers: List.generate(widget.dataList.length, (index) {
// Log.i('数据----->$index');
//分区的数据
ExpendedModel sectionModel =
widget.dataList[index] as ExpendedModel;
Log.i('数据----->$sectionModel.dataList');
return SliverExpandedList(
sectionModel,
"header $index",
visibleCount: sectionModel.dataList.length,
valueChanged: (_) {
setState(() {});
},
);
}),
),
),
StickHeader(
"header $_titleIndex",
showTopButton: _showTitleTopButton,
callback: () {
var item = widget.dataList[_titleIndex]!;
RenderSliver renderSliver = item.globalKey.currentContext!
.findRenderObject() as RenderSliver;
var position = _scrollController.position.pixels -
renderSliver.constraints.scrollOffset;
_scrollController.position.jumpTo(position);
},
sectionModel: widget.dataList[_titleIndex] as ExpendedModel,
)
],
));
}
}
class SliverExpandedList extends StatefulWidget {
final ExpendedModel? expendedModel;
final String title;
final int visibleCount;
final ValueChanged? valueChanged;
SliverExpandedList(this.expendedModel, this.title,
{required this.visibleCount, this.valueChanged});
@override
_SliverExpandedListState createState() => _SliverExpandedListState();
}
class _SliverExpandedListState extends State<SliverExpandedList> {
bool expanded = false;
toTop() {
}
getListCount(bool needExpanded) {
return (expanded)
? (needExpanded)
? widget.expendedModel!.dataList.length + 2
: widget.expendedModel!.dataList.length + 1
: (needExpanded)
? widget.visibleCount + 2
: widget.visibleCount + 1;
}
@override
Widget build(BuildContext context) {
//展开和控制的逻辑,不需要这个效果
bool needExpanded = (widget.expendedModel!.dataList.length > 3);
needExpanded = false;
List cellList = widget.expendedModel!.dataList;
Log.i('cell数据------>$cellList');
return SliverList(
key: widget.expendedModel!.globalKey,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
// ///增加bottom
// if (!expanded && needExpanded && index == widget.visibleCount + 1) {
// return renderExpendedMore();
// }
// if (index == widget.expendedModel!.dataList.length + 1) {
// return renderExpendedMore();
// }
// Log.i('cell索引------>$index');
///增加header
if (index == 0) {
return StickHeader(widget.title,
sectionModel: widget.expendedModel as ExpendedModel);
}
String cellValue = cellList[index - 1] as String;
Log.i('cell数据------>$cellValue');
///cell
return Card(
child: Container(
height: 44.0,
alignment: Alignment.centerLeft,
child: Text(cellValue),
),
);
},
childCount: getListCount(needExpanded),
),
);
}
}
class StickHeader extends StatelessWidget {
final String title;
final bool showTopButton;
final VoidCallback? callback;
final ExpendedModel sectionModel;
StickHeader(this.title,
{Key? key,
this.showTopButton = false,
required this.sectionModel,
this.callback})
: super(key: key);
@override
Widget build(BuildContext context) {
return Container(
height: stickHeader,
color: Colors.deepPurple,
padding: const EdgeInsets.only(left: 10.0),
alignment: Alignment.centerLeft,
child: Row(
children: <Widget>[
Expanded(
child: Text(
sectionModel.sectionTitle,
style: const TextStyle(color: Colors.white),
),
),
Visibility(
visible: showTopButton,
child: InkWell(
onTap: () {
callback?.call();
},
child: const Icon(
Icons.vertical_align_top,
color: Colors.red,
),
),
)
],
),
);
}
}
class ExpendedModel {
bool expended;
List dataList;
GlobalKey globalKey = GlobalKey();
String sectionTitle;
ExpendedModel(this.expended, this.dataList, this.sectionTitle);
}
1.第一步改造数据源
构造数据这一步很重要,因为框架里面的数据是比较特殊,那我们的后台下发的数据模型肯定和框架不一样,那我们如何改造数据呢?
final List<ExpendedModel?> dataList =
List.generate(moreItemSectionList.length, (index) {
final List _titles = moreItemSectionList.keys.toList();
String titlekey = _titles[index];
List cellList = moreItemSectionList[titlekey] as List;
return ExpendedModel(false, cellList, titlekey);
});
其实就是这段方法,将我们的数据源改成ExpendedModel类型的数据,而你分析了ExpendedModel就会发现,dataList是分区的list数据,sectionTitle是区头,也就是key值列表,key值列表的数据是来源于moreItemSectionList.keys.toList()这个方法。
2.第二步就是构造组件
仔细分析代码发现,这个列表构造不是用listview.build()方法来构造的,而是用Stack组件,CustomScrollView子组件和自定义的StickHeader组件构建这样一个区头列表的。
slivers: List.generate(widget.dataList.length, (index) {
// Log.i('数据----->$index');
//分区的数据
ExpendedModel sectionModel =
widget.dataList[index] as ExpendedModel;
Log.i('数据----->$sectionModel.dataList');
return SliverExpandedList(
sectionModel,
"header $index",
visibleCount: sectionModel.dataList.length,
valueChanged: (_) {
setState(() {});
},
);
}),
这是构造每个区的列表方法。就是用 List.generate方法构造每一行子组件。
3.第三步就是子组件SliverExpandedList的方法
SliverList(
key: widget.expendedModel!.globalKey,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
///增加header
if (index == 0) {
return StickHeader(widget.title,
sectionModel: widget.expendedModel as ExpendedModel);
}
String cellValue = cellList[index - 1] as String;
Log.i('cell数据------>$cellValue');
///cell
return Card(
child: Container(
height: 44.0,
alignment: Alignment.centerLeft,
child: Text(cellValue),
),
);
},
childCount: getListCount(needExpanded),
),
childCount是行数计算。
很多人疑问为什么要判断index = 0,因为我发现这个框架会把区头的key,也算在行数计算上,第一行其实是区头,后面的才是行数据。取值的时候要取index- 1的索引值,否则就不对了。