从零开始用flutter写一个完整应用⑷:基础控件列表ListView

说明

列表listview是前端应用中最基础的控件之一,大部分内容展示基本都在listview中,毫不夸张的说学好listview一个前端开发就基本可以挣钱了。ListView是滑动控件Scroll中的一种在系统scroll_view.dart中,其中包括基础的ScrollView,ListView,GridView等控件,这些都是最基础的控件,后面会一一讲解到。

ListView简介

ListView({
    Key? key,
    Axis scrollDirection = Axis.vertical,//控件滑动方向,有2个值Axis.vertical垂直,Axis.horizontal水平,
                                        //默认认垂直方向
    bool reverse = false,//控制数据读取方向,与scrollDirection配合使用,
                        //如scrollDirection为Axis.vertical时,false表示从左到右,true为从右到左;
                        //scrollDirection为Axis.horizontal时,false表示从上到下,true为从下到上。
                       //默认为false
    ScrollController? controller,//用于控制视图滚动,如控制初始位置,读取当前位置或者改变位置等,
                              //当primary为true时必须为null
    bool? primary,//是否与容器关联滑动,
                //为true时默认可滑动,不管有没有内容;
                //为false时只有内容超出容器边界时才可滑动;
                //当scrollDirection为Axis.vertical同时controller为null时,primary默认为true,否则默认为false
    ScrollPhysics? physics,//滚动视图应如何响应用户输入,例如,确定用户停止拖动滚动视图后滚动视图如何继续设置动画等
    bool shrinkWrap = false,//是否应根据正在查看的内容确定scrollDirection中滚动视图的范围,
                   //如果滚动视图shrinkWrap为false,则滚动视图将展开设置为[scrollDirection]中允许的最大大小。
                  //如果滚动视图在[scrollDirection]上有无界约束,则[shrinkWrap ]必须是true。
    EdgeInsetsGeometry? padding,//子view间的间隔
    this.itemExtent,//指定Item在滑动方向上的高度,用来提高滑动性能,如果是non_null,则必须得给定滑动范围;
    this.prototypeItem,//如果非null,则强制子级在滚动方向上具有与给定小部件相同的范围
    bool addAutomaticKeepAlives = true,//是否将子控件包裹在AutomaticKeepAlive控件内
    bool addRepaintBoundaries = true,//是否将子控件包裹在 RepaintBoundary 控件内。用于避免列表滚动时的重绘,如果子控件重绘开销很小时,比如子控件就是个色块或简短的文字,把这个字段设置为false性能会更好
    bool addSemanticIndexes = true,//是否把子控件包装在IndexedSemantics里,用来提供无障碍语义
    double? cacheExtent,//可见区域的前后会有一定高度的空间去缓存子控件,当滑动时就可以迅速呈现
    List<Widget> children = const <Widget>[],//子view
    int? semanticChildCount,//有含义的子控件的数量,如ListView会用children的长度,ListView.separated会用children长度的一半
    DragStartBehavior dragStartBehavior = DragStartBehavior.start,//拖动开始行为
    ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,//键盘关闭行为
    String? restorationId,//还原ID
    Clip clipBehavior = Clip.hardEdge,//剪切,默认Clip.hardEdge
  })

部分扩展知识

ScrollPhysics:控制用户滚动视图的交互
AlwaysScrollableScrollPhysics:列表总是可滚动的。在iOS上会有回弹效果,在android上不会回弹。那么问题来了,如果primary设置为false(内容不足时不滚动),且 physics设置为AlwaysScrollableScrollPhysics,列表是否可以滑动?答案是可以,感兴趣的可以试一下
PageScrollPhysics:一般是给PageView控件用的滑动效果。如果listview设置的话在滑动到末尾时会有个比较大的弹起和回弹
ClampingScrollPhysics:滚动时没有回弹效果,同android系统的listview效果
NeverScrollableScrollPhysics:就算内容超过列表范围也不会滑动
BouncingScrollPhysics:不论什么平台都会有回弹效果
FixedExtentScrollPhysics:不适用于ListView,原因:需要指定scroller为FixedExtentScrollController,这个scroller只能用于ListWheelScrollViews

ListView的4种构造方式

1,默认构造函数

适用场景:已知有限个Item的情况下

ListView(
     children: const <Widget>[
            ListTile(title: Text("普通ListView1")),
            ListTile(title: Text("普通ListView2")),
            ListTile(title: Text("普通ListView3")),
            ListTile(title: Text("普通ListView4"))
     ]
)

2,builder

适用场景:长列表时采用builder模式,能提高性能。不是把所有子控件都构造出来,而是在控件viewport加上头尾的cacheExtent这个范围内的子Item才会被构造。在构造时传递一个builder,按需加载是一个惯用模式,能提高加载性能

List<ListItem> items = [];
class ListItem  {
    final String sender;
    final String body;
    ListItem(this.sender, this.body);
}

ListView.builder(
     itemBuilder: (context, index) {
          final item = items[index];
          return ListTile(
               title: Text(item.sender),
               subtitle: Text(item.body),
          );
      },
      itemCount: items.length
)

3,separated

适用场景:列表中需要分割线时,可以自定义复杂的分割线

ListView.separated(
     itemBuilder: (context, index) {
         return Text("Item $index");
     },
     separatorBuilder: (context, index) {
         return Container(
             color: Colors.grey,
             height: 3,
         );
   },
  itemCount: 100
)

4,custom(自定义SliverChildDelegate)

适用场景:上面几种模式基本可以满足业务需求,如果你还想做一些事情,如设置列表的最大滚动范围或获取滑动时每次布局的子Item范围,可以尝试一下custom模式

ListView.custom(childrenDelegate: CustomSliverChildDelegate())

class CustomSliverChildDelegate extends SliverChildDelegate {
  /// 根据index构造child
  @override
  Widget build(BuildContext context, int index) {
    // KeepAlive将把所有子控件加入到cache,已输入的TextField文字不会因滚动消失
    // 仅用于演示
    return KeepAlive(
        keepAlive: true,
        child: TextField(decoration: InputDecoration(hintText: '请输入')));
  }

  /// 决定提供新的childDelegate时是否需要重新build。在调用此方法前会做类型检查,不同类型时才会调用此方法,所以一般返回true。
  @override
  bool shouldRebuild(SliverChildDelegate oldDelegate) {
    return true;
  }

  /// 提高children的count,当无法精确知道时返回null。
  /// 当 build 返回 null时,它也将需要返回一个非null值
  @override
  int get estimatedChildCount => 100;

  /// 预计最大可滑动高度,如果设置的过小会导致部分child不可见,设置报错
  @override
  double estimateMaxScrollOffset(int firstIndex, int lastIndex,
      double leadingScrollOffset, double trailingScrollOffset) {
    return 2500;
  }

  /// 完成layout后的回调,可以通过该方法获取即将渲染的视图树包括哪些子控件
  @override
  void didFinishLayout(int firstIndex, int lastIndex) {
    print('didFinishLayout firstIndex=$firstIndex firstIndex=$lastIndex');
  }
}

3种上下拉刷新

1,普通上下拉刷新

class pageRefresh1 extends State<HomePage>{

    String currentText = "普通上下拉刷新1";
    final int pageSize = 10;
    List<ListItem> items = [];
    bool disposed = false;

    final ScrollController scrollController = ScrollController();
    final GlobalKey<RefreshIndicatorState> refreshKey = GlobalKey();

    @override
    void dispose() {
        disposed = true;
        super.dispose();
    }

    Future<void> onRefresh() async {
        await Future.delayed(const Duration(seconds: 1));
        items.clear();
        for (int i = 0; i < pageSize; i++) {
            ListItem item = ListItem( name: 'refesh+ $i', subName: 'subName+ $i');
            items.add(item);
        }
        if(disposed) {
            return;
        }
        setState(() {});
    }

    Future<void> loadMore() async {
        await Future.delayed(const Duration(seconds: 1));
        for (int i = 0; i < pageSize; i++) {
            ListItem item = ListItem( name: 'loadMore+ $i', subName: 'loadMore+ $i');
            items.add(item);
        }
        if(disposed) {
            return;
        }
        setState(() {});
    }

    @override
    void initState() {
        super.initState();
        scrollController.addListener(() {
            ///判断当前滑动位置是不是到达底部,触发加载更多回调
            if (scrollController.position.pixels == scrollController.position.maxScrollExtent) {
                loadMore();
            }
        });
        Future.delayed(const Duration(seconds: 0), (){
            refreshKey.currentState!.show();
        });
    }

    @override
    Widget build(BuildContext context) {
        return Scaffold(
            appBar: AppBar(
                title: Text(currentText),
            ),
            body: Container(
                child: RefreshIndicator(
                    ///GlobalKey,用户外部获取RefreshIndicator的State,做显示刷新
                    key: refreshKey,

                    ///下拉刷新触发,返回的是一个Future
                    onRefresh: onRefresh,
                    child: ListView.builder(
                        ///保持ListView任何情况都能滚动,解决在RefreshIndicator的兼容问题。
                        physics: const AlwaysScrollableScrollPhysics(),

                        ///根据状态返回
                        itemBuilder: (context, index) {
                            if (index == items.length) {
                                return Container(
                                    margin: const EdgeInsets.all(10),
                                    child: const Align(
                                        child: CircularProgressIndicator(),
                                    ),
                                );
                            }
                            return Card(
                                child: Container(
                                    height: 60,
                                    alignment: Alignment.centerLeft,
                                    child: Text("Item ${items[index]} $index"),
                                ),
                            );
                        },

                        ///根据状态返回数量
                        itemCount: (items.length >= pageSize)
                            ? items.length + 1
                            : items.length,

                        ///滑动监听
                        controller: scrollController,
                    ),
                ),
            ),
        );
    }
}

class ListItem  {
    const ListItem({
        required this.name,
        required this.subName,
    });

    final String name;
    final String subName;
}

2,自定义上下拉刷新

class pageRefresh2 extends State<HomePage>{

    String currentText = "普通上下拉刷新2";
    final int pageSize = 10;
    List<ListItem> items = [];
    bool disposed = false;

    final ScrollController scrollController = ScrollController();
    final GlobalKey<RefreshIndicatorState> refreshKey = GlobalKey();

    @override
    void dispose() {
        disposed = true;
        super.dispose();
    }

    Future<void> onRefresh() async {
        await Future.delayed(const Duration(seconds: 1));
        items.clear();
        for (int i = 0; i < pageSize; i++) {
            ListItem item = ListItem( name: 'refesh+ $i', subName: 'subName+ $i');
            items.add(item);
        }
        if(disposed) {
            return;
        }
        setState(() {});
    }

    Future<void> loadMore() async {
        await Future.delayed(const Duration(seconds: 1));
        for (int i = 0; i < pageSize; i++) {
            ListItem item = ListItem( name: 'loadMore+ $i', subName: 'loadMore+ $i');
            items.add(item);
        }
        if(disposed) {
            return;
        }
        setState(() {});
    }

    @override
    void didChangeDependencies() {
        super.didChangeDependencies();

        ///直接触发下拉
        Future.delayed(const Duration(milliseconds: 500), () {
            scrollController.animateTo(-141,
                duration: const Duration(milliseconds: 600), curve: Curves.linear);
            return true;
        });
    }

    @override
    Widget build(BuildContext context) {
        return Scaffold(
            appBar: AppBar(
                title: Text(currentText),
            ),
            body: Container(
                child: NotificationListener(
                    onNotification: (ScrollNotification notification) {
                        ///判断当前滑动位置是不是到达底部,触发加载更多回调
                        if (notification is ScrollEndNotification) {
                            if (scrollController.position.pixels > 0 &&
                                scrollController.position.pixels ==
                                    scrollController.position.maxScrollExtent) {
                                loadMore();
                            }
                        }
                        return false;
                    },
                    child: CustomScrollView(
                        controller: scrollController,

                        ///回弹效果
                        physics: const BouncingScrollPhysics(
                            parent: AlwaysScrollableScrollPhysics()),
                            slivers: <Widget>[
                                ///控制显示刷新的 CupertinoSliverRefreshControl
                                CupertinoSliverRefreshControl(
                                    refreshIndicatorExtent: 100,
                                    refreshTriggerPullDistance: 140,
                                    onRefresh: onRefresh,
                                ),

                                ///列表区域
                                SliverSafeArea(
                                    sliver: SliverList(
                                        ///代理显示
                                        delegate: SliverChildBuilderDelegate(
                                                (BuildContext context, int index) {
                                                if (index == items.length) {
                                                    return Container(
                                                        margin: const EdgeInsets.all(10),
                                                        child: const Align(
                                                            child: CircularProgressIndicator(),
                                                        ),
                                                    );
                                                }
                                                return Card(
                                                    child: Container(
                                                        height: 60,
                                                        alignment: Alignment.centerLeft,
                                                        child: Text("Item ${items[index]} $index"),
                                                    ),
                                                );
                                            },
                                            childCount: (items.length >= pageSize)
                                                ? items.length + 1
                                                : items.length,
                                        ),
                                    ),
                                )
                        ],
                    ),
                ),
            ),
        );
    }
}

class ListItem  {
    const ListItem({
        required this.name,
        required this.subName,
    });

    final String name;
    final String subName;
}

3,自定义上下拉刷新样式

class pageRefresh3 extends State<HomePage>{

    String currentText = "自定义上下拉刷新样式";
    final int pageSize = 10;
    List<ListItem> items = [];
    bool disposed = false;

    final ScrollController scrollController = ScrollController();
    final GlobalKey<MyCupertinoSliverRefreshControlState> sliverRefreshKey = GlobalKey<MyCupertinoSliverRefreshControlState>();

    @override
    void dispose() {
        disposed = true;
        super.dispose();
    }

    Future<void> onRefresh() async {
        await Future.delayed(const Duration(seconds: 1));
        items.clear();
        for (int i = 0; i < pageSize; i++) {
            ListItem item = ListItem( name: 'refesh+ $i', subName: 'subName+ $i');
            items.add(item);
        }
        if(disposed) {
            return;
        }
        setState(() {});
    }

    Future<void> loadMore() async {
        await Future.delayed(const Duration(seconds: 1));
        for (int i = 0; i < pageSize; i++) {
            ListItem item = ListItem( name: 'loadMore+ $i', subName: 'loadMore+ $i');
            items.add(item);
        }
        if(disposed) {
            return;
        }
        setState(() {});
    }

    @override
    void didChangeDependencies() {
        super.didChangeDependencies();

        ///直接触发下拉
        Future.delayed(const Duration(milliseconds: 500), () {
            scrollController.animateTo(-141,
                duration: const Duration(milliseconds: 600), curve: Curves.linear);
            return true;
        });
    }

    @override
    Widget build(BuildContext context) {
        return Scaffold(
            appBar: AppBar(
                title: Text(currentText),
            ),
            body: Container(
                child: NotificationListener(
                    onNotification: (ScrollNotification notification) {
                        ///通知 CupertinoSliverRefreshControl 当前的拖拽状态
                        sliverRefreshKey.currentState!
                            .notifyScrollNotification(notification);
                        ///判断当前滑动位置是不是到达底部,触发加载更多回调
                        if (notification is ScrollEndNotification) {
                            if (scrollController.position.pixels > 0 &&
                                scrollController.position.pixels ==
                                    scrollController.position.maxScrollExtent) {
                                loadMore();
                            }

                        }
                        return false;
                    },
                    child: CustomScrollView(
                        controller: scrollController,

                        ///回弹效果
                        physics: const BouncingScrollPhysics(
                            parent: AlwaysScrollableScrollPhysics()),
                        slivers: <Widget>[
                            ///控制显示刷新的 CupertinoSliverRefreshControl
                            MyCupertinoSliverRefreshControl(
                                key: sliverRefreshKey,
                                refreshIndicatorExtent: 100,
                                refreshTriggerPullDistance: 140,
                                onRefresh: onRefresh,
                                builder: buildSimpleRefreshIndicator,
                            ),

                            ///列表区域
                            SliverSafeArea(
                                sliver: SliverList(
                                    ///代理显示
                                    delegate: SliverChildBuilderDelegate(
                                            (BuildContext context, int index) {
                                            if (index == items.length) {
                                                return Container(
                                                    margin: const EdgeInsets.all(10),
                                                    child: const Align(
                                                        child: CircularProgressIndicator(),
                                                    ),
                                                );
                                            }
                                            return Card(
                                                child: Container(
                                                    height: 60,
                                                    alignment: Alignment.centerLeft,
                                                    child: Text("Item ${items[index]} $index"),
                                                ),
                                            );
                                        },
                                        childCount: (items.length >= pageSize)
                                            ? items.length + 1
                                            : items.length,
                                    ),
                                ),
                            ),
                        ],
                    ),
                ),
            ),
        );
    }
}

Widget buildSimpleRefreshIndicator(
    BuildContext context,
    MyRefreshIndicatorMode? refreshState,
    double pulledExtent,
    double refreshTriggerPullDistance,
    double refreshIndicatorExtent,
    ) {
    const Curve opacityCurve = Interval(0.4, 0.8, curve: Curves.easeInOut);
    return Align(
        alignment: Alignment.bottomCenter,
        child: Padding(
            padding: const EdgeInsets.only(bottom: 16.0),
            child: refreshState != MyRefreshIndicatorMode.refresh
                ? Opacity(
                opacity: opacityCurve.transform(
                    min(pulledExtent / refreshTriggerPullDistance, 1.0)),
                child: const Icon(
                    CupertinoIcons.down_arrow,
                    color: CupertinoColors.inactiveGray,
                    size: 36.0,
                ),
            )
                : Opacity(
                opacity: opacityCurve
                    .transform(min(pulledExtent / refreshIndicatorExtent, 1.0)),
                child: const CupertinoActivityIndicator(radius: 14.0),
            ),
        ),
    );
}

class ListItem  {
    const ListItem({
        required this.name,
        required this.subName,
    });

    final String name;
    final String subName;
}

class _CupertinoSliverRefresh extends SingleChildRenderObjectWidget {
  const _CupertinoSliverRefresh({
    Key? key,
    this.refreshIndicatorLayoutExtent = 0.0,
    this.hasLayoutExtent = false,
    Widget? child,
  }) : assert(refreshIndicatorLayoutExtent >= 0.0),
        super(key: key, child: child);

  final double refreshIndicatorLayoutExtent;

  final bool hasLayoutExtent;

  @override
  _RenderCupertinoSliverRefresh createRenderObject(BuildContext context) {
    return _RenderCupertinoSliverRefresh(
      refreshIndicatorExtent: refreshIndicatorLayoutExtent,
      hasLayoutExtent: hasLayoutExtent,
    );
  }

  @override
  void updateRenderObject(BuildContext context, covariant _RenderCupertinoSliverRefresh renderObject) {
    renderObject
      ..refreshIndicatorLayoutExtent = refreshIndicatorLayoutExtent
      ..hasLayoutExtent = hasLayoutExtent;
  }
}

class _RenderCupertinoSliverRefresh extends RenderSliver
    with RenderObjectWithChildMixin<RenderBox> {
  _RenderCupertinoSliverRefresh({
    required double refreshIndicatorExtent,
    required bool hasLayoutExtent,
    RenderBox? child,
  }) : assert(refreshIndicatorExtent >= 0.0),
        _refreshIndicatorExtent = refreshIndicatorExtent,
        _hasLayoutExtent = hasLayoutExtent {
    this.child = child;
  }

  double get refreshIndicatorLayoutExtent => _refreshIndicatorExtent;
  double _refreshIndicatorExtent;
  set refreshIndicatorLayoutExtent(double value) {
    assert(value >= 0.0);
    if (value == _refreshIndicatorExtent)
      return;
    _refreshIndicatorExtent = value;
    markNeedsLayout();
  }

  bool get hasLayoutExtent => _hasLayoutExtent;
  bool _hasLayoutExtent;
  set hasLayoutExtent(bool value) {
    if (value == _hasLayoutExtent)
      return;
    _hasLayoutExtent = value;
    markNeedsLayout();
  }

  double layoutExtentOffsetCompensation = 0.0;

  @override
  void performLayout() {
    assert(constraints.axisDirection == AxisDirection.down);
    assert(constraints.growthDirection == GrowthDirection.forward);

    final double layoutExtent =
        (_hasLayoutExtent ? 1.0 : 0.0) * _refreshIndicatorExtent;
    if (layoutExtent != layoutExtentOffsetCompensation) {
      geometry = SliverGeometry(
        scrollOffsetCorrection: layoutExtent - layoutExtentOffsetCompensation,
      );
      layoutExtentOffsetCompensation = layoutExtent;
      return;
    }

    final bool active = constraints.overlap < 0.0 || layoutExtent > 0.0;
    final double overscrolledExtent =
    constraints.overlap < 0.0 ? constraints.overlap.abs() : 0.0;
    child!.layout(
      constraints.asBoxConstraints(
        maxExtent: layoutExtent
            + overscrolledExtent,
      ),
      parentUsesSize: true,
    );
    if (active) {
      geometry = SliverGeometry(
        scrollExtent: layoutExtent,
        paintOrigin: -overscrolledExtent - constraints.scrollOffset,
        paintExtent: max(
          max(child!.size.height, layoutExtent) - constraints.scrollOffset,
          0.0,
        ),
        maxPaintExtent: max(
          max(child!.size.height, layoutExtent) - constraints.scrollOffset,
          0.0,
        ),
        layoutExtent: max(layoutExtent - constraints.scrollOffset, 0.0),
      );
    } else {
      geometry = SliverGeometry.zero;
    }
  }

  @override
  void paint(PaintingContext paintContext, Offset offset) {
    if (constraints.overlap < 0.0 ||
        constraints.scrollOffset + child!.size.height > 0) {
      paintContext.paintChild(child!, offset);
    }
  }

  @override
  void applyPaintTransform(RenderObject child, Matrix4 transform) { }
}

enum MyRefreshIndicatorMode {

  inactive,

  drag,

  armed,

  refresh,

  done,
}

typedef RefreshControlIndicatorBuilder = Widget Function(
    BuildContext context,
    MyRefreshIndicatorMode? refreshState,
    double pulledExtent,
    double refreshTriggerPullDistance,
    double refreshIndicatorExtent,
    );


typedef RefreshCallback = Future<void> Function();

class MyCupertinoSliverRefreshControl extends StatefulWidget {

  const MyCupertinoSliverRefreshControl({
    Key? key,
    this.refreshTriggerPullDistance = _defaultRefreshTriggerPullDistance,
    this.refreshIndicatorExtent = _defaultRefreshIndicatorExtent,
    this.builder = buildSimpleRefreshIndicator,
    this.onRefresh,
  }) : assert(refreshTriggerPullDistance > 0.0),
        assert(refreshIndicatorExtent >= 0.0),
        assert(
        refreshTriggerPullDistance >= refreshIndicatorExtent,
        ),
        super(key: key);


  final double refreshTriggerPullDistance;

  final double refreshIndicatorExtent;

  final RefreshControlIndicatorBuilder builder;

  final RefreshCallback? onRefresh;

  static const double _defaultRefreshTriggerPullDistance = 100.0;
  static const double _defaultRefreshIndicatorExtent = 60.0;

  @visibleForTesting
  static MyRefreshIndicatorMode? state(BuildContext context) {
    final MyCupertinoSliverRefreshControlState state
    = context.findAncestorStateOfType<MyCupertinoSliverRefreshControlState>()!;
    return state.refreshState;
  }

  static Widget buildSimpleRefreshIndicator(
      BuildContext context,
      MyRefreshIndicatorMode? refreshState,
      double pulledExtent,
      double refreshTriggerPullDistance,
      double refreshIndicatorExtent,
      ) {
    const Curve opacityCurve = Interval(0.4, 0.8, curve: Curves.easeInOut);
    return Align(
      alignment: Alignment.bottomCenter,
      child: Padding(
        padding: const EdgeInsets.only(bottom: 16.0),
        child: refreshState == MyRefreshIndicatorMode.drag
            ? Opacity(
          opacity: opacityCurve.transform(
              min(pulledExtent / refreshTriggerPullDistance, 1.0)
          ),
          child: const Icon(
            CupertinoIcons.down_arrow,
            color: CupertinoColors.inactiveGray,
            size: 36.0,
          ),
        )
            : Opacity(
          opacity: opacityCurve.transform(
              min(pulledExtent / refreshIndicatorExtent, 1.0)
          ),
          child: const CupertinoActivityIndicator(radius: 14.0),
        ),
      ),
    );
  }

  @override
  MyCupertinoSliverRefreshControlState createState() => MyCupertinoSliverRefreshControlState();
}

class MyCupertinoSliverRefreshControlState extends State<MyCupertinoSliverRefreshControl> {

  static const double _inactiveResetOverscrollFraction = 0.1;

  MyRefreshIndicatorMode? refreshState;

  Future<void>? refreshTask;

  double latestIndicatorBoxExtent = 0.0;
  bool hasSliverLayoutExtent = false;
  bool needRefresh = false;
  bool draging = false;

  @override
  void initState() {
    super.initState();
    refreshState = MyRefreshIndicatorMode.inactive;
  }


  MyRefreshIndicatorMode? transitionNextState() {
    MyRefreshIndicatorMode? nextState;

    void goToDone() {
      nextState = MyRefreshIndicatorMode.done;

      if (SchedulerBinding.instance!.schedulerPhase == SchedulerPhase.idle) {
        setState(() => hasSliverLayoutExtent = false);
      } else {
        SchedulerBinding.instance!.addPostFrameCallback((Duration timestamp) {
          setState(() => hasSliverLayoutExtent = false);
        });
      }
    }

    switch (refreshState) {
      case MyRefreshIndicatorMode.inactive:
        if (latestIndicatorBoxExtent <= 0) {
          return MyRefreshIndicatorMode.inactive;
        } else {
          nextState = MyRefreshIndicatorMode.drag;
        }
        continue drag;
      drag:
      case MyRefreshIndicatorMode.drag:
        if (latestIndicatorBoxExtent == 0) {
          return MyRefreshIndicatorMode.inactive;
        } else if (latestIndicatorBoxExtent < widget.refreshTriggerPullDistance) {
          return MyRefreshIndicatorMode.drag;
        } else {
          ///超过 refreshTriggerPullDistance 就可以进入准备刷新的装备状态
          if (widget.onRefresh != null) {
            HapticFeedback.mediumImpact();
            SchedulerBinding.instance!.addPostFrameCallback((Duration timestamp) {
              needRefresh = true;
              setState(() => hasSliverLayoutExtent = true);
            });
          }
          return MyRefreshIndicatorMode.armed;
        }
      case MyRefreshIndicatorMode.armed:
        if (refreshState == MyRefreshIndicatorMode.armed && !needRefresh) {
          goToDone();
          continue done;
        }
        ///当已经进去装备阶段,拖拽距离没到 refreshIndicatorExtent 的时候
        ///继续返回 armed 状态,知道 latestIndicatorBoxExtent = refreshIndicatorExtent
        ///才进入刷新状态
        if (latestIndicatorBoxExtent > widget.refreshIndicatorExtent) {
          return MyRefreshIndicatorMode.armed;
        } else {
          ///如果这时候手还在拖拽
          if(draging) {
            goToDone();
            continue done;
          }
          nextState = MyRefreshIndicatorMode.refresh;
        }
        continue refresh;
      refresh:
      case MyRefreshIndicatorMode.refresh:
        ///进入刷新状态,先判断是否达到刷新标准
        if (needRefresh) {
          ///还没有触发外部刷新,触发一下
          if (widget.onRefresh != null && refreshTask == null) {
            HapticFeedback.mediumImpact();
            SchedulerBinding.instance!.addPostFrameCallback((Duration timestamp) {
              ///任务完成后清洗状态
              refreshTask = widget.onRefresh!()..whenComplete(() {
                if (mounted) {
                  setState(() {
                    refreshTask = null;
                    needRefresh = false;
                  });
                  refreshState = transitionNextState();
                }
              });
              setState(() => hasSliverLayoutExtent = true);
            });
          }
          return MyRefreshIndicatorMode.refresh;
        } else {
          goToDone();
        }
        continue done;
      done:
      case MyRefreshIndicatorMode.done:
      default:
        ///结束状态
        if (latestIndicatorBoxExtent >
            widget.refreshTriggerPullDistance * _inactiveResetOverscrollFraction) {
          return MyRefreshIndicatorMode.done;
        } else {
          nextState = MyRefreshIndicatorMode.inactive;
        }
        break;
    }

    return nextState;
  }

  ///增加外部判断,处理手是不是还在拖拽,如果还在拖拽不触发刷新
  void notifyScrollNotification(ScrollNotification notification) {
    if (notification is ScrollEndNotification) {
      if(refreshState == MyRefreshIndicatorMode.armed) {
        /// 放手了
        draging = false;
      }
    } else if (notification is UserScrollNotification) {
      if(notification.direction != ScrollDirection.idle) {
        /// 手还在拖动
        draging = true;
      } else {
        /// 放手了
        draging = false;
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return _CupertinoSliverRefresh(
      refreshIndicatorLayoutExtent: widget.refreshIndicatorExtent,
      hasLayoutExtent: hasSliverLayoutExtent,
      child: LayoutBuilder(
        builder: (BuildContext context, BoxConstraints constraints) {
          latestIndicatorBoxExtent = constraints.maxHeight;
          refreshState = transitionNextState();
          if (latestIndicatorBoxExtent > 0) {
            return widget.builder(
              context,
              refreshState,
              latestIndicatorBoxExtent,
              widget.refreshTriggerPullDistance,
              widget.refreshIndicatorExtent,
            );
          }
          return Container();
        },
      ),
    );
  }
}

其他一些说明

ListTitle:通常用于在 Flutter 中填充 ListView,系统自带的item,可以满足大多场景

demo

上主要是讲解了一些基本的用法,更详细的可参照demo
demo地址:https://github.com/liuyewu101/flutter_demo

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容