Flutter自定義下拉刷新時(shí)的loading樣式的方法詳解
前言
Flutter中的下拉刷新,我們通常RefreshIndicator
,可以通過(guò)backgroundColor
,color
或strokeWidth
設(shè)置下拉刷新的顏色粗細(xì)等樣式,但如果要自定義自己的widget,RefreshIndicator
并沒(méi)有暴露出對(duì)應(yīng)的屬性,那如何修改呢?
1. 簡(jiǎn)單更改RefreshIndicator的樣式
demo.dart
RefreshIndicator( backgroundColor: Colors.amber, // 滾動(dòng)loading的背景色 color: Colors.blue, // 滾動(dòng)loading線條的顏色 strokeWidth: 10, // 滾動(dòng)loading的粗細(xì) onRefresh: () async { await Future.delayed(Duration(seconds: 2)); }, child: Center( child: SingleChildScrollView( // 總是可以滾動(dòng),不能滾動(dòng)時(shí)無(wú)法觸發(fā)下拉刷新,因此設(shè)置為總是能滾動(dòng) physics: const AlwaysScrollableScrollPhysics(), // 滾動(dòng)區(qū)域的內(nèi)容 // child: , ), ), );
效果:
2. 自定義下拉loading的樣式
查看
RefreshIndicator
的屬性,我們可以發(fā)現(xiàn)并沒(méi)有直接更改loading widget的方式。
- 我們查看源碼,可以發(fā)現(xiàn)返回的
loading
主要是:RefreshProgressIndicator
和CupertinoActivityIndicator
兩種。
.../flutter/packages/flutter/lib/src/material/refresh_indicator.dart
- 以下是部分源碼:
- 我們注釋掉源碼中
loading
的部分,改為自己定義的樣式 - 如果要自定義進(jìn)出動(dòng)畫(huà)的話可以在替換更高層的widget,這里只替換
AnimatedBuilder
下的widget
// 源碼的最后部分,大概619行左右 child: AnimatedBuilder( animation: _positionController, builder: (BuildContext context, Widget? child) { // 以下widget就是下拉時(shí)顯示的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"), ); }, ),
效果如下:
注:
- 直接修改源碼會(huì)影響其他項(xiàng)目,且多人協(xié)作開(kāi)發(fā)的話,其他人無(wú)法獲得同樣的效果的
- 本文的解決方案是將源碼復(fù)制出來(lái),重新命名后使用
2.1. 優(yōu)化下拉回到頂部的時(shí)間
- 通過(guò)上面的效果,我們可以看到,下拉后,列表內(nèi)容部分立即回到了頂部,這里希望刷新完成后,列表再回到頂部
最終效果:
2.1.1. 思路
- 先將源碼拷貝出來(lái),更改
widget
名稱(chēng)和Flutter的RefreshIndicator
區(qū)分開(kāi),再在源碼基礎(chǔ)上進(jìn)行修改 - 刷新頂部如何不回彈?頂部增加一個(gè)
SizedBox
占位,根據(jù)下拉高度更改SizedBox
占位的高度,在源碼中_positionController
可以獲取到下拉的高度。 - 由于是滾動(dòng)列表,因此使用
NestedScrollView
融合占位元素和滾動(dòng)列表
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(); } // 改名稱(chēng),源碼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動(dòng)畫(huà)的高度,因此這里寫(xiě)死了 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( // 滾動(dòng)區(qū)域的內(nèi)容 // child: , ), ), );
3. 增加屬性控制
根據(jù)上述的試驗(yàn),我們優(yōu)化一下,使下拉刷新組件更合理,新增以下兩個(gè)屬性:
keepScrollOffset
:自定義是否需要等待刷新完成后列表再回彈到頂部loadingWidget
:可以自定義loading樣式,默認(rèn)使用RefreshIndicator
的的loading
3.1. 難點(diǎn)與思路
難點(diǎn):
- 占位元素的高度需要與用戶(hù)傳入的自定義
loading
的高度一致,如果寫(xiě)死的話,會(huì)導(dǎo)致類(lèi)似這樣的bug
思路:
- 占位
SizedBox
的child
設(shè)置為自定義的loading
,SizedBox
的高度不設(shè)置時(shí),他的高度就是元素的高度 - 當(dāng)處于正在刷新?tīng)顟B(tài)時(shí),就將
SizedBox
的高度設(shè)置為null
遺留問(wèn)題:
- 目前代碼中寫(xiě)死了默認(rèn)高度55(參照我完整代碼的396行),如果傳入的自定義
loading
高度大于55,松開(kāi)時(shí)會(huì)有一點(diǎn)彈跳效果,暫時(shí)沒(méi)有找到更好的解決方案,如果大家有更好的方案歡迎討論一下
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; // 刷新時(shí)是否保留頂部的偏移 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(); } // 改名稱(chēng),源碼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; } // 計(jì)算占位元素的高度 double? calcHeight(double percent) { // 刷新時(shí)不保留占位 if (!widget.keepScrollOffset) return 0; // 55是默認(rèn)loading動(dòng)畫(huà)的高度,如果傳入的自定義loading高度大于55,松開(kāi)時(shí)會(huì)有一點(diǎn)彈跳效果,暫時(shí)沒(méi)有找到好的結(jié)局方案,如果你有好的解決方案,希望分享一下 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或使用默認(rèn)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, // 刷新時(shí)是否保留頂部偏移,默認(rèn)不保留 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( // 滾動(dòng)區(qū)域的內(nèi)容 // child: , ), ), );
3.4. 效果
以上就是Flutter自定義下拉刷新時(shí)的loading樣式的方法詳解的詳細(xì)內(nèi)容,更多關(guān)于Flutter自定義loading樣式的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android多媒體應(yīng)用使用SoundPool播放音頻
這篇文章主要為大家詳細(xì)介紹了Android多媒體應(yīng)用使用SoundPool播放音頻,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-12-12Android自定義帶有圓形進(jìn)度條的可長(zhǎng)按控件功能
這篇文章主要介紹了Android自定義帶有圓形進(jìn)度條的可長(zhǎng)按控件,思路很簡(jiǎn)單,使用簡(jiǎn)單的畫(huà)筆工具就可以完成這個(gè)控件,本文結(jié)合示例代碼給大家介紹的非常詳細(xì),需要的朋友參考下吧2022-06-06Android如何使用正則表達(dá)式只保留字母數(shù)字
在做項(xiàng)目的過(guò)程中,使用正則表達(dá)式來(lái)匹配一段文本中的特定種類(lèi)字符,是比較常用的一種方式,下面這篇文章主要給大家介紹了關(guān)于Android如何使用正則表達(dá)式只保留字母數(shù)字的相關(guān)資料,需要的朋友可以參考下2022-05-05Flutter進(jìn)階之實(shí)現(xiàn)動(dòng)畫(huà)效果(十)
這篇文章主要為大家詳細(xì)介紹了Flutter進(jìn)階之實(shí)現(xiàn)動(dòng)畫(huà)效果的第十篇,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-08-08Android自定義View實(shí)現(xiàn)抖音飄動(dòng)紅心效果
這篇文章主要為大家詳細(xì)介紹了Android自定義View實(shí)現(xiàn)抖音飄動(dòng)紅心效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-05-05Android實(shí)現(xiàn)簡(jiǎn)易計(jì)算器小程序
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)簡(jiǎn)易計(jì)算器小程序,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-05-05Android幀動(dòng)畫(huà)、補(bǔ)間動(dòng)畫(huà)、屬性動(dòng)畫(huà)用法詳解
安卓的三種動(dòng)畫(huà),幀動(dòng)畫(huà),補(bǔ)間動(dòng)畫(huà),屬性動(dòng)畫(huà),大家了解多少,知道如何使用嗎?本文就為大家簡(jiǎn)單介紹Android幀動(dòng)畫(huà)、補(bǔ)間動(dòng)畫(huà)、屬性動(dòng)畫(huà)的使用方法,需要的朋友可以參考下2016-11-11Android實(shí)現(xiàn)TCP客戶(hù)端接收數(shù)據(jù)的方法
這篇文章主要介紹了Android實(shí)現(xiàn)TCP客戶(hù)端接收數(shù)據(jù)的方法,較為詳細(xì)的分析了Android基于TCP實(shí)現(xiàn)客戶(hù)端接收數(shù)據(jù)的相關(guān)技巧與注意事項(xiàng),需要的朋友可以參考下2016-04-04Android實(shí)現(xiàn)倒計(jì)時(shí)的按鈕效果
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)倒計(jì)時(shí)的按鈕效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-12-12一文帶你了解Android?Flutter中Transform的使用
flutter的強(qiáng)大之處在于,可以對(duì)所有的widget進(jìn)行Transform,因此可以做出非??犰诺男Ч?。本文就來(lái)大家了解一下Transform的具體使用,感興趣的可以了解一下2023-01-01