Flutter自定義下拉刷新時的loading樣式的方法詳解
前言
Flutter中的下拉刷新,我們通常RefreshIndicator,可以通過backgroundColor,color或strokeWidth設置下拉刷新的顏色粗細等樣式,但如果要自定義自己的widget,RefreshIndicator并沒有暴露出對應的屬性,那如何修改呢?
1. 簡單更改RefreshIndicator的樣式
demo.dart
RefreshIndicator(
backgroundColor: Colors.amber, // 滾動loading的背景色
color: Colors.blue, // 滾動loading線條的顏色
strokeWidth: 10, // 滾動loading的粗細
onRefresh: () async {
await Future.delayed(Duration(seconds: 2));
},
child: Center(
child: SingleChildScrollView(
// 總是可以滾動,不能滾動時無法觸發(fā)下拉刷新,因此設置為總是能滾動
physics: const AlwaysScrollableScrollPhysics(),
// 滾動區(qū)域的內容
// child: ,
),
),
);
效果:

2. 自定義下拉loading的樣式
查看
RefreshIndicator的屬性,我們可以發(fā)現并沒有直接更改loading widget的方式。
- 我們查看源碼,可以發(fā)現返回的
loading主要是:RefreshProgressIndicator和CupertinoActivityIndicator兩種。
.../flutter/packages/flutter/lib/src/material/refresh_indicator.dart
- 以下是部分源碼:
- 我們注釋掉源碼中
loading的部分,改為自己定義的樣式 - 如果要自定義進出動畫的話可以在替換更高層的widget,這里只替換
AnimatedBuilder下的widget
// 源碼的最后部分,大概619行左右
child: AnimatedBuilder(
animation: _positionController,
builder: (BuildContext context, Widget? child) {
// 以下widget就是下拉時顯示的loading,我們注釋掉
// final Widget materialIndicator = RefreshProgressIndicator(
// semanticsLabel: widget.semanticsLabel ??
// MaterialLocalizations.of(context)
// .refreshIndicatorSemanticLabel,
// semanticsValue: widget.semanticsValue,
// value: showIndeterminateIndicator ? null : _value.value,
// valueColor: _valueColor,
// backgroundColor: widget.backgroundColor,
// strokeWidth: widget.strokeWidth,
// );
// final Widget cupertinoIndicator =
// CupertinoActivityIndicator(
// color: widget.color,
// );
// switch (widget._indicatorType) {
// case _IndicatorType.material:
// return materialIndicator;
// case _IndicatorType.adaptive:
// {
// final ThemeData theme = Theme.of(context);
// switch (theme.platform) {
// case TargetPlatform.android:
// case TargetPlatform.fuchsia:
// case TargetPlatform.linux:
// case TargetPlatform.windows:
// return materialIndicator;
// case TargetPlatform.iOS:
// case TargetPlatform.macOS:
// return cupertinoIndicator;
// }
// }
// }
// 改為自己定義的樣式
return Container(
color: widget.color,
width: 100,
height: 100,
child: Text("loading"),
);
},
),
效果如下:

注:
- 直接修改源碼會影響其他項目,且多人協(xié)作開發(fā)的話,其他人無法獲得同樣的效果的
- 本文的解決方案是將源碼復制出來,重新命名后使用
2.1. 優(yōu)化下拉回到頂部的時間
- 通過上面的效果,我們可以看到,下拉后,列表內容部分立即回到了頂部,這里希望刷新完成后,列表再回到頂部
最終效果:

2.1.1. 思路
- 先將源碼拷貝出來,更改
widget名稱和Flutter的RefreshIndicator區(qū)分開,再在源碼基礎上進行修改 - 刷新頂部如何不回彈?頂部增加一個
SizedBox占位,根據下拉高度更改SizedBox占位的高度,在源碼中_positionController可以獲取到下拉的高度。 - 由于是滾動列表,因此使用
NestedScrollView融合占位元素和滾動列表
2.1.2. 代碼
- 以下是完整代碼,有注釋的部分才是修改部分
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/foundation.dart' show clampDouble;
import 'package:flutter/material.dart';
// =========修改下拉比例觸發(fā)刷新,源碼18行左右=========
const double _kDragContainerExtentPercentage = 0.1;
const double _kDragSizeFactorLimit = 1;
// =========修改下拉比例觸發(fā)刷新=========
const Duration _kIndicatorSnapDuration = Duration(milliseconds: 150);
const Duration _kIndicatorScaleDuration = Duration(milliseconds: 200);
typedef RefreshCallback = Future<void> Function();
enum _RefreshIndicatorMode {
drag, // Pointer is down.
armed, // Dragged far enough that an up event will run the onRefresh callback.
snap, // Animating to the indicator's final "displacement".
refresh, // Running the refresh callback.
done, // Animating the indicator's fade-out after refreshing.
canceled, // Animating the indicator's fade-out after not arming.
}
/// Used to configure how [RefreshIndicator] can be triggered.
enum RefreshIndicatorTriggerMode {
anywhere,
onEdge,
}
enum _IndicatorType { material, adaptive }
// ======更改名字,源碼119行左右======
class RefreshWidget extends StatefulWidget {
const RefreshWidget({
super.key,
required this.child,
this.displacement = 40.0,
this.edgeOffset = 0.0,
required this.onRefresh,
this.color,
this.backgroundColor,
this.notificationPredicate = defaultScrollNotificationPredicate,
this.semanticsLabel,
this.semanticsValue,
this.strokeWidth = RefreshProgressIndicator.defaultStrokeWidth,
this.triggerMode = RefreshIndicatorTriggerMode.onEdge,
}) : _indicatorType = _IndicatorType.material;
const RefreshWidget.adaptive({
super.key,
required this.child,
this.displacement = 40.0,
this.edgeOffset = 0.0,
required this.onRefresh,
this.color,
this.backgroundColor,
this.notificationPredicate = defaultScrollNotificationPredicate,
this.semanticsLabel,
this.semanticsValue,
this.strokeWidth = RefreshProgressIndicator.defaultStrokeWidth,
this.triggerMode = RefreshIndicatorTriggerMode.onEdge,
}) : _indicatorType = _IndicatorType.adaptive;
final Widget child;
final double displacement;
final double edgeOffset;
final RefreshCallback onRefresh;
final Color? color;
final Color? backgroundColor;
final ScrollNotificationPredicate notificationPredicate;
final String? semanticsLabel;
final String? semanticsValue;
final double strokeWidth;
final _IndicatorType _indicatorType;
final RefreshIndicatorTriggerMode triggerMode;
@override
RefreshWidgetState createState() => RefreshWidgetState();
}
// 改名稱,源碼266行左右
class RefreshWidgetState extends State<RefreshWidget>
with TickerProviderStateMixin<RefreshWidget> {
late AnimationController _positionController;
late AnimationController _scaleController;
late Animation<double> _positionFactor;
late Animation<double> _scaleFactor;
late Animation<double> _value;
late Animation<Color?> _valueColor;
_RefreshIndicatorMode? _mode;
late Future<void> _pendingRefreshFuture;
bool? _isIndicatorAtTop;
double? _dragOffset;
late Color _effectiveValueColor =
widget.color ?? Theme.of(context).colorScheme.primary;
static final Animatable<double> _threeQuarterTween =
Tween<double>(begin: 0.0, end: 0.75);
static final Animatable<double> _kDragSizeFactorLimitTween =
Tween<double>(begin: 0.0, end: _kDragSizeFactorLimit);
static final Animatable<double> _oneToZeroTween =
Tween<double>(begin: 1.0, end: 0.0);
@override
void initState() {
super.initState();
_positionController = AnimationController(vsync: this);
_positionFactor = _positionController.drive(_kDragSizeFactorLimitTween);
_value = _positionController.drive(
_threeQuarterTween); // The "value" of the circular progress indicator during a drag.
_scaleController = AnimationController(vsync: this);
_scaleFactor = _scaleController.drive(_oneToZeroTween);
}
@override
void didChangeDependencies() {
_setupColorTween();
super.didChangeDependencies();
}
@override
void didUpdateWidget(covariant RefreshWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.color != widget.color) {
_setupColorTween();
}
}
@override
void dispose() {
_positionController.dispose();
_scaleController.dispose();
super.dispose();
}
void _setupColorTween() {
// Reset the current value color.
_effectiveValueColor =
widget.color ?? Theme.of(context).colorScheme.primary;
final Color color = _effectiveValueColor;
if (color.alpha == 0x00) {
// Set an always stopped animation instead of a driven tween.
_valueColor = AlwaysStoppedAnimation<Color>(color);
} else {
// Respect the alpha of the given color.
_valueColor = _positionController.drive(
ColorTween(
begin: color.withAlpha(0),
end: color.withAlpha(color.alpha),
).chain(
CurveTween(
curve: const Interval(0.0, 1.0 / _kDragSizeFactorLimit),
),
),
);
}
}
bool _shouldStart(ScrollNotification notification) {
return ((notification is ScrollStartNotification &&
notification.dragDetails != null) ||
(notification is ScrollUpdateNotification &&
notification.dragDetails != null &&
widget.triggerMode == RefreshIndicatorTriggerMode.anywhere)) &&
((notification.metrics.axisDirection == AxisDirection.up &&
notification.metrics.extentAfter == 0.0) ||
(notification.metrics.axisDirection == AxisDirection.down &&
notification.metrics.extentBefore == 0.0)) &&
_mode == null &&
_start(notification.metrics.axisDirection);
}
bool _handleScrollNotification(ScrollNotification notification) {
if (!widget.notificationPredicate(notification)) {
return false;
}
if (_shouldStart(notification)) {
setState(() {
_mode = _RefreshIndicatorMode.drag;
});
return false;
}
bool? indicatorAtTopNow;
switch (notification.metrics.axisDirection) {
case AxisDirection.down:
case AxisDirection.up:
indicatorAtTopNow = true;
case AxisDirection.left:
case AxisDirection.right:
indicatorAtTopNow = null;
}
if (indicatorAtTopNow != _isIndicatorAtTop) {
if (_mode == _RefreshIndicatorMode.drag ||
_mode == _RefreshIndicatorMode.armed) {
_dismiss(_RefreshIndicatorMode.canceled);
}
} else if (notification is ScrollUpdateNotification) {
if (_mode == _RefreshIndicatorMode.drag ||
_mode == _RefreshIndicatorMode.armed) {
if ((notification.metrics.axisDirection == AxisDirection.down &&
notification.metrics.extentBefore > 0.0) ||
(notification.metrics.axisDirection == AxisDirection.up &&
notification.metrics.extentAfter > 0.0)) {
_dismiss(_RefreshIndicatorMode.canceled);
} else {
if (notification.metrics.axisDirection == AxisDirection.down) {
_dragOffset = _dragOffset! - notification.scrollDelta!;
} else if (notification.metrics.axisDirection == AxisDirection.up) {
_dragOffset = _dragOffset! + notification.scrollDelta!;
}
_checkDragOffset(notification.metrics.viewportDimension);
}
}
if (_mode == _RefreshIndicatorMode.armed &&
notification.dragDetails == null) {
_show();
}
} else if (notification is OverscrollNotification) {
if (_mode == _RefreshIndicatorMode.drag ||
_mode == _RefreshIndicatorMode.armed) {
if (notification.metrics.axisDirection == AxisDirection.down) {
_dragOffset = _dragOffset! - notification.overscroll;
} else if (notification.metrics.axisDirection == AxisDirection.up) {
_dragOffset = _dragOffset! + notification.overscroll;
}
_checkDragOffset(notification.metrics.viewportDimension);
}
} else if (notification is ScrollEndNotification) {
switch (_mode) {
case _RefreshIndicatorMode.armed:
_show();
case _RefreshIndicatorMode.drag:
_dismiss(_RefreshIndicatorMode.canceled);
case _RefreshIndicatorMode.canceled:
case _RefreshIndicatorMode.done:
case _RefreshIndicatorMode.refresh:
case _RefreshIndicatorMode.snap:
case null:
// do nothing
break;
}
}
return false;
}
bool _handleIndicatorNotification(
OverscrollIndicatorNotification notification) {
if (notification.depth != 0 || !notification.leading) {
return false;
}
if (_mode == _RefreshIndicatorMode.drag) {
notification.disallowIndicator();
return true;
}
return false;
}
bool _start(AxisDirection direction) {
assert(_mode == null);
assert(_isIndicatorAtTop == null);
assert(_dragOffset == null);
switch (direction) {
case AxisDirection.down:
case AxisDirection.up:
_isIndicatorAtTop = true;
case AxisDirection.left:
case AxisDirection.right:
_isIndicatorAtTop = null;
return false;
}
_dragOffset = 0.0;
_scaleController.value = 0.0;
_positionController.value = 0.0;
return true;
}
void _checkDragOffset(double containerExtent) {
assert(_mode == _RefreshIndicatorMode.drag ||
_mode == _RefreshIndicatorMode.armed);
double newValue =
_dragOffset! / (containerExtent * _kDragContainerExtentPercentage);
if (_mode == _RefreshIndicatorMode.armed) {
newValue = math.max(newValue, 1.0 / _kDragSizeFactorLimit);
}
_positionController.value =
clampDouble(newValue, 0.0, 1.0); // this triggers various rebuilds
if (_mode == _RefreshIndicatorMode.drag &&
_valueColor.value!.alpha == _effectiveValueColor.alpha) {
_mode = _RefreshIndicatorMode.armed;
}
}
// Stop showing the refresh indicator.
Future<void> _dismiss(_RefreshIndicatorMode newMode) async {
await Future<void>.value();
assert(newMode == _RefreshIndicatorMode.canceled ||
newMode == _RefreshIndicatorMode.done);
setState(() {
_mode = newMode;
});
switch (_mode!) {
// ===========刷新完成,需要將_positionController置為0,源碼498行左右=========
case _RefreshIndicatorMode.done:
await Future.wait([
_scaleController.animateTo(1.0, duration: _kIndicatorScaleDuration),
_positionController.animateTo(0.0, duration: _kIndicatorScaleDuration)
]);
// ===========刷新完成,需要將_positionController置為0=========
case _RefreshIndicatorMode.canceled:
await _positionController.animateTo(0.0,
duration: _kIndicatorScaleDuration);
case _RefreshIndicatorMode.armed:
case _RefreshIndicatorMode.drag:
case _RefreshIndicatorMode.refresh:
case _RefreshIndicatorMode.snap:
assert(false);
}
if (mounted && _mode == newMode) {
_dragOffset = null;
_isIndicatorAtTop = null;
setState(() {
_mode = null;
});
}
}
void _show() {
assert(_mode != _RefreshIndicatorMode.refresh);
assert(_mode != _RefreshIndicatorMode.snap);
final Completer<void> completer = Completer<void>();
_pendingRefreshFuture = completer.future;
_mode = _RefreshIndicatorMode.snap;
_positionController
.animateTo(1.0 / _kDragSizeFactorLimit,
duration: _kIndicatorSnapDuration)
.then<void>((void value) {
if (mounted && _mode == _RefreshIndicatorMode.snap) {
setState(() {
// Show the indeterminate progress indicator.
_mode = _RefreshIndicatorMode.refresh;
});
final Future<void> refreshResult = widget.onRefresh();
refreshResult.whenComplete(() {
if (mounted && _mode == _RefreshIndicatorMode.refresh) {
completer.complete();
_dismiss(_RefreshIndicatorMode.done);
}
});
}
});
}
Future<void> show({bool atTop = true}) {
if (_mode != _RefreshIndicatorMode.refresh &&
_mode != _RefreshIndicatorMode.snap) {
if (_mode == null) {
_start(atTop ? AxisDirection.down : AxisDirection.up);
}
_show();
}
return _pendingRefreshFuture;
}
@override
Widget build(BuildContext context) {
// assert(debugCheckHasMaterialLocalizations(context));
final Widget child = NotificationListener<ScrollNotification>(
onNotification: _handleScrollNotification,
child: NotificationListener<OverscrollIndicatorNotification>(
onNotification: _handleIndicatorNotification,
child: widget.child,
),
);
assert(() {
if (_mode == null) {
assert(_dragOffset == null);
assert(_isIndicatorAtTop == null);
} else {
assert(_dragOffset != null);
assert(_isIndicatorAtTop != null);
}
return true;
}());
final bool showIndeterminateIndicator =
_mode == _RefreshIndicatorMode.refresh ||
_mode == _RefreshIndicatorMode.done;
return Stack(
children: <Widget>[
// ============增加占位,源碼600行左右=================
NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) {
return [
SliverToBoxAdapter(
child: AnimatedBuilder(
animation: _positionController,
builder: (context, _) {
// 50是我loading動畫的高度,因此這里寫死了
return SizedBox(height: 50 * _positionController.value);
}),
)
];
},
body: child,
),
// ============增加占位=================
if (_mode != null)
Positioned(
top: _isIndicatorAtTop! ? widget.edgeOffset : null,
bottom: !_isIndicatorAtTop! ? widget.edgeOffset : null,
left: 0.0,
right: 0.0,
child: SizeTransition(
axisAlignment: _isIndicatorAtTop! ? 1.0 : -1.0,
sizeFactor: _positionFactor, // this is what brings it down
// ============修改返回的loading樣式=================
child: Container(
alignment: _isIndicatorAtTop!
? Alignment.topCenter
: Alignment.bottomCenter,
child: ScaleTransition(
scale: _scaleFactor,
child: Container(
color: widget.color,
width: 50,
height: 50,
child: const Text("loading"),
),
),
),
// ============修改返回的loading樣式=================
),
),
],
);
}
}
2.1.3. 使用
RefreshWidget(
color: Colors.blue,
onRefresh: () async {
await Future.delayed(Duration(seconds: 2));
},
child: Center(
child: SingleChildScrollView(
// 滾動區(qū)域的內容
// child: ,
),
),
);
3. 增加屬性控制
根據上述的試驗,我們優(yōu)化一下,使下拉刷新組件更合理,新增以下兩個屬性:
keepScrollOffset:自定義是否需要等待刷新完成后列表再回彈到頂部loadingWidget:可以自定義loading樣式,默認使用RefreshIndicator的的loading
3.1. 難點與思路
難點:
- 占位元素的高度需要與用戶傳入的自定義
loading的高度一致,如果寫死的話,會導致類似這樣的bug

思路:
- 占位
SizedBox的child設置為自定義的loading,SizedBox的高度不設置時,他的高度就是元素的高度 - 當處于正在刷新狀態(tài)時,就將
SizedBox的高度設置為null
遺留問題:
- 目前代碼中寫死了默認高度55(參照我完整代碼的396行),如果傳入的自定義
loading高度大于55,松開時會有一點彈跳效果,暫時沒有找到更好的解決方案,如果大家有更好的方案歡迎討論一下
3.2. 完整代碼
lib/widget/refresh_widget.dart
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart' show clampDouble;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
// =========修改下拉比例觸發(fā)刷新,源碼18行左右=========
const double _kDragContainerExtentPercentage = 0.1;
const double _kDragSizeFactorLimit = 1;
// =========修改下拉比例觸發(fā)刷新=========
const Duration _kIndicatorSnapDuration = Duration(milliseconds: 150);
const Duration _kIndicatorScaleDuration = Duration(milliseconds: 200);
typedef RefreshCallback = Future<void> Function();
enum _RefreshIndicatorMode {
drag, // Pointer is down.
armed, // Dragged far enough that an up event will run the onRefresh callback.
snap, // Animating to the indicator's final "displacement".
refresh, // Running the refresh callback.
done, // Animating the indicator's fade-out after refreshing.
canceled, // Animating the indicator's fade-out after not arming.
}
/// Used to configure how [RefreshIndicator] can be triggered.
enum RefreshIndicatorTriggerMode {
anywhere,
onEdge,
}
enum _IndicatorType { material, adaptive }
// ======更改名字,源碼119行左右======
class RefreshWidget extends StatefulWidget {
const RefreshWidget({
super.key,
this.loadingWidget,
this.keepScrollOffset = false,
required this.child,
this.displacement = 40.0,
this.edgeOffset = 0.0,
required this.onRefresh,
this.color,
this.backgroundColor,
this.notificationPredicate = defaultScrollNotificationPredicate,
this.semanticsLabel,
this.semanticsValue,
this.strokeWidth = RefreshProgressIndicator.defaultStrokeWidth,
this.triggerMode = RefreshIndicatorTriggerMode.onEdge,
}) : _indicatorType = _IndicatorType.material;
const RefreshWidget.adaptive({
super.key,
this.loadingWidget,
this.keepScrollOffset = false,
required this.child,
this.displacement = 40.0,
this.edgeOffset = 0.0,
required this.onRefresh,
this.color,
this.backgroundColor,
this.notificationPredicate = defaultScrollNotificationPredicate,
this.semanticsLabel,
this.semanticsValue,
this.strokeWidth = RefreshProgressIndicator.defaultStrokeWidth,
this.triggerMode = RefreshIndicatorTriggerMode.onEdge,
}) : _indicatorType = _IndicatorType.adaptive;
// 自定義loading
final Widget? loadingWidget;
// 刷新時是否保留頂部的偏移
final bool keepScrollOffset;
final Widget child;
final double displacement;
final double edgeOffset;
final RefreshCallback onRefresh;
final Color? color;
final Color? backgroundColor;
final ScrollNotificationPredicate notificationPredicate;
final String? semanticsLabel;
final String? semanticsValue;
final double strokeWidth;
final _IndicatorType _indicatorType;
final RefreshIndicatorTriggerMode triggerMode;
@override
RefreshWidgetState createState() => RefreshWidgetState();
}
// 改名稱,源碼266行左右
class RefreshWidgetState extends State<RefreshWidget>
with TickerProviderStateMixin<RefreshWidget> {
late AnimationController _positionController;
late AnimationController _scaleController;
late Animation<double> _positionFactor;
late Animation<double> _scaleFactor;
late Animation<double> _value;
late Animation<Color?> _valueColor;
_RefreshIndicatorMode? _mode;
late Future<void> _pendingRefreshFuture;
bool? _isIndicatorAtTop;
double? _dragOffset;
late Color _effectiveValueColor =
widget.color ?? Theme.of(context).colorScheme.primary;
static final Animatable<double> _threeQuarterTween =
Tween<double>(begin: 0.0, end: 0.75);
static final Animatable<double> _kDragSizeFactorLimitTween =
Tween<double>(begin: 0.0, end: _kDragSizeFactorLimit);
static final Animatable<double> _oneToZeroTween =
Tween<double>(begin: 1.0, end: 0.0);
@override
void initState() {
super.initState();
_positionController = AnimationController(vsync: this);
_positionFactor = _positionController.drive(_kDragSizeFactorLimitTween);
_value = _positionController.drive(
_threeQuarterTween); // The "value" of the circular progress indicator during a drag.
_scaleController = AnimationController(vsync: this);
_scaleFactor = _scaleController.drive(_oneToZeroTween);
}
@override
void didChangeDependencies() {
_setupColorTween();
super.didChangeDependencies();
}
@override
void didUpdateWidget(covariant RefreshWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.color != widget.color) {
_setupColorTween();
}
}
@override
void dispose() {
_positionController.dispose();
_scaleController.dispose();
super.dispose();
}
void _setupColorTween() {
// Reset the current value color.
_effectiveValueColor =
widget.color ?? Theme.of(context).colorScheme.primary;
final Color color = _effectiveValueColor;
if (color.alpha == 0x00) {
// Set an always stopped animation instead of a driven tween.
_valueColor = AlwaysStoppedAnimation<Color>(color);
} else {
// Respect the alpha of the given color.
_valueColor = _positionController.drive(
ColorTween(
begin: color.withAlpha(0),
end: color.withAlpha(color.alpha),
).chain(
CurveTween(
curve: const Interval(0.0, 1.0 / _kDragSizeFactorLimit),
),
),
);
}
}
bool _shouldStart(ScrollNotification notification) {
return ((notification is ScrollStartNotification &&
notification.dragDetails != null) ||
(notification is ScrollUpdateNotification &&
notification.dragDetails != null &&
widget.triggerMode == RefreshIndicatorTriggerMode.anywhere)) &&
((notification.metrics.axisDirection == AxisDirection.up &&
notification.metrics.extentAfter == 0.0) ||
(notification.metrics.axisDirection == AxisDirection.down &&
notification.metrics.extentBefore == 0.0)) &&
_mode == null &&
_start(notification.metrics.axisDirection);
}
bool _handleScrollNotification(ScrollNotification notification) {
if (!widget.notificationPredicate(notification)) {
return false;
}
if (_shouldStart(notification)) {
setState(() {
_mode = _RefreshIndicatorMode.drag;
});
return false;
}
bool? indicatorAtTopNow;
switch (notification.metrics.axisDirection) {
case AxisDirection.down:
case AxisDirection.up:
indicatorAtTopNow = true;
case AxisDirection.left:
case AxisDirection.right:
indicatorAtTopNow = null;
}
if (indicatorAtTopNow != _isIndicatorAtTop) {
if (_mode == _RefreshIndicatorMode.drag ||
_mode == _RefreshIndicatorMode.armed) {
_dismiss(_RefreshIndicatorMode.canceled);
}
} else if (notification is ScrollUpdateNotification) {
if (_mode == _RefreshIndicatorMode.drag ||
_mode == _RefreshIndicatorMode.armed) {
if ((notification.metrics.axisDirection == AxisDirection.down &&
notification.metrics.extentBefore > 0.0) ||
(notification.metrics.axisDirection == AxisDirection.up &&
notification.metrics.extentAfter > 0.0)) {
_dismiss(_RefreshIndicatorMode.canceled);
} else {
if (notification.metrics.axisDirection == AxisDirection.down) {
_dragOffset = _dragOffset! - notification.scrollDelta!;
} else if (notification.metrics.axisDirection == AxisDirection.up) {
_dragOffset = _dragOffset! + notification.scrollDelta!;
}
_checkDragOffset(notification.metrics.viewportDimension);
}
}
if (_mode == _RefreshIndicatorMode.armed &&
notification.dragDetails == null) {
_show();
}
} else if (notification is OverscrollNotification) {
if (_mode == _RefreshIndicatorMode.drag ||
_mode == _RefreshIndicatorMode.armed) {
if (notification.metrics.axisDirection == AxisDirection.down) {
_dragOffset = _dragOffset! - notification.overscroll;
} else if (notification.metrics.axisDirection == AxisDirection.up) {
_dragOffset = _dragOffset! + notification.overscroll;
}
_checkDragOffset(notification.metrics.viewportDimension);
}
} else if (notification is ScrollEndNotification) {
switch (_mode) {
case _RefreshIndicatorMode.armed:
_show();
case _RefreshIndicatorMode.drag:
_dismiss(_RefreshIndicatorMode.canceled);
case _RefreshIndicatorMode.canceled:
case _RefreshIndicatorMode.done:
case _RefreshIndicatorMode.refresh:
case _RefreshIndicatorMode.snap:
case null:
// do nothing
break;
}
}
return false;
}
bool _handleIndicatorNotification(
OverscrollIndicatorNotification notification) {
if (notification.depth != 0 || !notification.leading) {
return false;
}
if (_mode == _RefreshIndicatorMode.drag) {
notification.disallowIndicator();
return true;
}
return false;
}
bool _start(AxisDirection direction) {
assert(_mode == null);
assert(_isIndicatorAtTop == null);
assert(_dragOffset == null);
switch (direction) {
case AxisDirection.down:
case AxisDirection.up:
_isIndicatorAtTop = true;
case AxisDirection.left:
case AxisDirection.right:
_isIndicatorAtTop = null;
return false;
}
_dragOffset = 0.0;
_scaleController.value = 0.0;
_positionController.value = 0.0;
return true;
}
void _checkDragOffset(double containerExtent) {
assert(_mode == _RefreshIndicatorMode.drag ||
_mode == _RefreshIndicatorMode.armed);
double newValue =
_dragOffset! / (containerExtent * _kDragContainerExtentPercentage);
if (_mode == _RefreshIndicatorMode.armed) {
newValue = math.max(newValue, 1.0 / _kDragSizeFactorLimit);
}
_positionController.value =
clampDouble(newValue, 0.0, 1.0); // this triggers various rebuilds
if (_mode == _RefreshIndicatorMode.drag &&
_valueColor.value!.alpha == _effectiveValueColor.alpha) {
_mode = _RefreshIndicatorMode.armed;
}
}
// Stop showing the refresh indicator.
Future<void> _dismiss(_RefreshIndicatorMode newMode) async {
await Future<void>.value();
assert(newMode == _RefreshIndicatorMode.canceled ||
newMode == _RefreshIndicatorMode.done);
setState(() {
_mode = newMode;
});
switch (_mode!) {
// ===========刷新完成,需要將_positionController置為0,源碼498行左右=========
case _RefreshIndicatorMode.done:
await Future.wait([
_scaleController.animateTo(1.0, duration: _kIndicatorScaleDuration),
_positionController.animateTo(0.0, duration: _kIndicatorScaleDuration)
]);
// ===========刷新完成,需要將_positionController置為0=========
case _RefreshIndicatorMode.canceled:
await _positionController.animateTo(0.0,
duration: _kIndicatorScaleDuration);
case _RefreshIndicatorMode.armed:
case _RefreshIndicatorMode.drag:
case _RefreshIndicatorMode.refresh:
case _RefreshIndicatorMode.snap:
assert(false);
}
if (mounted && _mode == newMode) {
_dragOffset = null;
_isIndicatorAtTop = null;
setState(() {
_mode = null;
});
}
}
void _show() {
assert(_mode != _RefreshIndicatorMode.refresh);
assert(_mode != _RefreshIndicatorMode.snap);
final Completer<void> completer = Completer<void>();
_pendingRefreshFuture = completer.future;
_mode = _RefreshIndicatorMode.snap;
_positionController
.animateTo(1.0 / _kDragSizeFactorLimit,
duration: _kIndicatorSnapDuration)
.then<void>((void value) {
if (mounted && _mode == _RefreshIndicatorMode.snap) {
setState(() {
// Show the indeterminate progress indicator.
_mode = _RefreshIndicatorMode.refresh;
});
final Future<void> refreshResult = widget.onRefresh();
refreshResult.whenComplete(() {
if (mounted && _mode == _RefreshIndicatorMode.refresh) {
completer.complete();
_dismiss(_RefreshIndicatorMode.done);
}
});
}
});
}
Future<void> show({bool atTop = true}) {
if (_mode != _RefreshIndicatorMode.refresh &&
_mode != _RefreshIndicatorMode.snap) {
if (_mode == null) {
_start(atTop ? AxisDirection.down : AxisDirection.up);
}
_show();
}
return _pendingRefreshFuture;
}
// 計算占位元素的高度
double? calcHeight(double percent) {
// 刷新時不保留占位
if (!widget.keepScrollOffset) return 0;
// 55是默認loading動畫的高度,如果傳入的自定義loading高度大于55,松開時會有一點彈跳效果,暫時沒有找到好的結局方案,如果你有好的解決方案,希望分享一下
if (widget.loadingWidget == null) {
return 55 * percent;
}
if (_mode != _RefreshIndicatorMode.refresh) {
return 55 * percent;
}
return null;
}
@override
Widget build(BuildContext context) {
// assert(debugCheckHasMaterialLocalizations(context));
final Widget child = NotificationListener<ScrollNotification>(
onNotification: _handleScrollNotification,
child: NotificationListener<OverscrollIndicatorNotification>(
onNotification: _handleIndicatorNotification,
child: widget.child,
),
);
assert(() {
if (_mode == null) {
assert(_dragOffset == null);
assert(_isIndicatorAtTop == null);
} else {
assert(_dragOffset != null);
assert(_isIndicatorAtTop != null);
}
return true;
}());
final bool showIndeterminateIndicator =
_mode == _RefreshIndicatorMode.refresh ||
_mode == _RefreshIndicatorMode.done;
return Stack(
children: <Widget>[
// ============增加占位=================
NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) {
return [
SliverToBoxAdapter(
child: AnimatedBuilder(
animation: _positionController,
builder: (context, _) {
// 占位元素
return SizedBox(
height: calcHeight(_positionController.value),
child: Opacity(
opacity: 0,
child: widget.loadingWidget,
),
);
}),
)
];
},
body: child,
),
if (_mode != null)
Positioned(
top: _isIndicatorAtTop! ? widget.edgeOffset : null,
bottom: !_isIndicatorAtTop! ? widget.edgeOffset : null,
left: 0.0,
right: 0.0,
child: SizeTransition(
axisAlignment: _isIndicatorAtTop! ? 1.0 : -1.0,
sizeFactor: _positionFactor, // this is what brings it down
child: Container(
alignment: _isIndicatorAtTop!
? Alignment.topCenter
: Alignment.bottomCenter,
child: ScaleTransition(
scale: _scaleFactor,
// ============自定loading或使用默認loading=================
child: widget.loadingWidget ??
AnimatedBuilder(
animation: _positionController,
builder: (BuildContext context, Widget? child) {
final Widget materialIndicator =
RefreshProgressIndicator(
semanticsLabel: widget.semanticsLabel ??
MaterialLocalizations.of(context)
.refreshIndicatorSemanticLabel,
semanticsValue: widget.semanticsValue,
value: showIndeterminateIndicator
? null
: _value.value,
valueColor: _valueColor,
backgroundColor: widget.backgroundColor,
strokeWidth: widget.strokeWidth,
);
final Widget cupertinoIndicator =
CupertinoActivityIndicator(
color: widget.color,
);
switch (widget._indicatorType) {
case _IndicatorType.material:
return materialIndicator;
case _IndicatorType.adaptive:
{
final ThemeData theme = Theme.of(context);
switch (theme.platform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
return materialIndicator;
case TargetPlatform.iOS:
case TargetPlatform.macOS:
return cupertinoIndicator;
}
}
}
},
),
),
),
),
),
],
);
}
}
3.3. 使用
RefreshWidget(
keepScrollOffset: true, // 刷新時是否保留頂部偏移,默認不保留
loadingWidget: Container(
height: 30,
width: 100,
color: Colors.amber,
alignment: Alignment.center,
child: const Text('正在加載...'),
),
onRefresh: () async {
await Future.delayed(Duration(seconds: 2));
},
child: Center(
child: SingleChildScrollView(
// 滾動區(qū)域的內容
// child: ,
),
),
);
3.4. 效果

以上就是Flutter自定義下拉刷新時的loading樣式的方法詳解的詳細內容,更多關于Flutter自定義loading樣式的資料請關注腳本之家其它相關文章!

