美文网首页
从零开始用flutter写一个完整应用⑷:基础控件列表ListV

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

作者: 逃离_102 | 来源:发表于2022-06-05 22:28 被阅读0次

    说明

    列表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

    相关文章

      网友评论

          本文标题:从零开始用flutter写一个完整应用⑷:基础控件列表ListV

          本文链接:https://www.haomeiwen.com/subject/pbqlmrtx.html