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( // 总是可以滚动,不能滚动时无法触发下拉刷新,因此设置为总是能滚动 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
// 源码的最后部分,大概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. 代码
- 以下是完整代码,有注释的部分才是修改部分
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. 使用
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
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. 使用
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: , ), ), );
3.4. 效果
以上就是Flutter自定义下拉刷新时的loading样式的方法详解的详细内容,更多关于Flutter自定义loading样式的资料请关注脚本之家其它相关文章!
相关文章
Kotlin协程启动createCoroutine及创建startCoroutine原理
这篇文章主要为大家介绍了Kotlin协程启动createCoroutine及创建startCoroutine原理详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪2022-08-08Android学习笔记--Activity中使用Intent传值示例代码
Intent负责对应用中一次操作的动作、动作涉及数据、附加数据进行描述,Android则根据此Intent的描述,负责找到对应的组件,将Intent传递给调用的组件,并完成组件的调用2013-06-06详解flutter中常用的container layout实例
这篇文章主要为大家介绍了详解flutter中常用的container layout实例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪2022-09-09Android 让自定义TextView的drawableLeft与文本一起居中
本文主要介绍Android 自定义控件TextView显示居中问题,在开发过程中经常会遇到控件的重写,这里主要介绍TextView的drawableLeft与文本一起居中的问题2016-07-07Android开发实现去除bitmap无用白色边框的方法示例
这篇文章主要介绍了Android开发实现去除bitmap无用白色边框的方法,结合实例形式给出了Android去除bitmap无用白色边框的具体操作步骤与相关实现技巧,需要的朋友可以参考下2017-11-11
最新评论