欢迎访问移动开发之家(rcyd.net),关注移动开发教程。移动开发之家  移动开发问答|  每日更新
页面位置 : > > > 内容正文

Flutter自定义下拉刷新时的loading样式的方法详解,

来源: 开发者 投稿于  被查看 40340 次 评论:245

Flutter自定义下拉刷新时的loading样式的方法详解,


目录
  • 前言
  • 1. 简单更改RefreshIndicator的样式
  • 2. 自定义下拉loading的样式
    • 2.1. 优化下拉回到顶部的时间
      • 2.1.1. 思路
      • 2.1.2. 代码
      • 2.1.3. 使用
  • 3. 增加属性控制
    • 3.1. 难点与思路
      • 3.2. 完整代码
        • 3.3. 使用
          • 3.4. 效果

          前言

          Flutter中的下拉刷新,我们通常RefreshIndicator,可以通过backgroundColorcolorstrokeWidth设置下拉刷新的颜色粗细等样式,但如果要自定义自己的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主要是:RefreshProgressIndicatorCupertinoActivityIndicator两种。

          .../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

          思路:

          • 占位SizedBoxchild设置为自定义的loadingSizedBox的高度不设置时,他的高度就是元素的高度
          • 当处于正在刷新状态时,就将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样式的资料请关注3672js教程其它相关文章!

          您可能感兴趣的文章:
          • Flutter开发通用页面Loading组件示例详解
          • Android Flutter绘制有趣的 loading加载动画
          • Flutter刷新组件RefreshIndicator自定义样式demo
          • Flutter自定义实现弹出层的示例代码
          • 详解Android Flutter如何自定义动画路由

          用户评论