說明
列表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<Widget> children = const <Widget>[],//子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 <Widget>[
ListTile(title: Text("普通ListView1")),
ListTile(title: Text("普通ListView2")),
ListTile(title: Text("普通ListView3")),
ListTile(title: Text("普通ListView4"))
]
)
2,builder
適用場景:長列表時采用builder模式,能提高性能。不是把所有子控件都構造出來,而是在控件viewport加上頭尾的cacheExtent這個范圍內的子Item才會被構造。在構造時傳遞一個builder,按需加載是一個慣用模式,能提高加載性能
List<ListItem> 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<HomePage>{
String currentText = "普通上下拉刷新1";
final int pageSize = 10;
List<ListItem> items = [];
bool disposed = false;
final ScrollController scrollController = ScrollController();
final GlobalKey<RefreshIndicatorState> refreshKey = GlobalKey();
@override
void dispose() {
disposed = true;
super.dispose();
}
Future<void> 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<void> 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<HomePage>{
String currentText = "普通上下拉刷新2";
final int pageSize = 10;
List<ListItem> items = [];
bool disposed = false;
final ScrollController scrollController = ScrollController();
final GlobalKey<RefreshIndicatorState> refreshKey = GlobalKey();
@override
void dispose() {
disposed = true;
super.dispose();
}
Future<void> 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<void> 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: <Widget>[
///控制顯示刷新的 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<HomePage>{
String currentText = "自定義上下拉刷新樣式";
final int pageSize = 10;
List<ListItem> items = [];
bool disposed = false;
final ScrollController scrollController = ScrollController();
final GlobalKey<MyCupertinoSliverRefreshControlState> sliverRefreshKey = GlobalKey<MyCupertinoSliverRefreshControlState>();
@override
void dispose() {
disposed = true;
super.dispose();
}
Future<void> 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<void> 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: <Widget>[
///控制顯示刷新的 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<RenderBox> {
_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<void> 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<MyCupertinoSliverRefreshControlState>()!;
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<MyCupertinoSliverRefreshControl> {
static const double _inactiveResetOverscrollFraction = 0.1;
MyRefreshIndicatorMode? refreshState;
Future<void>? 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