说明
列表listview是前端应用中最基础的控件之一,大部分内容展示基本都在listview中,毫不夸张的说学好listview一个前端开发就基本可以挣钱了。ListView是滑动控件Scroll中的一种在系统scroll_view.dart中,其中包括基础的ScrollView,ListView,GridView等控件,这些都是最基础的控件,后面会一一讲解到。
ListView简介
ListView({
Key? key,
Axis scrollDirection = Axis.vertical,//控件滑动方向,有2个值Axis.vertical垂直,Axis.horizontal水平,
//默认认垂直方向
bool reverse = false,//控制数据读取方向,与scrollDirection配合使用,
//如scrollDirection为Axis.vertical时,false表示从左到右,true为从右到左;
//scrollDirection为Axis.horizontal时,false表示从上到下,true为从下到上。
//默认为false
ScrollController? controller,//用于控制视图滚动,如控制初始位置,读取当前位置或者改变位置等,
//当primary为true时必须为null
bool? primary,//是否与容器关联滑动,
//为true时默认可滑动,不管有没有内容;
//为false时只有内容超出容器边界时才可滑动;
//当scrollDirection为Axis.vertical同时controller为null时,primary默认为true,否则默认为false
ScrollPhysics? physics,//滚动视图应如何响应用户输入,例如,确定用户停止拖动滚动视图后滚动视图如何继续设置动画等
bool shrinkWrap = false,//是否应根据正在查看的内容确定scrollDirection中滚动视图的范围,
//如果滚动视图shrinkWrap为false,则滚动视图将展开设置为[scrollDirection]中允许的最大大小。
//如果滚动视图在[scrollDirection]上有无界约束,则[shrinkWrap ]必须是true。
EdgeInsetsGeometry? padding,//子view间的间隔
this.itemExtent,//指定Item在滑动方向上的高度,用来提高滑动性能,如果是non_null,则必须得给定滑动范围;
this.prototypeItem,//如果非null,则强制子级在滚动方向上具有与给定小部件相同的范围
bool addAutomaticKeepAlives = true,//是否将子控件包裹在AutomaticKeepAlive控件内
bool addRepaintBoundaries = true,//是否将子控件包裹在 RepaintBoundary 控件内。用于避免列表滚动时的重绘,如果子控件重绘开销很小时,比如子控件就是个色块或简短的文字,把这个字段设置为false性能会更好
bool addSemanticIndexes = true,//是否把子控件包装在IndexedSemantics里,用来提供无障碍语义
double? cacheExtent,//可见区域的前后会有一定高度的空间去缓存子控件,当滑动时就可以迅速呈现
List children = const [],//子view
int? semanticChildCount,//有含义的子控件的数量,如ListView会用children的长度,ListView.separated会用children长度的一半
DragStartBehavior dragStartBehavior = DragStartBehavior.start,//拖动开始行为
ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,//键盘关闭行为
String? restorationId,//还原ID
Clip clipBehavior = Clip.hardEdge,//剪切,默认Clip.hardEdge
})
部分扩展知识
ScrollPhysics:控制用户滚动视图的交互
AlwaysScrollableScrollPhysics:列表总是可滚动的。在iOS上会有回弹效果,在android上不会回弹。那么问题来了,如果primary设置为false(内容不足时不滚动),且 physics设置为AlwaysScrollableScrollPhysics,列表是否可以滑动?答案是可以,感兴趣的可以试一下
PageScrollPhysics:一般是给PageView控件用的滑动效果。如果listview设置的话在滑动到末尾时会有个比较大的弹起和回弹
ClampingScrollPhysics:滚动时没有回弹效果,同android系统的listview效果
NeverScrollableScrollPhysics:就算内容超过列表范围也不会滑动
BouncingScrollPhysics:不论什么平台都会有回弹效果
FixedExtentScrollPhysics:不适用于ListView,原因:需要指定scroller为FixedExtentScrollController,这个scroller只能用于ListWheelScrollViews
ListView的4种构造方式
1,默认构造函数
适用场景:已知有限个Item的情况下
ListView(
children: const [
ListTile(title: Text("普通ListView1")),
ListTile(title: Text("普通ListView2")),
ListTile(title: Text("普通ListView3")),
ListTile(title: Text("普通ListView4"))
]
)
2,builder
适用场景:长列表时采用builder模式,能提高性能。不是把所有子控件都构造出来,而是在控件viewport加上头尾的cacheExtent这个范围内的子Item才会被构造。在构造时传递一个builder,按需加载是一个惯用模式,能提高加载性能
List items = [];
class ListItem {
final String sender;
final String body;
ListItem(this.sender, this.body);
}
ListView.builder(
itemBuilder: (context, index) {
final item = items[index];
return ListTile(
title: Text(item.sender),
subtitle: Text(item.body),
);
},
itemCount: items.length
)
3,separated
适用场景:列表中需要分割线时,可以自定义复杂的分割线
ListView.separated(
itemBuilder: (context, index) {
return Text("Item $index");
},
separatorBuilder: (context, index) {
return Container(
color: Colors.grey,
height: 3,
);
},
itemCount: 100
)
4,custom(自定义SliverChildDelegate)
适用场景:上面几种模式基本可以满足业务需求,如果你还想做一些事情,如设置列表的最大滚动范围或获取滑动时每次布局的子Item范围,可以尝试一下custom模式
ListView.custom(childrenDelegate: CustomSliverChildDelegate())
class CustomSliverChildDelegate extends SliverChildDelegate {
/// 根据index构造child
@override
Widget build(BuildContext context, int index) {
// KeepAlive将把所有子控件加入到cache,已输入的TextField文字不会因滚动消失
// 仅用于演示
return KeepAlive(
keepAlive: true,
child: TextField(decoration: InputDecoration(hintText: '请输入')));
}
/// 决定提供新的childDelegate时是否需要重新build。在调用此方法前会做类型检查,不同类型时才会调用此方法,所以一般返回true。
@override
bool shouldRebuild(SliverChildDelegate oldDelegate) {
return true;
}
/// 提高children的count,当无法精确知道时返回null。
/// 当 build 返回 null时,它也将需要返回一个非null值
@override
int get estimatedChildCount => 100;
/// 预计最大可滑动高度,如果设置的过小会导致部分child不可见,设置报错
@override
double estimateMaxScrollOffset(int firstIndex, int lastIndex,
double leadingScrollOffset, double trailingScrollOffset) {
return 2500;
}
/// 完成layout后的回调,可以通过该方法获取即将渲染的视图树包括哪些子控件
@override
void didFinishLayout(int firstIndex, int lastIndex) {
print('didFinishLayout firstIndex=$firstIndex firstIndex=$lastIndex');
}
}
3种上下拉刷新
1,普通上下拉刷新
class pageRefresh1 extends State{
String currentText = "普通上下拉刷新1";
final int pageSize = 10;
List items = [];
bool disposed = false;
final ScrollController scrollController = ScrollController();
final GlobalKey refreshKey = GlobalKey();
@override
void dispose() {
disposed = true;
super.dispose();
}
Future onRefresh() async {
await Future.delayed(const Duration(seconds: 1));
items.clear();
for (int i = 0; i < pageSize; i++) {
ListItem item = ListItem( name: 'refesh+ $i', subName: 'subName+ $i');
items.add(item);
}
if(disposed) {
return;
}
setState(() {});
}
Future loadMore() async {
await Future.delayed(const Duration(seconds: 1));
for (int i = 0; i < pageSize; i++) {
ListItem item = ListItem( name: 'loadMore+ $i', subName: 'loadMore+ $i');
items.add(item);
}
if(disposed) {
return;
}
setState(() {});
}
@override
void initState() {
super.initState();
scrollController.addListener(() {
///判断当前滑动位置是不是到达底部,触发加载更多回调
if (scrollController.position.pixels == scrollController.position.maxScrollExtent) {
loadMore();
}
});
Future.delayed(const Duration(seconds: 0), (){
refreshKey.currentState!.show();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(currentText),
),
body: Container(
child: RefreshIndicator(
///GlobalKey,用户外部获取RefreshIndicator的State,做显示刷新
key: refreshKey,
///下拉刷新触发,返回的是一个Future
onRefresh: onRefresh,
child: ListView.builder(
///保持ListView任何情况都能滚动,解决在RefreshIndicator的兼容问题。
physics: const AlwaysScrollableScrollPhysics(),
///根据状态返回
itemBuilder: (context, index) {
if (index == items.length) {
return Container(
margin: const EdgeInsets.all(10),
child: const Align(
child: CircularProgressIndicator(),
),
);
}
return Card(
child: Container(
height: 60,
alignment: Alignment.centerLeft,
child: Text("Item ${items[index]} $index"),
),
);
},
///根据状态返回数量
itemCount: (items.length >= pageSize)
? items.length + 1
: items.length,
///滑动监听
controller: scrollController,
),
),
),
);
}
}
class ListItem {
const ListItem({
required this.name,
required this.subName,
});
final String name;
final String subName;
}
2,自定义上下拉刷新
class pageRefresh2 extends State{
String currentText = "普通上下拉刷新2";
final int pageSize = 10;
List items = [];
bool disposed = false;
final ScrollController scrollController = ScrollController();
final GlobalKey refreshKey = GlobalKey();
@override
void dispose() {
disposed = true;
super.dispose();
}
Future onRefresh() async {
await Future.delayed(const Duration(seconds: 1));
items.clear();
for (int i = 0; i < pageSize; i++) {
ListItem item = ListItem( name: 'refesh+ $i', subName: 'subName+ $i');
items.add(item);
}
if(disposed) {
return;
}
setState(() {});
}
Future loadMore() async {
await Future.delayed(const Duration(seconds: 1));
for (int i = 0; i < pageSize; i++) {
ListItem item = ListItem( name: 'loadMore+ $i', subName: 'loadMore+ $i');
items.add(item);
}
if(disposed) {
return;
}
setState(() {});
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
///直接触发下拉
Future.delayed(const Duration(milliseconds: 500), () {
scrollController.animateTo(-141,
duration: const Duration(milliseconds: 600), curve: Curves.linear);
return true;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(currentText),
),
body: Container(
child: NotificationListener(
onNotification: (ScrollNotification notification) {
///判断当前滑动位置是不是到达底部,触发加载更多回调
if (notification is ScrollEndNotification) {
if (scrollController.position.pixels > 0 &&
scrollController.position.pixels ==
scrollController.position.maxScrollExtent) {
loadMore();
}
}
return false;
},
child: CustomScrollView(
controller: scrollController,
///回弹效果
physics: const BouncingScrollPhysics(
parent: AlwaysScrollableScrollPhysics()),
slivers: [
///控制显示刷新的 CupertinoSliverRefreshControl
CupertinoSliverRefreshControl(
refreshIndicatorExtent: 100,
refreshTriggerPullDistance: 140,
onRefresh: onRefresh,
),
///列表区域
SliverSafeArea(
sliver: SliverList(
///代理显示
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
if (index == items.length) {
return Container(
margin: const EdgeInsets.all(10),
child: const Align(
child: CircularProgressIndicator(),
),
);
}
return Card(
child: Container(
height: 60,
alignment: Alignment.centerLeft,
child: Text("Item ${items[index]} $index"),
),
);
},
childCount: (items.length >= pageSize)
? items.length + 1
: items.length,
),
),
)
],
),
),
),
);
}
}
class ListItem {
const ListItem({
required this.name,
required this.subName,
});
final String name;
final String subName;
}
3,自定义上下拉刷新样式
class pageRefresh3 extends State{
String currentText = "自定义上下拉刷新样式";
final int pageSize = 10;
List items = [];
bool disposed = false;
final ScrollController scrollController = ScrollController();
final GlobalKey sliverRefreshKey = GlobalKey();
@override
void dispose() {
disposed = true;
super.dispose();
}
Future onRefresh() async {
await Future.delayed(const Duration(seconds: 1));
items.clear();
for (int i = 0; i < pageSize; i++) {
ListItem item = ListItem( name: 'refesh+ $i', subName: 'subName+ $i');
items.add(item);
}
if(disposed) {
return;
}
setState(() {});
}
Future loadMore() async {
await Future.delayed(const Duration(seconds: 1));
for (int i = 0; i < pageSize; i++) {
ListItem item = ListItem( name: 'loadMore+ $i', subName: 'loadMore+ $i');
items.add(item);
}
if(disposed) {
return;
}
setState(() {});
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
///直接触发下拉
Future.delayed(const Duration(milliseconds: 500), () {
scrollController.animateTo(-141,
duration: const Duration(milliseconds: 600), curve: Curves.linear);
return true;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(currentText),
),
body: Container(
child: NotificationListener(
onNotification: (ScrollNotification notification) {
///通知 CupertinoSliverRefreshControl 当前的拖拽状态
sliverRefreshKey.currentState!
.notifyScrollNotification(notification);
///判断当前滑动位置是不是到达底部,触发加载更多回调
if (notification is ScrollEndNotification) {
if (scrollController.position.pixels > 0 &&
scrollController.position.pixels ==
scrollController.position.maxScrollExtent) {
loadMore();
}
}
return false;
},
child: CustomScrollView(
controller: scrollController,
///回弹效果
physics: const BouncingScrollPhysics(
parent: AlwaysScrollableScrollPhysics()),
slivers: [
///控制显示刷新的 CupertinoSliverRefreshControl
MyCupertinoSliverRefreshControl(
key: sliverRefreshKey,
refreshIndicatorExtent: 100,
refreshTriggerPullDistance: 140,
onRefresh: onRefresh,
builder: buildSimpleRefreshIndicator,
),
///列表区域
SliverSafeArea(
sliver: SliverList(
///代理显示
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
if (index == items.length) {
return Container(
margin: const EdgeInsets.all(10),
child: const Align(
child: CircularProgressIndicator(),
),
);
}
return Card(
child: Container(
height: 60,
alignment: Alignment.centerLeft,
child: Text("Item ${items[index]} $index"),
),
);
},
childCount: (items.length >= pageSize)
? items.length + 1
: items.length,
),
),
),
],
),
),
),
);
}
}
Widget buildSimpleRefreshIndicator(
BuildContext context,
MyRefreshIndicatorMode? refreshState,
double pulledExtent,
double refreshTriggerPullDistance,
double refreshIndicatorExtent,
) {
const Curve opacityCurve = Interval(0.4, 0.8, curve: Curves.easeInOut);
return Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: refreshState != MyRefreshIndicatorMode.refresh
? Opacity(
opacity: opacityCurve.transform(
min(pulledExtent / refreshTriggerPullDistance, 1.0)),
child: const Icon(
CupertinoIcons.down_arrow,
color: CupertinoColors.inactiveGray,
size: 36.0,
),
)
: Opacity(
opacity: opacityCurve
.transform(min(pulledExtent / refreshIndicatorExtent, 1.0)),
child: const CupertinoActivityIndicator(radius: 14.0),
),
),
);
}
class ListItem {
const ListItem({
required this.name,
required this.subName,
});
final String name;
final String subName;
}
class _CupertinoSliverRefresh extends SingleChildRenderObjectWidget {
const _CupertinoSliverRefresh({
Key? key,
this.refreshIndicatorLayoutExtent = 0.0,
this.hasLayoutExtent = false,
Widget? child,
}) : assert(refreshIndicatorLayoutExtent >= 0.0),
super(key: key, child: child);
final double refreshIndicatorLayoutExtent;
final bool hasLayoutExtent;
@override
_RenderCupertinoSliverRefresh createRenderObject(BuildContext context) {
return _RenderCupertinoSliverRefresh(
refreshIndicatorExtent: refreshIndicatorLayoutExtent,
hasLayoutExtent: hasLayoutExtent,
);
}
@override
void updateRenderObject(BuildContext context, covariant _RenderCupertinoSliverRefresh renderObject) {
renderObject
..refreshIndicatorLayoutExtent = refreshIndicatorLayoutExtent
..hasLayoutExtent = hasLayoutExtent;
}
}
class _RenderCupertinoSliverRefresh extends RenderSliver
with RenderObjectWithChildMixin {
_RenderCupertinoSliverRefresh({
required double refreshIndicatorExtent,
required bool hasLayoutExtent,
RenderBox? child,
}) : assert(refreshIndicatorExtent >= 0.0),
_refreshIndicatorExtent = refreshIndicatorExtent,
_hasLayoutExtent = hasLayoutExtent {
this.child = child;
}
double get refreshIndicatorLayoutExtent => _refreshIndicatorExtent;
double _refreshIndicatorExtent;
set refreshIndicatorLayoutExtent(double value) {
assert(value >= 0.0);
if (value == _refreshIndicatorExtent)
return;
_refreshIndicatorExtent = value;
markNeedsLayout();
}
bool get hasLayoutExtent => _hasLayoutExtent;
bool _hasLayoutExtent;
set hasLayoutExtent(bool value) {
if (value == _hasLayoutExtent)
return;
_hasLayoutExtent = value;
markNeedsLayout();
}
double layoutExtentOffsetCompensation = 0.0;
@override
void performLayout() {
assert(constraints.axisDirection == AxisDirection.down);
assert(constraints.growthDirection == GrowthDirection.forward);
final double layoutExtent =
(_hasLayoutExtent ? 1.0 : 0.0) * _refreshIndicatorExtent;
if (layoutExtent != layoutExtentOffsetCompensation) {
geometry = SliverGeometry(
scrollOffsetCorrection: layoutExtent - layoutExtentOffsetCompensation,
);
layoutExtentOffsetCompensation = layoutExtent;
return;
}
final bool active = constraints.overlap < 0.0 || layoutExtent > 0.0;
final double overscrolledExtent =
constraints.overlap < 0.0 ? constraints.overlap.abs() : 0.0;
child!.layout(
constraints.asBoxConstraints(
maxExtent: layoutExtent
+ overscrolledExtent,
),
parentUsesSize: true,
);
if (active) {
geometry = SliverGeometry(
scrollExtent: layoutExtent,
paintOrigin: -overscrolledExtent - constraints.scrollOffset,
paintExtent: max(
max(child!.size.height, layoutExtent) - constraints.scrollOffset,
0.0,
),
maxPaintExtent: max(
max(child!.size.height, layoutExtent) - constraints.scrollOffset,
0.0,
),
layoutExtent: max(layoutExtent - constraints.scrollOffset, 0.0),
);
} else {
geometry = SliverGeometry.zero;
}
}
@override
void paint(PaintingContext paintContext, Offset offset) {
if (constraints.overlap < 0.0 ||
constraints.scrollOffset + child!.size.height > 0) {
paintContext.paintChild(child!, offset);
}
}
@override
void applyPaintTransform(RenderObject child, Matrix4 transform) { }
}
enum MyRefreshIndicatorMode {
inactive,
drag,
armed,
refresh,
done,
}
typedef RefreshControlIndicatorBuilder = Widget Function(
BuildContext context,
MyRefreshIndicatorMode? refreshState,
double pulledExtent,
double refreshTriggerPullDistance,
double refreshIndicatorExtent,
);
typedef RefreshCallback = Future Function();
class MyCupertinoSliverRefreshControl extends StatefulWidget {
const MyCupertinoSliverRefreshControl({
Key? key,
this.refreshTriggerPullDistance = _defaultRefreshTriggerPullDistance,
this.refreshIndicatorExtent = _defaultRefreshIndicatorExtent,
this.builder = buildSimpleRefreshIndicator,
this.onRefresh,
}) : assert(refreshTriggerPullDistance > 0.0),
assert(refreshIndicatorExtent >= 0.0),
assert(
refreshTriggerPullDistance >= refreshIndicatorExtent,
),
super(key: key);
final double refreshTriggerPullDistance;
final double refreshIndicatorExtent;
final RefreshControlIndicatorBuilder builder;
final RefreshCallback? onRefresh;
static const double _defaultRefreshTriggerPullDistance = 100.0;
static const double _defaultRefreshIndicatorExtent = 60.0;
@visibleForTesting
static MyRefreshIndicatorMode? state(BuildContext context) {
final MyCupertinoSliverRefreshControlState state
= context.findAncestorStateOfType()!;
return state.refreshState;
}
static Widget buildSimpleRefreshIndicator(
BuildContext context,
MyRefreshIndicatorMode? refreshState,
double pulledExtent,
double refreshTriggerPullDistance,
double refreshIndicatorExtent,
) {
const Curve opacityCurve = Interval(0.4, 0.8, curve: Curves.easeInOut);
return Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: refreshState == MyRefreshIndicatorMode.drag
? Opacity(
opacity: opacityCurve.transform(
min(pulledExtent / refreshTriggerPullDistance, 1.0)
),
child: const Icon(
CupertinoIcons.down_arrow,
color: CupertinoColors.inactiveGray,
size: 36.0,
),
)
: Opacity(
opacity: opacityCurve.transform(
min(pulledExtent / refreshIndicatorExtent, 1.0)
),
child: const CupertinoActivityIndicator(radius: 14.0),
),
),
);
}
@override
MyCupertinoSliverRefreshControlState createState() => MyCupertinoSliverRefreshControlState();
}
class MyCupertinoSliverRefreshControlState extends State {
static const double _inactiveResetOverscrollFraction = 0.1;
MyRefreshIndicatorMode? refreshState;
Future? refreshTask;
double latestIndicatorBoxExtent = 0.0;
bool hasSliverLayoutExtent = false;
bool needRefresh = false;
bool draging = false;
@override
void initState() {
super.initState();
refreshState = MyRefreshIndicatorMode.inactive;
}
MyRefreshIndicatorMode? transitionNextState() {
MyRefreshIndicatorMode? nextState;
void goToDone() {
nextState = MyRefreshIndicatorMode.done;
if (SchedulerBinding.instance!.schedulerPhase == SchedulerPhase.idle) {
setState(() => hasSliverLayoutExtent = false);
} else {
SchedulerBinding.instance!.addPostFrameCallback((Duration timestamp) {
setState(() => hasSliverLayoutExtent = false);
});
}
}
switch (refreshState) {
case MyRefreshIndicatorMode.inactive:
if (latestIndicatorBoxExtent <= 0) {
return MyRefreshIndicatorMode.inactive;
} else {
nextState = MyRefreshIndicatorMode.drag;
}
continue drag;
drag:
case MyRefreshIndicatorMode.drag:
if (latestIndicatorBoxExtent == 0) {
return MyRefreshIndicatorMode.inactive;
} else if (latestIndicatorBoxExtent < widget.refreshTriggerPullDistance) {
return MyRefreshIndicatorMode.drag;
} else {
///超过 refreshTriggerPullDistance 就可以进入准备刷新的装备状态
if (widget.onRefresh != null) {
HapticFeedback.mediumImpact();
SchedulerBinding.instance!.addPostFrameCallback((Duration timestamp) {
needRefresh = true;
setState(() => hasSliverLayoutExtent = true);
});
}
return MyRefreshIndicatorMode.armed;
}
case MyRefreshIndicatorMode.armed:
if (refreshState == MyRefreshIndicatorMode.armed && !needRefresh) {
goToDone();
continue done;
}
///当已经进去装备阶段,拖拽距离没到 refreshIndicatorExtent 的时候
///继续返回 armed 状态,知道 latestIndicatorBoxExtent = refreshIndicatorExtent
///才进入刷新状态
if (latestIndicatorBoxExtent > widget.refreshIndicatorExtent) {
return MyRefreshIndicatorMode.armed;
} else {
///如果这时候手还在拖拽
if(draging) {
goToDone();
continue done;
}
nextState = MyRefreshIndicatorMode.refresh;
}
continue refresh;
refresh:
case MyRefreshIndicatorMode.refresh:
///进入刷新状态,先判断是否达到刷新标准
if (needRefresh) {
///还没有触发外部刷新,触发一下
if (widget.onRefresh != null && refreshTask == null) {
HapticFeedback.mediumImpact();
SchedulerBinding.instance!.addPostFrameCallback((Duration timestamp) {
///任务完成后清洗状态
refreshTask = widget.onRefresh!()..whenComplete(() {
if (mounted) {
setState(() {
refreshTask = null;
needRefresh = false;
});
refreshState = transitionNextState();
}
});
setState(() => hasSliverLayoutExtent = true);
});
}
return MyRefreshIndicatorMode.refresh;
} else {
goToDone();
}
continue done;
done:
case MyRefreshIndicatorMode.done:
default:
///结束状态
if (latestIndicatorBoxExtent >
widget.refreshTriggerPullDistance * _inactiveResetOverscrollFraction) {
return MyRefreshIndicatorMode.done;
} else {
nextState = MyRefreshIndicatorMode.inactive;
}
break;
}
return nextState;
}
///增加外部判断,处理手是不是还在拖拽,如果还在拖拽不触发刷新
void notifyScrollNotification(ScrollNotification notification) {
if (notification is ScrollEndNotification) {
if(refreshState == MyRefreshIndicatorMode.armed) {
/// 放手了
draging = false;
}
} else if (notification is UserScrollNotification) {
if(notification.direction != ScrollDirection.idle) {
/// 手还在拖动
draging = true;
} else {
/// 放手了
draging = false;
}
}
}
@override
Widget build(BuildContext context) {
return _CupertinoSliverRefresh(
refreshIndicatorLayoutExtent: widget.refreshIndicatorExtent,
hasLayoutExtent: hasSliverLayoutExtent,
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
latestIndicatorBoxExtent = constraints.maxHeight;
refreshState = transitionNextState();
if (latestIndicatorBoxExtent > 0) {
return widget.builder(
context,
refreshState,
latestIndicatorBoxExtent,
widget.refreshTriggerPullDistance,
widget.refreshIndicatorExtent,
);
}
return Container();
},
),
);
}
}
其他一些说明
ListTitle:通常用于在 Flutter 中填充 ListView,系统自带的item,可以满足大多场景
demo
上主要是讲解了一些基本的用法,更详细的可参照demo
demo地址:https://github.com/liuyewu101/flutter_demo