06_Flutter自定义锚点分类列表

发布于:2024-05-07 ⋅ 阅读:(20) ⋅ 点赞:(0)

06_Flutter自定义锚点分类列表

在这里插入图片描述

这样的效果,大家在一些商超应用里,应该也看到过。接下来咱们就用Flutter一步一步的来实现。

一.自定义属性抽取

在这里插入图片描述

  • categoryWidth: 左侧边栏的宽度,右侧区域的宽度填充剩余空间即可。
  • itemCount: 总共有多少个分类项,也就是左侧边栏中有多少个字项。
  • sticky: 滑动过程中,右侧标题是否吸顶。
  • controller: 外部通过controller可以控制左侧边栏中子项的选中以及右侧列表滑动位置的联动,同时监听选中状态。
  • categoryItemBuilder: 创建左侧边栏中的每一个分类项。
  • sectionItemBuilder: 创建右侧滑动列表中的每一个标题项。
  • sectionOfChildrenBuilder: 创建右侧滑动列表中的每一个标题项对应的子列表
class AnchorCategoryController extends ChangeNotifier {
  int selectedIndex = 0;

  void selectTo(int value) {
    selectedIndex = value;
    notifyListeners();
  }

  
  void dispose() {
    selectedIndex = 0;
    super.dispose();
  }
}

class _HomePageState extends State<HomePage> {

  final List<String> _sections = ["标题1", "标题2", "标题3", "标题4", "标题5", "标题6", "标题7", "标题8", "标题9", "标题10"];
  final List<List<String>> _childrenList = [
    ["item1", "item2", "item3", "item4", "item5"],
    ["item1", "item2", "item3"],
    ["item1", "item2", "item3", "item4"],
    ["item1"],
    ["item1", "item2"],
    ["item1", "item2", "item3", "item4", "item5", "item6"],
    ["item1", "item2", "item3", "item4"],
    ["item1", "item2", "item3", "item4", "item5"],
    ["item1", "item2", "item3"],
    ["item1", "item2", "item3", "item4", "item5"]
  ];

  int _selectedSectionsIndex = 0;
  final AnchorCategoryController _controller = AnchorCategoryController();

  
  void initState() {
    super.initState();
    _controller.addListener(_onCategoryChanged);
  }

  void _onCategoryChanged() {
    setState(() {
      _selectedSectionsIndex = _controller.selectedIndex;
    });
  }
  
  
  void dispose() {
    _controller.removeListener(_onCategoryChanged);
    _controller.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: SafeArea(
        child: AnchorCategoryList(
          controller: _controller,
          itemCount: _sections.length,
          sticky: true,
          categoryItemBuilder: (BuildContext context, int index) {
            return AlphaButton(
              onTap: () {
                _controller.selectTo(index);
              },
              child: Container(
                padding: const EdgeInsets.all(10),
                color: _selectedSectionsIndex == index ? const Color(0xFFFFFFFF): const Color(0xFFF2F2F2),
                child: Text(_sections[index]),
              ),
            );
          },
          sectionItemBuilder: (BuildContext context, int index) {
            return Container(
              padding: const EdgeInsets.symmetric(vertical: 10),
              alignment: Alignment.centerLeft,
              color: const Color(0xFFF2F2F2),
              child: Text(_sections[index]),
            );
          },
          sectionOfChildrenBuilder: (BuildContext context, int index) {
            return List<Widget>.generate(_childrenList[index].length, (childIndex) {
              return Container(
                padding: const EdgeInsets.symmetric(vertical: 10),
                alignment: Alignment.centerLeft,
                child: Text(_childrenList[index][childIndex]),
              );
            });
          },
        )
      )
    );
  }
}
二.组件基本布局
class AnchorCategoryList extends StatefulWidget {

  final double categoryWidth;
  final int itemCount;
  final IndexedWidgetBuilder categoryItemBuilder;
  final IndexedWidgetBuilder sectionItemBuilder;
  final IndexedWidgetListBuilder sectionOfChildrenBuilder;
  final bool sticky;
  final AnchorCategoryController? controller;

  const AnchorCategoryList({
    super.key,
    required this.categoryItemBuilder,
    required this.sectionItemBuilder,
    required this.sectionOfChildrenBuilder,
    this.controller,
    double? categoryWidth,
    int? itemCount,
    bool? sticky
  }): categoryWidth = categoryWidth ?? 112,
        itemCount = itemCount ?? 0,
        sticky = sticky ?? true;

  
  State<StatefulWidget> createState() => _AnchorCategoryListState();

}

class _AnchorCategoryListState extends State<AnchorCategoryList> {

  
  Widget build(BuildContext context) {
    return Row(
      mainAxisSize: MainAxisSize.max,
      mainAxisAlignment: MainAxisAlignment.start,
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        SizedBox(
          width: widget.categoryWidth,
          child: LayoutBuilder(
            builder: (context, viewportConstraints) {
              return SingleChildScrollView(
                child: ConstrainedBox(
                  constraints: BoxConstraints(
                    minHeight: viewportConstraints.maxHeight != double.infinity ? viewportConstraints.maxHeight:0
                  ),
                  child: Column(
                    mainAxisSize: MainAxisSize.max,
                    mainAxisAlignment: MainAxisAlignment.start,
                    crossAxisAlignment: CrossAxisAlignment.stretch,
                    children: List.generate(widget.itemCount, (index) {
                      return widget.categoryItemBuilder.call(context, index);
                    }),
                  ),
                ),
              );
            },
          )
        ),
        Expanded(
          child: CustomScrollView(
            physics: const ClampingScrollPhysics(),
            slivers: [
              ...(
                List<Widget>.generate(widget.itemCount * 2, (allIndex) {
                  int index = allIndex ~/ 2;
                  if(allIndex.isEven) {
                    //section
                    return SliverToBoxAdapter(
                      child: widget.sectionItemBuilder.call(context, index),
                    );
                  } else {
                    //children
                    return SliverToBoxAdapter(
                      child: Column(
                        children: widget.sectionOfChildrenBuilder.call(context, index),
                      ),
                    );
                  }
                })
              ),
            ]
          )
        )
      ],
    );
  }

}
三.获取并保存标题项、标题项对应子列表的高度

这里获取标题项、标题项对应子列表的高度,需要等到控件build完成后,才能获取到,因此需要自定义一个控件继承SingleChildRenderObjectWidget,并指定一个自定义的RenderBox,在performLayout中通过回调通知外部,控件layout完成了

typedef AfterLayoutCallback = Function(RenderBox ral);

class AfterLayout extends SingleChildRenderObjectWidget {

  final AfterLayoutCallback callback;

  const AfterLayout({
    Key? key,
    required this.callback,
    Widget? child,
  }) : super(key: key, child: child);

  
  RenderObject createRenderObject(BuildContext context) {
    return RenderAfterLayout(callback);
  }

  
  void updateRenderObject(context, RenderAfterLayout renderObject) {
    renderObject.callback = callback;
  }
}

class RenderAfterLayout extends RenderProxyBox {

  AfterLayoutCallback callback;

  RenderAfterLayout(this.callback);

  
  void performLayout() {
    super.performLayout();
    SchedulerBinding.instance
        .addPostFrameCallback((timeStamp) => callback(this));
  }
  
}

使用AfterLayout获取并保存标题项、标题项对应子列表的高度


Widget build(BuildContext context) {
  return Row(
    mainAxisSize: MainAxisSize.max,
    mainAxisAlignment: MainAxisAlignment.start,
    crossAxisAlignment: CrossAxisAlignment.stretch,
    children: [
      SizedBox(
        width: widget.categoryWidth,
        child: LayoutBuilder(
          builder: (context, viewportConstraints) {
            return SingleChildScrollView(
              child: ConstrainedBox(
                constraints: BoxConstraints(
                  minHeight: viewportConstraints.maxHeight != double.infinity ? viewportConstraints.maxHeight:0
                ),
                child: Column(
                  mainAxisSize: MainAxisSize.max,
                  mainAxisAlignment: MainAxisAlignment.start,
                  crossAxisAlignment: CrossAxisAlignment.stretch,
                  children: List.generate(widget.itemCount, (index) {
                    return widget.categoryItemBuilder.call(context, index);
                  }),
                ),
              ),
            );
          },
        )
      ),
      Expanded(
        child: CustomScrollView(
          physics: const ClampingScrollPhysics(),
          slivers: [
            ...(
              List<Widget>.generate(widget.itemCount * 2, (allIndex) {
                int index = allIndex ~/ 2;
                if(allIndex.isEven) {
                  //section
                  return SliverToBoxAdapter(
                    child: AfterLayout(
                      callback: (renderBox) {
                        double height = renderBox.size.height;
                        setState(() {
                          if(_sectionHeightList.length > index) {
                            _sectionHeightList[index] = height;
                          } else {
                            _sectionHeightList.add(height);
                          }
                        });
                      },
                      child: widget.sectionItemBuilder.call(context, index),
                    ),
                  );
                } else {
                  //children
                  return SliverToBoxAdapter(
                    child: AfterLayout(
                      callback: (renderBox) {
                        double height = renderBox.size.height;
                        setState(() {
                          if(_childrenHeightList.length > index) {
                            _childrenHeightList[index] = height;
                          } else {
                            _childrenHeightList.add(height);
                          }
                        });
                      },
                      child: Column(
                        children: widget.sectionOfChildrenBuilder.call(context, index),
                      ),
                    ),
                  );
                }
              })
            ),
          ]
        )
      )
    ],
  );
}

计算并保存右侧面板每一项选中时的初始滑动偏移量

在这里插入图片描述


Widget build(BuildContext context) {
  return Row(
    mainAxisSize: MainAxisSize.max,
    mainAxisAlignment: MainAxisAlignment.start,
    crossAxisAlignment: CrossAxisAlignment.stretch,
    children: [
      SizedBox(
        width: widget.categoryWidth,
        child: LayoutBuilder(
          builder: (context, viewportConstraints) {
            return SingleChildScrollView(
              child: ConstrainedBox(
                constraints: BoxConstraints(
                  minHeight: viewportConstraints.maxHeight != double.infinity ? viewportConstraints.maxHeight:0
                ),
                child: Column(
                  mainAxisSize: MainAxisSize.max,
                  mainAxisAlignment: MainAxisAlignment.start,
                  crossAxisAlignment: CrossAxisAlignment.stretch,
                  children: List.generate(widget.itemCount, (index) {
                    return widget.categoryItemBuilder.call(context, index);
                  }),
                ),
              ),
            );
          },
        )
      ),
      Expanded(
        child: AfterLayout(
          callback: (renderBox) {
            setState(() {
              for(int i = 0; i < widget.itemCount; i ++) {
                double scrollOffset = 0;
                for(int j=0; j<i; j++) {
                  scrollOffset += _sectionHeightList[j] + _childrenHeightList[j];
                }
                if(_scrollOffsetList.length > i) {
                  _scrollOffsetList[i] = scrollOffset;
                } else {
                  _scrollOffsetList.add(scrollOffset);
                }
              }
              debugPrint("CustomScrollView AfterLayout: $_scrollOffsetList");
            });
          },
          child: CustomScrollView(
            physics: const ClampingScrollPhysics(),
            slivers: [
              ...(
                List<Widget>.generate(widget.itemCount * 2, (allIndex) {
                  int index = allIndex ~/ 2;
                  if(allIndex.isEven) {
                    //section
                    return SliverToBoxAdapter(
                      child: AfterLayout(
                        callback: (renderBox) {
                          double height = renderBox.size.height;
                          setState(() {
                            if(_sectionHeightList.length > index) {
                              _sectionHeightList[index] = height;
                            } else {
                              _sectionHeightList.add(height);
                            }
                          });
                        },
                        child: widget.sectionItemBuilder.call(context, index),
                      ),
                    );
                  } else {
                    //children
                    return SliverToBoxAdapter(
                      child: AfterLayout(
                        callback: (renderBox) {
                          double height = renderBox.size.height;
                          setState(() {
                            if(_childrenHeightList.length > index) {
                              _childrenHeightList[index] = height;
                            } else {
                              _childrenHeightList.add(height);
                            }
                          });
                        },
                        child: Column(
                          children: widget.sectionOfChildrenBuilder.call(context, index),
                        ),
                      ),
                    );
                  }
                })
              ),
            ]
          ),
        )
      )
    ],
  );
}
四.点击选中分类项时,右侧自动滑动至相应位置

首先,这里需要把右侧列表最后一项的高度设置为ViewPort的高度,保证最后能够滑动到最后一项。只需要在右侧列表添加一个空白区域即可。


Widget build(BuildContext context) {
  return Row(
    mainAxisSize: MainAxisSize.max,
    mainAxisAlignment: MainAxisAlignment.start,
    crossAxisAlignment: CrossAxisAlignment.stretch,
    children: [
      ...,
      Expanded(
        child: AfterLayout(
          callback: (renderBox) {
            setState(() {
              ...

              if(widget.itemCount > 0) {
                _extraHeight = max(renderBox.size.height - _childrenHeightList[widget.itemCount - 1], 0);
              } else {
                _extraHeight = 0;
              }
            });
          },
          child: CustomScrollView(
            physics: const ClampingScrollPhysics(),
            slivers: [
              ...,

              SliverToBoxAdapter(
                child: SizedBox(
                  height: _extraHeight,
                ),
              )
            ]
          ),
        )
      )
    ],
  );
}

根据前面确定好初始的滑动偏移量之后,就能很方便的控制右侧列表的滑动了,我们通过给右侧列表指定ScrollController,同时调用ScrollController的animateTo(double offset, {required Duration duration, required Curve curve})方法即可。

class _AnchorCategoryListState extends State<AnchorCategoryList> {

  ...

  final ScrollController _scrollController = ScrollController();
  int _selectedIndex = 0;
  bool _scrollLocked = false;

  
  void initState() {
    super.initState();
    if(widget.controller != null) {
      widget.controller!.addListener(_onIndexChange);
    }
  }

  void _onIndexChange() {
    if(_selectedIndex == widget.controller!.selectedIndex) {
      return;
    }

    _scrollLocked = true;

    _selectedIndex = widget.controller!.selectedIndex;
    widget.controller!.selectTo(_selectedIndex);
    _scrollController.animateTo(
        _scrollOffsetList[widget.controller!.selectedIndex],
        duration: const Duration(milliseconds: 300),
        curve: Curves.linear
    ).then((value) {
      _scrollLocked = false;
    });
  }

  
  void dispose() {
    _scrollController.dispose();
    
    if(widget.controller != null) {
      widget.controller!.removeListener(_onIndexChange);
    }
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Row(
      mainAxisSize: MainAxisSize.max,
      mainAxisAlignment: MainAxisAlignment.start,
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        ...,
        Expanded(
          child: AfterLayout(
            callback: (renderBox) {
              setState(() {
                for(int i = 0; i < widget.itemCount; i ++) {
                  double scrollOffset = 0;
                  for(int j=0; j<i; j++) {
                    scrollOffset += _sectionHeightList[j] + _childrenHeightList[j];
                  }
                  if(_scrollOffsetList.length > i) {
                    _scrollOffsetList[i] = scrollOffset;
                  } else {
                    _scrollOffsetList.add(scrollOffset);
                  }
                }

                if(widget.itemCount > 0) {
                  _extraHeight = max(renderBox.size.height - _childrenHeightList[widget.itemCount - 1], 0);
                } else {
                  _extraHeight = 0;
                }
              });
            },
            child: CustomScrollView(
              physics: const ClampingScrollPhysics(),
              controller: _scrollController,
              slivers: [
                ...
              ]
            ),
          )
        )
      ],
    );
  }

}

在这里插入图片描述

五.右侧列表滚动时,动态改变左侧边栏的选中状态

监听右侧列表的滑动,获取滑动位置,与所有子项的初始滑动偏移量对比,可以计算出左侧边栏的哪一个子项应该被选中,然后通过AnchorCategoryController的selectTo(int value)方法更新选中状态即可。

class _AnchorCategoryListState extends State<AnchorCategoryList> {

  ...

  
  void initState() {
    super.initState();
    if(widget.controller != null) {
      widget.controller!.addListener(_onIndexChange);
    }

    _scrollController.addListener(_onScrollChange);
  }

  ...

  void _onScrollChange() {
    if(_scrollLocked) {
      return;
    }

    double scrollOffset = _scrollController.offset;
    int selectedIndex = 0;
    for(int index = _scrollOffsetList.length - 1; index >= 0; index --) {
      selectedIndex = index;
      if(scrollOffset.roundToDouble() >= _scrollOffsetList[index]) {
        break;
      }
    }

    if(_selectedIndex != selectedIndex) {
      _selectedIndex = selectedIndex;
      widget.controller!.selectTo(selectedIndex);
    }
  }

  
  void dispose() {
    _scrollController.removeListener(_onScrollChange);
    _scrollController.dispose();

    if(widget.controller != null) {
      widget.controller!.removeListener(_onIndexChange);
    }
    super.dispose();
  }
  
  ...

}
六.控制标题项吸顶

将标题项的SliverToBoxAdapter替换成StickySliverToBoxAdapter即可,关于StickySliverToBoxAdapter可以查看这篇文章02_Flutter自定义Sliver组件实现分组列表吸顶效果

class _AnchorCategoryListState extends State<AnchorCategoryList> {

  ...

  
  Widget build(BuildContext context) {
    return Row(
      mainAxisSize: MainAxisSize.max,
      mainAxisAlignment: MainAxisAlignment.start,
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        ...,
        Expanded(
          child: AfterLayout(
            callback: (renderBox) {
              ...
            },
            child: CustomScrollView(
              physics: const ClampingScrollPhysics(),
              controller: _scrollController,
              slivers: [
                ...(
                  List<Widget>.generate(widget.itemCount * 2, (allIndex) {
                    int index = allIndex ~/ 2;
                    if(allIndex.isEven) {
                      //section
                      Widget sectionItem = AfterLayout(
                        callback: (renderBox) {
                          double height = renderBox.size.height;
                          setState(() {
                            if(_sectionHeightList.length > index) {
                              _sectionHeightList[index] = height;
                            } else {
                              _sectionHeightList.add(height);
                            }
                          });
                        },
                        child: widget.sectionItemBuilder.call(context, index),
                      );

                      if(widget.sticky) {
                        return StickySliverToBoxAdapter(
                          child: sectionItem,
                        );
                      } else {
                        return SliverToBoxAdapter(
                          child: sectionItem,
                        );
                      }
                    } else {
                      //children
                      ...
                    }
                  })
                ),

                ...
              ]
            ),
          )
        )
      ],
    );
  }

}

在这里插入图片描述

搞定,模拟器录屏掉帧了,改用真机录屏😄。

七.完整代码
typedef IndexedWidgetListBuilder = List<Widget> Function(BuildContext context, int index);

class AnchorCategoryController extends ChangeNotifier {
  int selectedIndex = 0;

  void selectTo(int value) {
    selectedIndex = value;
    notifyListeners();
  }

  
  void dispose() {
    selectedIndex = 0;
    super.dispose();
  }
}

class AnchorCategoryList extends StatefulWidget {

  final double categoryWidth;
  final int itemCount;
  final IndexedWidgetBuilder categoryItemBuilder;
  final IndexedWidgetBuilder sectionItemBuilder;
  final IndexedWidgetListBuilder sectionOfChildrenBuilder;
  final bool sticky;
  final AnchorCategoryController? controller;

  const AnchorCategoryList({
    super.key,
    required this.categoryItemBuilder,
    required this.sectionItemBuilder,
    required this.sectionOfChildrenBuilder,
    this.controller,
    double? categoryWidth,
    int? itemCount,
    bool? sticky
  }): categoryWidth = categoryWidth ?? 112,
        itemCount = itemCount ?? 0,
        sticky = sticky ?? true;

  
  State<StatefulWidget> createState() => _AnchorCategoryListState();

}

class _AnchorCategoryListState extends State<AnchorCategoryList> {

  final List<double> _sectionHeightList = [];
  final List<double> _childrenHeightList = [];
  final List<double> _scrollOffsetList = [];
  double _extraHeight = 0;

  final ScrollController _scrollController = ScrollController();
  int _selectedIndex = 0;
  bool _scrollLocked = false;

  
  void initState() {
    super.initState();
    if(widget.controller != null) {
      widget.controller!.addListener(_onIndexChange);
    }

    _scrollController.addListener(_onScrollChange);
  }

  void _onIndexChange() {
    if(_selectedIndex == widget.controller!.selectedIndex) {
      return;
    }

    _scrollLocked = true;

    _selectedIndex = widget.controller!.selectedIndex;
    widget.controller!.selectTo(_selectedIndex);
    _scrollController.animateTo(
        _scrollOffsetList[widget.controller!.selectedIndex],
        duration: const Duration(milliseconds: 300),
        curve: Curves.linear
    ).then((value) {
      _scrollLocked = false;
    });
  }

  void _onScrollChange() {
    if(_scrollLocked) {
      return;
    }

    double scrollOffset = _scrollController.offset;
    int selectedIndex = 0;
    for(int index = _scrollOffsetList.length - 1; index >= 0; index --) {
      selectedIndex = index;
      if(scrollOffset.roundToDouble() >= _scrollOffsetList[index]) {
        break;
      }
    }

    if(_selectedIndex != selectedIndex) {
      _selectedIndex = selectedIndex;
      widget.controller!.selectTo(selectedIndex);
    }
  }

  
  void dispose() {
    _scrollController.removeListener(_onScrollChange);
    _scrollController.dispose();

    if(widget.controller != null) {
      widget.controller!.removeListener(_onIndexChange);
    }
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Row(
      mainAxisSize: MainAxisSize.max,
      mainAxisAlignment: MainAxisAlignment.start,
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        SizedBox(
          width: widget.categoryWidth,
          child: LayoutBuilder(
            builder: (context, viewportConstraints) {
              return SingleChildScrollView(
                child: ConstrainedBox(
                  constraints: BoxConstraints(
                    minHeight: viewportConstraints.maxHeight != double.infinity ? viewportConstraints.maxHeight:0
                  ),
                  child: Column(
                    mainAxisSize: MainAxisSize.max,
                    mainAxisAlignment: MainAxisAlignment.start,
                    crossAxisAlignment: CrossAxisAlignment.stretch,
                    children: List.generate(widget.itemCount, (index) {
                      return widget.categoryItemBuilder.call(context, index);
                    }),
                  ),
                ),
              );
            },
          )
        ),
        Expanded(
          child: AfterLayout(
            callback: (renderBox) {
              setState(() {
                for(int i = 0; i < widget.itemCount; i ++) {
                  double scrollOffset = 0;
                  for(int j=0; j<i; j++) {
                    scrollOffset += _sectionHeightList[j] + _childrenHeightList[j];
                  }
                  if(_scrollOffsetList.length > i) {
                    _scrollOffsetList[i] = scrollOffset;
                  } else {
                    _scrollOffsetList.add(scrollOffset);
                  }
                }

                if(widget.itemCount > 0) {
                  _extraHeight = max(renderBox.size.height - _childrenHeightList[widget.itemCount - 1], 0);
                } else {
                  _extraHeight = 0;
                }
              });
            },
            child: CustomScrollView(
              physics: const ClampingScrollPhysics(),
              controller: _scrollController,
              slivers: [
                ...(
                  List<Widget>.generate(widget.itemCount * 2, (allIndex) {
                    int index = allIndex ~/ 2;
                    if(allIndex.isEven) {
                      //section
                      Widget sectionItem = AfterLayout(
                        callback: (renderBox) {
                          double height = renderBox.size.height;
                          setState(() {
                            if(_sectionHeightList.length > index) {
                              _sectionHeightList[index] = height;
                            } else {
                              _sectionHeightList.add(height);
                            }
                          });
                        },
                        child: widget.sectionItemBuilder.call(context, index),
                      );

                      if(widget.sticky) {
                        return StickySliverToBoxAdapter(
                          child: sectionItem,
                        );
                      } else {
                        return SliverToBoxAdapter(
                          child: sectionItem,
                        );
                      }
                    } else {
                      //children
                      return SliverToBoxAdapter(
                        child: AfterLayout(
                          callback: (renderBox) {
                            double height = renderBox.size.height;
                            setState(() {
                              if(_childrenHeightList.length > index) {
                                _childrenHeightList[index] = height;
                              } else {
                                _childrenHeightList.add(height);
                              }
                            });
                          },
                          child: Column(
                            children: widget.sectionOfChildrenBuilder.call(context, index),
                          ),
                        ),
                      );
                    }
                  })
                ),

                SliverToBoxAdapter(
                  child: SizedBox(
                    height: _extraHeight,
                  ),
                )
              ]
            ),
          )
        )
      ],
    );
  }

}