欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Flutter自定義下拉刷新時(shí)的loading樣式的方法詳解

 更新時(shí)間:2024年01月25日 09:00:29   作者:Jewel105  
Flutter中的下拉刷新,我們通常RefreshIndicator,可以通過(guò)color或strokeWidth設(shè)置下拉刷新的顏色粗細(xì)等樣式,但如果要自定義自己的widget,RefreshIndicator并沒(méi)有暴露出對(duì)應(yīng)的屬性,那如何修改呢,文中給大家介紹的非常詳細(xì),需要的朋友可以參考下

前言

Flutter中的下拉刷新,我們通常RefreshIndicator,可以通過(guò)backgroundColorcolorstrokeWidth設(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主要是:RefreshProgressIndicatorCupertinoActivityIndicator兩種。

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

思路:

  • 占位SizedBoxchild設(shè)置為自定義的loadingSizedBox的高度不設(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)文章

最新評(píng)論