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

icnyk63a  于 2023-05-30  发布在  Flutter
关注(0)|答案(3)|浏览(271)

我有数据,我想在一个列表中显示,使用粘性头来分组每个记录。我已经找到了很多关于如何在水平上做到这一点的例子,但我有两个。所以每个记录都有一个主组和一个子组。因此,当用户滚动我想为当前的主组以及当前的子组是粘性。
数据集示例

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

我已经设法嵌套了3个ListViews并获得了所有要呈现的数据,并且还使用了sticky_headers包中的StickyHeader来获得主组的粘性,但是当在子组上使用StickyHeader时,它只是向右滚动通过了主组

ListView.builder(
  itemCount: 10,
  itemBuilder: (BuildContext context, int mainGroupIndex) {
    return StickyHeader(
      header: Text('Main Group: ${mainGroupIndex + 1}'),
      content: ListView.builder(
        itemCount: 10,
        primary: false,
        shrinkWrap: true,
        itemBuilder: (BuildContext context, int subGroupIndex) {
          return StickyHeader(
            header: Text('Sub Group: ${subGroupIndex + 1}'),
            content: ListView.builder(
              itemCount: 10,
              primary: false,
              shrinkWrap: true,
              itemBuilder: (BuildContext context, int recordIndex) {
                return Text('Record: ${recordIndex + 1}');
              },
            ),
          );
        },
      ),
    );
  },
)

在最坏的情况下,数据集将有大约100条记录,这些记录被分组在不同的主组和子组中,因此可以在嵌套列表中使用收缩 Package 为true,但如果有另一种方法来避免这种情况,那将是最好的。
有谁知道如何解决这个问题吗?

xsuvu9jc

xsuvu9jc1#

我可以通过将父ListView.builderScrollController传递给父StickyHeader和子StickyHeader来创建嵌套的粘性标题,这样两个标题将获得相同的滚动相关信息。
我扩展了类StickyHeaderRenderStickyHeader,这样我们就可以添加偏移量,使子标题在滚动时保持在父标题之下。
还要注意的是,这里我假设父头和子头的高度是相同的,如果不是这样,那么你应该把你的父头的高度作为参数发送给_MyRenderStickyHeaderperformLayout方法中的determineStuckOffsetWithHeight方法。

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;
  }
}
blmhpbnm

blmhpbnm2#

使用修改后的flutter_sticky_header可以实现这一点,即使是几个级别的深度。修改后的版本可以在这里找到:pull request
您可以将修改后的版本用于:

dependency_overrides:
  flutter_sticky_header:
    git:
      url: https://github.com/UnderKoen/flutter_sticky_header.git
      ref: master

使用这种实现,您需要更改为CustomScrollView而不是ListView。您的示例如下所示:

CustomScrollView(
  slivers: List.generate(
    10,
    (i) => SliverStickyHeader(
      header: Text('Main Group $i'),
      slivers: List.generate(
        10,
        (j) => SliverStickyHeader(
          header: Text('Sub Group $j'),
          slivers: [
            SliverList(
              delegate: SliverChildBuilderDelegate(
                (context, k) => Text('Record $k'),
                childCount: 10,
              ),
            ),
          ],
        ),
      ),
    ),
  ),
)
vx6bjr1n

vx6bjr1n3#

我想分享一个很棒的库,它允许你拥有多层嵌套的标题,并且是高度可定制的:flutter_sticky_infinite_list
查看工作演示https://zapp.run/edit/z99i06vq99j0?theme=dark&lazy=false
对于一个没有嵌套头部的简单设置,您可以像这样使用它:

import 'package:sticky_infinite_list/sticky_infinite_list.dart';

class Example extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return InfiniteList(
      builder: (BuildContext context, int index) {
        /// Builder requires [InfiniteList] to be returned
        return InfiniteListItem(
          /// Header builder
          headerBuilder: (BuildContext context) {
            return Container(
              ///...
            );
          },
          /// Content builder
          contentBuilder: (BuildContext context) {
            return Container(
              ///...
            );
          },
        );
      }
    );
  }
}

现在,如果你想添加一个嵌套的header,你可以把StickyListItem放在widget树的任何地方:

contentBuilder: (context) => Padding(
    padding: const EdgeInsets.only(left: 8),
    child: Column(
      children: [
        Container(
          color: Colors.blue,
          child: StickyListItem<String>.overlay(
            header: Padding(
              padding: EdgeInsets.only(left: 100, top: 4),
              child: Text(
                "NESTED HEADER 1",
                style: TextStyle(
                  fontSize: 12,
                  fontWeight: FontWeight.w300,
                ),
              ),
            ),
            content: Padding(
              padding: EdgeInsets.only(left: 34),
              child: Container(height: 500),
            ),
            itemIndex: '1',
          ),
        ),
        Container(
          color: Colors.green,
          child: StickyListItem<String>.overlay(
            header: Padding(
              padding: EdgeInsets.only(left: 100, top: 4),
              child: Text(
                "NESTED HEADER 2",
                style: TextStyle(
                  fontSize: 12,
                  fontWeight: FontWeight.w300,
                ),
              ),
            ),
            content: Padding(
              padding: EdgeInsets.only(left: 34),
              child: Container(height: 500),
            ),
            itemIndex: '1',
          ),
        ),
      ],
    ),
  ),

相关问题