Flutter - 8 :一个附带手势刷新与自动加载的列表

Flutter - 8 :一个附带手势刷新与自动加载的列表

友情提示 : 这个仅仅只是做出来看的,其中有些东西也是直接定死的,用到的东西可能会对其他人有些许提示效果,然而并不能保证这个东西一定不会出现错误。

列表这种东西在移动设备上还是很必要的,毕竟屏幕就那么大,数据多了,肯定要进行手势滑动,随之而来的就是手势刷新与加载,其中,数据的刷新是必须要用户触发的,加载则不是,毕竟每次拉到底要用户再下拉一次,行为上来说,并不合适,毕竟要把使用人当成傻子。

所以下面定义的列表,下拉刷新需要用户触发,至于加载则是自动加载,只有在出现错误时才能再进行上拉触发加载行为,如果没有更多的数据了,那么也同样不会触发上拉加载行为。

以下为简单展示图:

第一步:

首先需要定义几个状态值,对滑动过程中的几种不同的状态进行区分,以及动画时间等等基础变量

// 滑动状态值
const int Init = 1;
const int Ready = 2;

// 刷新状态值
const int CouldRefresh = 3;
const int ReadyToRefresh = 4;
const int Refreshing = 5;

// 加载状态值
const int CouldLoad = 6;
const int ReadyToLoad = 7;
const int Loading = 8;

// 结果状态值
const int Success = -1;
const int End = -2;
const int Error = -3;

// 动画相关
const double animateStartPosition = 0.01;
const int animateTime = 1250;

// 刷新完成后,顶部Item停留的时间
const int headerDelayTime = 1250;
第二步:

手势操作中,核心就是对用户的操作进行判定,进而触发不一样的效果,对于列表而言,NotificationListener就是用来反馈用户行为的回调控件,在ListView的外面加一层就能够收到用户的滑动回调信息,用于判定滑动的起点,终点,距离等等。剩下的就是对这些行为进行区分判定了,除了繁复一点,其实也并不困难。下面的代码当中有对判定的注释。

class CustomRefresher extends StatefulWidget {
  int itemCount;
  final IndexedWidgetBuilder itemBuilder;
  final ActiveCallBack headActives;
  final ActiveCallBack footActives;

  final Axis scrollDirection;
  final bool reverse;
  final ScrollController controller;
  final bool primary;
  final ScrollPhysics physics;
  final bool shrinkWrap;
  final EdgeInsetsGeometry padding;

  ResaultState loadState; // 控件加载状态

  CustomRefresher(
      {@required this.itemCount,
      @required this.itemBuilder,
      @required this.headActives,
      @required this.footActives,
      this.scrollDirection = Axis.vertical,
      this.reverse = false,
      this.controller,
      this.primary,
      this.physics,
      this.shrinkWrap = false,
      this.padding,
      this.loadState});

  @override
  State<StatefulWidget> createState() {
    return _CustomRefresherState();
  }
}

class _CustomRefresherState extends State<CustomRefresher>
    with TickerProviderStateMixin {
  final double trigerLength = 20.0;
  final double activeLength = 100.0;

  StateSetter _headStateSetter;
  StateSetter _footStateSetter;

  double _startMetricsPosition = 0.0;
  double _startDragPosition = 0.0;
  double _currentDragPosition = 0.0;

  ResaultState _refreshState = ResaultState.init; // 控件刷新状态
  ResaultState _loadState = ResaultState.init; // 控件加载状态
  int _scrollState = Init;

  final Tween<double> sizeTween = Tween(begin: animateStartPosition, end: 1.0);

  AnimationController _controller;
  Animation<double> _animation;

  int _newDataCount = 0;

  @override
  void initState() {
    super.initState();
    _controller =
        AnimationController(duration: Duration(milliseconds: 250), vsync: this);
    _animation = sizeTween
        .animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut));
    if(widget.loadState != null){
      _loadState = widget.loadState;
      widget.loadState = null;
    }
  }

  @override
  void didUpdateWidget(CustomRefresher oldWidget) {
    super.didUpdateWidget(oldWidget);
    if(widget.loadState != null){
      _loadState = widget.loadState;
      widget.loadState = null;
    }
  }

  @override
  void dispose() {
    super.dispose();
    _controller.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return NotificationListener<ScrollNotification>(
      child: ListView.builder(
        itemCount: (widget.itemCount + 2),
        scrollDirection: widget.scrollDirection,
        reverse: widget.reverse,
        controller: widget.controller,
        primary: widget.primary,
        physics: (widget.physics != null
            ? widget.physics
            : CustomBuncingScrollPhysicis(
                parent: AlwaysScrollableScrollPhysics())),
        shrinkWrap: widget.shrinkWrap,
        padding: widget.padding,
        itemBuilder: itemBuilder,
      ),
      onNotification: onScrollNotificated,
    );
  }

  // 列表item
  Widget itemBuilder(context, index) {
    // 0号位刷新header
    if (index == 0) {
      return getRefreshHeader();
    }
    // max号位加载footer
    if (index == (widget.itemCount + 1)) {
      return getRefreshFooter();
    }
    // 正常的item
    return widget.itemBuilder(context, (index - 1));
  }

  // 获取刷新header
  Widget getRefreshHeader() {
    return SizeTransition(
      sizeFactor: _animation,
      child: Material(
        color: Colors.black12,
        child: SizedBox(
          height: 48.0,
          child: StatefulBuilder(
              builder: (BuildContext context, StateSetter setState) {
            _headStateSetter = setState;
            int headerState;
            if (_scrollState == Init) {
              switch (_refreshState) {
                case ResaultState.init:
                  headerState = Init;
                  break;

                case ResaultState.success:
                  headerState = Success;
                  break;
                case ResaultState.end:
                  headerState = End;
                  break;

                case ResaultState.error:
                  headerState = Error;
                  break;
              }
            } else {
              headerState = _scrollState;
            }
            return getRefreshWidget(headerState);
          }),
        ),
      ),
    );
  }

  // 获取加载footer
  Widget getRefreshFooter() {
    return StatefulBuilder(
        builder: (BuildContext context, StateSetter setState) {
      _footStateSetter = setState;
      int footerState;
      if (_scrollState == Init) {
        switch (_loadState) {
          case ResaultState.end:
            footerState = End;
            break;

          case ResaultState.error:
            footerState = Error;
            break;
          default:
            footerState = Loading;
            _scrollState = Loading;
            break;
        }
      } else {
        footerState = _scrollState;
      }
      return Material(
        color: Colors.transparent,
        child: SizedBox(
          height: 48.0,
          child: getLoadingWidget(footerState),
        ),
      );
    });
  }

  // 刷新结束
  void onRefreshEnd(ActiveResault resaule) {
    _scrollState = Init;
    _refreshState = resaule.state;
    switch (resaule.state) {
      case ResaultState.init:
        _headStateSetter(() {
          _controller.value = animateStartPosition;
        });
        break;

      case ResaultState.success:
        setState(() {
          _newDataCount = resaule.dataCount - widget.itemCount;
          widget.itemCount = resaule.dataCount;
          Future.delayed(Duration(milliseconds: headerDelayTime), () {
            _headStateSetter(() {
              _refreshState = ResaultState.init;
              _controller.reverse();
            });
          });
        });
        break;

      case ResaultState.end:
        _headStateSetter(() {
          Future.delayed(Duration(milliseconds: headerDelayTime), () {
            _headStateSetter(() {
              _refreshState = ResaultState.init;
              _controller.reverse();
            });
          });
        });
        break;

      case ResaultState.error:
        _headStateSetter(() {
          Future.delayed(Duration(milliseconds: headerDelayTime), () {
            _headStateSetter(() {
              _refreshState = ResaultState.init;
              _controller.reverse();
            });
          });
        });
        break;
    }
  }

  // 加载结束
  void onLoadEnd(ActiveResault resaule) {
    _scrollState = Init;
    if (ResaultState.success == resaule.state) {
      setState(() {
        _newDataCount = resaule.dataCount - widget.itemCount;
        widget.itemCount = resaule.dataCount;
        _loadState = ResaultState.success;
      });
    } else {
      _footStateSetter(() {
        _loadState = resaule.state;
      });
    }
  }

  // 滑动事件监听
  bool onScrollNotificated(ScrollNotification notification) {
    switch (notification.runtimeType) {
      case ScrollStartNotification:
        startDragCheck(notification);
        break;

      case ScrollUpdateNotification:
        updateDragCheck(notification);
        break;
    }
    return false;
  }

  // 检测起始点 --- 确定状态
  void startDragCheck(ScrollStartNotification notification) {
    if (_scrollState == Init) {
      _startMetricsPosition = notification.metrics.extentBefore;
      _startDragPosition = notification.dragDetails.globalPosition.dy;
      _scrollState = Ready;
    }
  }

  // 确定当前滑动时的状态
  void updateDragCheck(ScrollUpdateNotification notification) {
    switch (_scrollState) {
      case Ready:
        // 起始滑动距顶部小于触发距离 && 滑动方向向下,判定为可以触发刷新状态
        if (_startMetricsPosition < trigerLength &&
            notification.dragDetails != null) {
          if (_startDragPosition < notification.dragDetails.globalPosition.dy) {
            Logs.p("couldRefresh");
            // 刷新header出现
            _headStateSetter(() {
              _scrollState = CouldRefresh;
              _controller.forward();
            });
            return;
          }
        }
        // 起始滑动距底部小于触发距离 && 滑动方向向上,判定为可以触发加载状态
        if ((_startMetricsPosition + trigerLength) >
                notification.metrics.maxScrollExtent &&
            notification.dragDetails != null &&
            _loadState != ResaultState.end) {
          if (_startDragPosition > notification.dragDetails.globalPosition.dy) {
            Logs.p("couldLoad");
            _footStateSetter(() {
              _scrollState = CouldLoad;
            });
            return;
          }
        }
        // 两次判定均为false --- 取消ready状态
        _scrollState = Init;
        Logs.p(_controller.value);
        if (_controller.value != animateStartPosition) {
          _headStateSetter(() {
            _controller.reverse();
          });
        }
        break;

      case CouldRefresh:
        if (notification.dragDetails != null) {
          // 判定拖动距离 --- 与起始点距离大于激活距离时触发刷新状态
          _currentDragPosition = notification.dragDetails.globalPosition.dy;
          double dragLength = _currentDragPosition - _startDragPosition;
          if (dragLength > activeLength) {
            Logs.p("readyRefresh");
            _headStateSetter(() {
              _scrollState = ReadyToRefresh;
            });
          } else if (dragLength < 0.0) {
            // 刷新header显示
            _headStateSetter(() {
              _scrollState = Init;
              _controller.reverse();
            });
          }
        } else {
          // 刷新header显示,并且处于松开状态
          if (_controller.value != 0.0) {
            _headStateSetter(() {
              _scrollState = Init;
              _controller.reverse();
            });
          }
        }
        break;

      case ReadyToRefresh:
        if (notification.dragDetails != null) {
          // 判定拖动距离 --- 与起始距离小于激活距离时取消加载状态
          _currentDragPosition = notification.dragDetails.globalPosition.dy;
          double dragLength = _currentDragPosition - _startDragPosition;
          if (dragLength <= activeLength) {
            Logs.p("couldRefresh");
            _headStateSetter(() {
              _scrollState = CouldRefresh;
            });
          }
        } else {
          // 松开时的距离大于激活距离,执行加载
          Logs.p("Refreshing");
          _headStateSetter(() {
            _scrollState = Refreshing;
          });
        }
        break;

      case CouldLoad:
        if (notification.dragDetails != null) {
          _currentDragPosition = notification.dragDetails.globalPosition.dy;
          // 判定拖动距离 --- 与起始点距离大于激活距离时触发加载状态
          if (_startDragPosition - _currentDragPosition > activeLength) {
            Logs.p("readyLoad");
            _footStateSetter(() {
              _scrollState = ReadyToLoad;
            });
          }
        } else {
          _footStateSetter(() {
            _scrollState = Init;
          });
        }
        break;

      case ReadyToLoad:
        if (notification.dragDetails != null) {
          _currentDragPosition = notification.dragDetails.globalPosition.dy;
          // 判定拖动距离 --- 与起始距离小于激活距离时取消加载状态
          if (_startDragPosition - _currentDragPosition <= activeLength) {
            Logs.p("couldLoad");
            _footStateSetter(() {
              _scrollState = CouldLoad;
            });
          }
        } else {
          // 松开时的距离大于激活距离,执行加载
          if (_startDragPosition - _currentDragPosition > activeLength) {
            Logs.p("loading");
            _footStateSetter(() {
              _scrollState = Loading;
            });
          }
        }
        break;

      default:
        break;
    }
  }

  // 获取刷新header控件组
  Widget getRefreshWidget(int refreshState) {
    switch (refreshState) {
      case Refreshing:
        // 执行传入的刷新方法
        Future.delayed(Duration(milliseconds: animateTime), () {
          widget.headActives().then(onRefreshEnd);
        });
        return Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            SizedBox(
                width: 28.0,
                height: 28.0,
                child: CircularProgressIndicator(
                  strokeWidth: 2.0,
                )),
            Padding(
              padding: EdgeInsets.only(left: 8.0),
              child: Text("加载中...",
                  style: TextStyle(
                      fontSize: 18.0,
                      fontWeight: FontWeight.bold,
                      fontStyle: FontStyle.normal,
                      color: Colors.blue)),
            )
          ],
        );

      case CouldRefresh:
        return Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Icon(
              Icons.arrow_downward,
              color: Colors.grey,
              size: 24.0,
            ),
            Text("下拉刷新数据!",
                textAlign: TextAlign.center,
                style: TextStyle(
                  fontSize: 18.0,
                  fontWeight: FontWeight.bold,
                  fontStyle: FontStyle.normal,
                  color: Colors.blue,
                )),
          ],
        );

      case ReadyToRefresh:
        return Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Icon(
              Icons.arrow_upward,
              color: Colors.grey,
              size: 24.0,
            ),
            Text("放开加载!",
                textAlign: TextAlign.center,
                style: TextStyle(
                  fontSize: 18.0,
                  fontWeight: FontWeight.bold,
                  fontStyle: FontStyle.normal,
                  color: Colors.blue,
                )),
          ],
        );

      case Success:
        return Center(
          child: Text("成功刷新${_newDataCount}条数据!",
              textAlign: TextAlign.center,
              style: TextStyle(
                fontSize: 18.0,
                fontWeight: FontWeight.bold,
                fontStyle: FontStyle.normal,
                color: Colors.blue,
              )),
        );

      case End:
        return Center(
          child: Text("数据已是最新!",
              textAlign: TextAlign.center,
              style: TextStyle(
                fontSize: 18.0,
                fontWeight: FontWeight.bold,
                fontStyle: FontStyle.normal,
                color: Colors.blue,
              )),
        );

      case Error:
        return Center(
          child: Text("数据刷新失败!",
              textAlign: TextAlign.center,
              style: TextStyle(
                fontSize: 18.0,
                fontWeight: FontWeight.bold,
                fontStyle: FontStyle.normal,
                color: Colors.red,
              )),
        );

      default:
        return Material(
          color: Colors.transparent,
        );
    }
  }

  // 获取加载footer控件组
  Widget getLoadingWidget(int loadState) {
    switch (loadState) {
      case Loading:
        // 执行传入的加载方法
        Future.delayed(Duration(milliseconds: animateTime), () {
          widget.footActives().then(onLoadEnd);
        });
        return Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            SizedBox(
                width: 28.0,
                height: 28.0,
                child: CircularProgressIndicator(
                  strokeWidth: 2.0,
                )),
            Padding(
              padding: EdgeInsets.only(left: 8.0),
              child: Text("加载中...",
                  style: TextStyle(
                      fontSize: 18.0,
                      fontWeight: FontWeight.bold,
                      fontStyle: FontStyle.normal,
                      color: Colors.blue)),
            )
          ],
        );

      case CouldLoad:
        return Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Icon(
              Icons.arrow_upward,
              color: Colors.grey,
              size: 24.0,
            ),
            Padding(
                padding: EdgeInsets.only(left: 8.0),
                child: Text("上拉加载更多!",
                    textAlign: TextAlign.center,
                    style: TextStyle(
                      fontSize: 18.0,
                      fontWeight: FontWeight.bold,
                      fontStyle: FontStyle.normal,
                      color: Colors.blue,
                    ))),
          ],
        );

      case ReadyToLoad:
        return Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Icon(
              Icons.arrow_downward,
              color: Colors.grey,
              size: 24.0,
            ),
            Padding(
                padding: EdgeInsets.only(left: 8.0),
                child: Text("放开加载!",
                    textAlign: TextAlign.center,
                    style: TextStyle(
                      fontSize: 18.0,
                      fontWeight: FontWeight.bold,
                      fontStyle: FontStyle.normal,
                      color: Colors.blue,
                    ))),
          ],
        );

      case End:
        return Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            SizedBox(
              width: 56.0,
              child: Divider(
                height: 2.0,
                color: Colors.grey,
              ),
            ),
            Padding(
              padding: EdgeInsets.only(left: 6.0, right: 6.0),
              child: Text(
                "没有更多数据了!",
                style: TextStyle(
                    fontSize: 18.0,
                    fontWeight: FontWeight.bold,
                    fontStyle: FontStyle.normal,
                    color: Colors.grey),
              ),
            ),
            SizedBox(
              width: 56.0,
              child: Divider(
                height: 2.0,
                color: Colors.grey,
              ),
            )
          ],
        );

      case Error:
        return Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text("加载出错!",
                textAlign: TextAlign.center,
                style: TextStyle(
                  fontSize: 18.0,
                  fontWeight: FontWeight.bold,
                  fontStyle: FontStyle.normal,
                  color: Colors.red,
                )),
            Text("上拉加载更多!",
                textAlign: TextAlign.center,
                style: TextStyle(
                  fontSize: 18.0,
                  fontWeight: FontWeight.bold,
                  fontStyle: FontStyle.normal,
                  color: Colors.red,
                ))
          ],
        );

      default:
        return Material(
          color: Colors.transparent,
        );
    }
  }
}
第三步:

定义刷新与加载方法,以及加载状态的枚举,用来在刷新控件时通知底部item加载需要的布局以及手势操作。

typedef ActiveCallBack = Future<ActiveResault> Function();

enum ResaultState { init, success, end, error }

class ActiveResault {
  final ResaultState state;
  final int dataCount;
  ActiveResault(this.state, {this.dataCount});
}
第四步:

当前控件中,用的是BouncingScrollPhysics,然而,这个滑动物理效果本身是不能满足需求的,毕竟一旦进行刷新操作,顶部会出现一大块空白,所以需要自定义修改一下,直接继承,然后修改一下在滑动到顶端时的返回值就可以了。

class CustomBuncingScrollPhysicis extends BouncingScrollPhysics {
  const CustomBuncingScrollPhysicis({ScrollPhysics parent})
      : super(parent: parent);

  @override
  BouncingScrollPhysics applyTo(ScrollPhysics ancestor) {
    return CustomBuncingScrollPhysicis(parent: buildParent(ancestor));
  }

  @override
  double applyPhysicsToUserOffset(ScrollMetrics position, double offset) {
    assert(offset != 0.0);
    assert(position.minScrollExtent <= position.maxScrollExtent);

    if (!position.outOfRange) return offset;

    // 缩小比例 --- 处于顶部时,滑动距离为正常状态的1/10
    double percent = 1.0;
    if (position.extentBefore <= 20.0) {
      percent = 0.1;
    }

    final double overscrollPastStart =
        math.max(position.minScrollExtent - position.pixels, 0.0);
    final double overscrollPastEnd =
        math.max(position.pixels - position.maxScrollExtent, 0.0);
    final double overscrollPast =
        math.max(overscrollPastStart, overscrollPastEnd);
    final bool easing = (overscrollPastStart > 0.0 && offset < 0.0) ||
        (overscrollPastEnd > 0.0 && offset > 0.0);

    final double friction = easing
        // Apply less resistance when easing the overscroll vs tensioning.
        ? frictionFactor(
            (overscrollPast - offset.abs()) / position.viewportDimension)
        : frictionFactor(overscrollPast / position.viewportDimension);
    final double direction = offset.sign;

    return direction *
        _applyFriction(overscrollPast, offset.abs(), friction) *
        percent;
  }

  static double _applyFriction(
      double extentOutside, double absDelta, double gamma) {
    assert(absDelta > 0);
    double total = 0.0;
    if (extentOutside > 0) {
      final double deltaToLimit = extentOutside / gamma;
      if (absDelta < deltaToLimit) return absDelta * gamma;
      total += extentOutside;
      absDelta -= deltaToLimit;
    }
    return total + absDelta;
  }
}
第五步:

最后附加开始的gif中展示的测试代码:

void main() {
  SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp])
      .then((Null) {
    runApp(ForFun());
  });
}

class ForFun extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'ForFun',
      theme: ThemeData(
        fontFamily: "hwxw",
        primaryColorDark: Colors.blueAccent,
        primaryColor: Colors.blue,
        primaryColorLight: Colors.lightBlue,
        primarySwatch: Colors.blue,
      ),
      home: TestListPage(),
    );
  }
}

class TestListPage extends StatelessWidget {
  final List<String> list = [
    "0",
    "1",
    "2",
    "3",
    "4",
    "5",
    "6",
    "7",
    "8",
    "9",
    "10",
    "11",
    "12",
    "13",
    "14",
    "15",
    "16",
    "17",
    "18",
    "19",
    "20",
    "21",
  ];

  int count = 3;

  @override
  Widget build(BuildContext context) {
    return Material(
        color: Colors.red,
        child: CustomRefresher(
          itemCount: count,
          itemBuilder: itemBuilder,
          headActives: () async {
            int newElement_1 = int.parse(list[0]) - 1;
            list.insert(0, newElement_1.toString());
            int newElement_2 = int.parse(list[0]) - 1;
            list.insert(0, newElement_2.toString());
            int newElement_3 = int.parse(list[0]) - 1;
            list.insert(0, newElement_3.toString());
            count = count + 3;
            return ActiveResault(ResaultState.success,dataCount: count);
          },
          footActives: () async {
            if (count < 6) {
              count += 2;
              return ActiveResault(ResaultState.success, dataCount: count);
            } else {
              return ActiveResault(ResaultState.end);
            }
          },
        ));
  }

  Widget itemBuilder(BuildContext context, int index) {
    return Padding(
      padding: EdgeInsets.only(left: 12.0, top: 4.0, right: 12.0, bottom: 4.0),
      child: Material(
        elevation: 2.0,
        borderRadius: BorderRadius.all(Radius.circular(2.0)),
        child: SizedBox(
          height: 64.0,
          child: Padding(
            padding: EdgeInsets.all(8.0),
            child: Center(
              child: Text(list[index]),
            ),
          ),
        ),
      ),
    );
  }
}
本集完!
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值