Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Create list with nested sticky headers

Tags:

flutter

I have data that I would like to be displayed in a list, with the use of sticky headers to group each record. I have found plenty of examples on how to do this with on level, but I have two. So each record will have a main group and a sub group. So when the users scrolls I would like for the current main group as well as the current sub group to be sticky.

Example of data set

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

I have managed to nest 3 ListViews and get all the data to render, and have also used a StickyHeader from the sticky_headers package to get the main group sticky, but when using a StickyHeader on the sub group, it just scrolls right passed the the main group

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}');
              },
            ),
          );
        },
      ),
    );
  },
)

The dataset would in worst case have around 100 records that are grouped in different main groups and sub groups, so it would be possible for to use shrink wrap to true in the nested lists, but if there is another way to avoid this that would be best.

Anyone have any ideas on how this could be solved?

like image 959
user3407591 Avatar asked Aug 14 '26 09:08

user3407591


1 Answers

I was able to create nested sticky headers by passing ScrollController of parent ListView.builder to both parent and child StickyHeaders so that both headers will be getting same scrolling related information.

I extended the classes StickyHeader and RenderStickyHeader so that we can add offset that will keep child header below parent header while scrolling.

Also note that here I assumed height of parent header and child header is same, if that is not the case then you should send your parent header's height as argument to determineStuckOffsetWithHeight method in performLayout method of _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;
  }
}

like image 134
Yashawant Avatar answered Aug 16 '26 21:08

Yashawant



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!