创建具有嵌套粘性标题的列表

use*_*591 5 flutter

我希望将数据显示在列表中,并使用粘性标题对每个记录进行分组。我找到了很多关于如何在水平上做到这一点的例子,但我只有两个。所以每条记录都会有一个主组和一个子组。因此,当用户滚动时,我希望当前的主组和当前的子组具有粘性。

数据集示例

Main Group 1
    Sub Group 1
        Record 1
        ...
        Record n
    Sub Group 2
        ...
        Record n
    ...
    Sub Group n
        ...
        Record n
...
Main Group n
    ...
    Sub Group n
        ...
        Record n

Run Code Online (Sandbox Code Playgroud)

我已经成功地嵌套了 3 个 ListView 并获取所有要渲染的数据,并且还使用了 Sticky_headers 包中的 StickyHeader 来使主组粘性,但是当在子组上使用 StickyHeader 时,它只是向右滚动通过主组团体

Main Group 1
    Sub Group 1
        Record 1
        ...
        Record n
    Sub Group 2
        ...
        Record n
    ...
    Sub Group n
        ...
        Record n
...
Main Group n
    ...
    Sub Group n
        ...
        Record n

Run Code Online (Sandbox Code Playgroud)

在最坏的情况下,数据集将有大约 100 条记录,这些记录被分组在不同的主组和子组中,因此可以在嵌套列表中使用收缩换行为 true,但如果有另一种方法可以避免这种情况,那就是最好的。

有人对如何解决这个问题有任何想法吗?

Yas*_*ant 1

我能够通过将ScrollController父级传递ListView.builder给父级和子级来创建嵌套的粘性标头StickyHeader,以便两个标头都将获得相同的滚动相关信息。

我扩展了这些类StickyHeaderRenderStickyHeader以便我们可以添加偏移量,使子标题在滚动时保持在父标题下方。

另请注意,这里我假设父标题和子标题的高度相同,如果情况并非如此,那么您应该将父标题的高度作为参数发送给方法determineStuckOffsetWithHeight中的方法。performLayout_MyRenderStickyHeader

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

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

final ScrollController scrollController = ScrollController();

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: ListView.builder(
        itemCount: 10,
        controller: scrollController,
        itemBuilder: (BuildContext context, int mainGroupIndex) {
          return StickyHeader(
            header: Text('Main Group: ${mainGroupIndex + 1}'),
            controller: scrollController,
            overlapHeaders: false,
            content: ListView.builder(
              itemCount: 10,
              primary: false,
              shrinkWrap: true,
              itemBuilder: (BuildContext context, int subGroupIndex) {
                final list = ListView.builder(
                  itemCount: 10,
                  primary: false,
                  shrinkWrap: true,
                  itemBuilder: (BuildContext context, int recordIndex) {
                    return Text('Record: ${recordIndex + 1}');
                  },
                );
                return _MyStickyHeader(
                  header: Text('Sub Group: ${subGroupIndex + 1}'),
                  controller: scrollController,
                  content: list,
                );
              },
            ),
          );
        },
      ),
    );
  }
}

class _MyStickyHeader extends StickyHeader {
  _MyStickyHeader({
    Key? key,
    required this.header,
    required this.content,
    this.overlapHeaders: false,
    this.controller,
    this.callback,
  }) : super(
          key: key,
          header: header,
          content: content,
          overlapHeaders: overlapHeaders,
          controller: controller,
          callback: callback,
        );

  final Widget header;

  final Widget content;

  final bool overlapHeaders;

  final ScrollController? controller;

  final RenderStickyHeaderCallback? callback;

  @override
  _MyRenderStickyHeader createRenderObject(BuildContext context) {
    final scrollPosition =
        this.controller?.position ?? Scrollable.of(context)!.position;
    return _MyRenderStickyHeader(
      scrollPosition: scrollPosition,
      callback: this.callback,
      overlapHeaders: this.overlapHeaders,
    );
  }

  @override
  void updateRenderObject(
      BuildContext context, _MyRenderStickyHeader renderObject) {
    final scrollPosition =
        this.controller?.position ?? Scrollable.of(context)!.position;

    renderObject
      ..scrollPosition = scrollPosition
      ..callback = this.callback
      ..overlapHeaders = this.overlapHeaders;
  }
}

class _MyRenderStickyHeader extends RenderStickyHeader {
  bool _overlapHeaders;
  RenderStickyHeaderCallback? _callback;
  ScrollPosition _scrollPosition;

  _MyRenderStickyHeader({
    required ScrollPosition scrollPosition,
    RenderStickyHeaderCallback? callback,
    bool overlapHeaders: false,
    RenderBox? header,
    RenderBox? content,
  })  : _overlapHeaders = overlapHeaders,
        _callback = callback,
        _scrollPosition = scrollPosition,
        super(
          scrollPosition: scrollPosition,
          callback: callback,
          overlapHeaders: overlapHeaders,
          header: header,
          content: content,
        );

  RenderBox get _headerBox => lastChild!;

  RenderBox get _contentBox => firstChild!;

  @override
  void performLayout() {
    assert(childCount == 2);

    final childConstraints = constraints.loosen();
    _headerBox.layout(childConstraints, parentUsesSize: true);
    _contentBox.layout(childConstraints, parentUsesSize: true);

    final headerHeight = _headerBox.size.height;
    final contentHeight = _contentBox.size.height;

    final width = max(constraints.minWidth, _contentBox.size.width);
    final height = max(constraints.minHeight,
        _overlapHeaders ? contentHeight : headerHeight + contentHeight);
    size = Size(width, height);
    assert(size.width == constraints.constrainWidth(width));
    assert(size.height == constraints.constrainHeight(height));
    assert(size.isFinite);

    final contentParentData =
        _contentBox.parentData as MultiChildLayoutParentData;
    contentParentData.offset =
        Offset(0.0, _overlapHeaders ? 0.0 : headerHeight);

    final double stuckOffset = determineStuckOffsetWithHeight(headerHeight);

    final double maxOffset = height - headerHeight;
    final headerParentData =
        _headerBox.parentData as MultiChildLayoutParentData;

    headerParentData.offset =
        Offset(0.0, max(0.0, min(-stuckOffset, maxOffset)));

    if (_callback != null) {
      final stuckAmount =
          max(min(headerHeight, stuckOffset), -headerHeight) / headerHeight;
      _callback!(stuckAmount);
    }
  }

  double determineStuckOffsetWithHeight(double headerHeight) {
    final scrollBox =
        _scrollPosition.context.notificationContext!.findRenderObject();
    if (scrollBox?.attached ?? false) {
      try {
        return localToGlobal(Offset.zero, ancestor: scrollBox).dy -
            headerHeight;
      } catch (e) {
        // ignore and fall-through and return 0.0
      }
    }
    return 0.0;
  }
}

Run Code Online (Sandbox Code Playgroud)