
最近app想要類似于ios的那種tableview,區(qū)頭懸停的效果。最后找到了一個(gè)大神的方案。比較不錯(cuò),UI效果也達(dá)到了理想效果。
具體的實(shí)現(xiàn)代碼:
主要是gsy大神的這個(gè)類:(地址:https://github.com/CarGuo/gsy_flutter_demo)
引入的頭文件
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
準(zhǔn)備好數(shù)據(jù)源:
Map<String, List<String>> moreItemSectionList = {
'拜訪一區(qū)': ["遠(yuǎn)程拜訪", "遠(yuǎn)程訂單", "店前拜訪提醒"],
'拜訪二區(qū)': ["遠(yuǎn)程拜訪", "遠(yuǎn)程訂單", "店前拜訪提醒", "1-進(jìn)入門店", "2-生動(dòng)化執(zhí)行"],
'拜訪三區(qū)': ["遠(yuǎn)程拜訪", "遠(yuǎn)程訂單", "店前拜訪提醒", "1-進(jìn)入門店", "2-生動(dòng)化執(zhí)行", "3-店鋪檢查"],
'拜訪四區(qū)': ["遠(yuǎn)程拜訪"],
'拜訪五區(qū)': ["遠(yuǎn)程拜訪", "遠(yuǎn)程訂單", "店前拜訪提醒", "1-進(jìn)入門店", "2-生動(dòng)化執(zhí)行", "3-店鋪檢查"],
'拜訪六區(qū)': ["1-進(jìn)入門店", "2-生動(dòng)化執(zhí)行", "3-店鋪檢查"],
'拜訪⑦區(qū)': ["遠(yuǎn)程拜訪", "遠(yuǎn)程訂單", "店前拜訪提醒", "1-進(jìn)入門店", "2-生動(dòng)化執(zhí)行", "3-店鋪檢查"],
'拜訪⑧區(qū)': ["3-店鋪檢查"],
'拜訪⑨區(qū)': ["遠(yuǎn)程拜訪", "遠(yuǎn)程訂單", "1-進(jìn)入門店", "2-生動(dòng)化執(zhí)行", "3-店鋪檢查"],
};
實(shí)現(xiàn)方法:
final random = math.Random();
const stickHeader = 50.0;
class StickSliverListDemoPage extends StatefulWidget {
//整理數(shù)據(jù)
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('數(shù)據(jù)----->$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('數(shù)據(jù)----->$_titleIndex');
return Scaffold(
appBar: AppBar(
title: new Text("分區(qū)列表"),
),
body: Stack(
children: <Widget>[
Container(
child: CustomScrollView(
key: scrollKey,
controller: _scrollController,
physics: const ClampingScrollPhysics(),
slivers: List.generate(widget.dataList.length, (index) {
// Log.i('數(shù)據(jù)----->$index');
//分區(qū)的數(shù)據(jù)
ExpendedModel sectionModel =
widget.dataList[index] as ExpendedModel;
Log.i('數(shù)據(jù)----->$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) {
//展開和控制的邏輯,不需要這個(gè)效果
bool needExpanded = (widget.expendedModel!.dataList.length > 3);
needExpanded = false;
List cellList = widget.expendedModel!.dataList;
Log.i('cell數(shù)據(jù)------>$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數(shù)據(jù)------>$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.第一步改造數(shù)據(jù)源
構(gòu)造數(shù)據(jù)這一步很重要,因?yàn)榭蚣芾锩娴臄?shù)據(jù)是比較特殊,那我們的后臺(tái)下發(fā)的數(shù)據(jù)模型肯定和框架不一樣,那我們?nèi)绾胃脑鞌?shù)據(jù)呢?
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);
});
其實(shí)就是這段方法,將我們的數(shù)據(jù)源改成ExpendedModel類型的數(shù)據(jù),而你分析了ExpendedModel就會(huì)發(fā)現(xiàn),dataList是分區(qū)的list數(shù)據(jù),sectionTitle是區(qū)頭,也就是key值列表,key值列表的數(shù)據(jù)是來源于moreItemSectionList.keys.toList()這個(gè)方法。
2.第二步就是構(gòu)造組件
仔細(xì)分析代碼發(fā)現(xiàn),這個(gè)列表構(gòu)造不是用listview.build()方法來構(gòu)造的,而是用Stack組件,CustomScrollView子組件和自定義的StickHeader組件構(gòu)建這樣一個(gè)區(qū)頭列表的。
slivers: List.generate(widget.dataList.length, (index) {
// Log.i('數(shù)據(jù)----->$index');
//分區(qū)的數(shù)據(jù)
ExpendedModel sectionModel =
widget.dataList[index] as ExpendedModel;
Log.i('數(shù)據(jù)----->$sectionModel.dataList');
return SliverExpandedList(
sectionModel,
"header $index",
visibleCount: sectionModel.dataList.length,
valueChanged: (_) {
setState(() {});
},
);
}),
這是構(gòu)造每個(gè)區(qū)的列表方法。就是用 List.generate方法構(gòu)造每一行子組件。
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數(shù)據(jù)------>$cellValue');
///cell
return Card(
child: Container(
height: 44.0,
alignment: Alignment.centerLeft,
child: Text(cellValue),
),
);
},
childCount: getListCount(needExpanded),
),
childCount是行數(shù)計(jì)算。
很多人疑問為什么要判斷index = 0,因?yàn)槲野l(fā)現(xiàn)這個(gè)框架會(huì)把區(qū)頭的key,也算在行數(shù)計(jì)算上,第一行其實(shí)是區(qū)頭,后面的才是行數(shù)據(jù)。取值的時(shí)候要取index- 1的索引值,否則就不對(duì)了。