Flutter中的下拉刷新,我们通常
RefreshIndicator
,可以通过backgroundColor
,color
或strokeWidth
设置下拉刷新的颜色粗细等样式,但如果要自定义自己的widget,RefreshIndicator
并没有暴露出对应的属性,那如何修改呢?
1. 简单更改RefreshIndicator的样式
demo.dart
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(
// 总是可以滚动,不能滚动时无法触发下拉刷新,因此设置为总是能滚动
physics: const AlwaysScrollableScrollPhysics(),
// 滚动区域的内容
// child: ,
),
),
);
效果:
2. 自定义下拉loading的样式
查看
RefreshIndicator
的属性,我们可以发现并没有直接更改loading widget的方式。
- 我们查看源码,可以发现返回的
loading
主要是:RefreshProgressIndicator
和CupertinoActivityIndicator
两种。
.../flutter/packages/flutter/lib/src/material/refresh_indicator.dart
- 以下是部分源码:
- 我们注释掉源码中
loading
的部分,改为自己定义的样式 - 如果要自定义进出动画的话可以在替换更高层的widget,这里只替换
AnimatedBuilder
下的widget
dart
// 源码的最后部分,大概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"),
);
},
),
效果如下:
注:
- 直接修改源码会影响其他项目,且多人协作开发的话,其他人无法获得同样的效果的
- 本文的解决方案是将源码复制出来,重新命名后使用
2.1. 优化下拉回到顶部的时间
- 通过上面的效果,我们可以看到,下拉后,列表内容部分立即回到了顶部,这里希望刷新完成后,列表再回到顶部
最终效果:
2.1.1. 思路
- 先将源码拷贝出来,更改
widget
名称和Flutter的RefreshIndicator
区分开,再在源码基础上进行修改 - 刷新顶部如何不回弹?顶部增加一个
SizedBox
占位,根据下拉高度更改SizedBox
占位的高度,在源码中_positionController
可以获取到下拉的高度。 - 由于是滚动列表,因此使用
NestedScrollView
融合占位元素和滚动列表
2.1.2. 代码
- 以下是完整代码,有注释的部分才是修改部分
dart
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/foundation.dart' show clampDouble;
import 'package:flutter/material.dart';
// =========修改下拉比例触发刷新,源码18行左右=========
const double _kDragContainerExtentPercentage = 0.1;
const double _kDragSizeFactorLimit = 1;
// =========修改下拉比例触发刷新=========
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. 使用
dart
RefreshWidget(
color: Colors.blue,
onRefresh: () async {
await Future.delayed(Duration(seconds: 2));
},
child: Center(
child: SingleChildScrollView(
// 滚动区域的内容
// child: ,
),
),
);
3. 增加属性控制
根据上述的试验,我们优化一下,使下拉刷新组件更合理,新增以下两个属性:
keepScrollOffset
:自定义是否需要等待刷新完成后列表再回弹到顶部loadingWidget
:可以自定义loading样式,默认使用RefreshIndicator
的的loading
3.1. 难点与思路
难点:
- 占位元素的高度需要与用户传入的自定义
loading
的高度一致,如果写死的话,会导致类似这样的bug
思路:
- 占位
SizedBox
的child
设置为自定义的loading
,SizedBox
的高度不设置时,他的高度就是元素的高度 - 当处于正在刷新状态时,就将
SizedBox
的高度设置为null
遗留问题:
- 目前代码中写死了默认高度55(参照我完整代码的396行),如果传入的自定义
loading
高度大于55,松开时会有一点弹跳效果,暂时没有找到更好的解决方案,如果大家有更好的方案欢迎讨论一下
3.2. 完整代码
lib/widget/refresh_widget.dart
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';
// =========修改下拉比例触发刷新,源码18行左右=========
const double _kDragContainerExtentPercentage = 0.1;
const double _kDragSizeFactorLimit = 1;
// =========修改下拉比例触发刷新=========
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. 使用
dart
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(
// 滚动区域的内容
// child: ,
),
),
);