從零開始用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閱讀 229,908評論 6 541
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,324評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,018評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,675評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,417評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,783評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,779評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,960評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,522評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,267評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,471評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,009評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,698評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,099評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,386評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,204評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,436評論 2 378

推薦閱讀更多精彩內容