06_Flutter自定义锚点分类列表

06_Flutter自定义锚点分类列表

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

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

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

  @override
  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();

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

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

  @override
  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]),
              );
            });
          },
        )
      )
    );
  }
}
二.组件基本布局
dart 复制代码
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;

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

}

class _AnchorCategoryListState extends State<AnchorCategoryList> {

  @override
  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完成了

dart 复制代码
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);

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

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

class RenderAfterLayout extends RenderProxyBox {

  AfterLayoutCallback callback;

  RenderAfterLayout(this.callback);

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

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

dart 复制代码
@override
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),
                      ),
                    ),
                  );
                }
              })
            ),
          ]
        )
      )
    ],
  );
}

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

dart 复制代码
@override
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的高度,保证最后能够滑动到最后一项。只需要在右侧列表添加一个空白区域即可。

dart 复制代码
@override
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})方法即可。

dart 复制代码
class _AnchorCategoryListState extends State<AnchorCategoryList> {

  ...

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

  @override
  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;
    });
  }

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

  @override
  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)方法更新选中状态即可。

dart 复制代码
class _AnchorCategoryListState extends State<AnchorCategoryList> {

  ...

  @override
  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);
    }
  }

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

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

}
六.控制标题项吸顶

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

dart 复制代码
class _AnchorCategoryListState extends State<AnchorCategoryList> {

  ...

  @override
  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
                      ...
                    }
                  })
                ),

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

}

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

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

class AnchorCategoryController extends ChangeNotifier {
  int selectedIndex = 0;

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

  @override
  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;

  @override
  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;

  @override
  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);
    }
  }

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

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

  @override
  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,
                  ),
                )
              ]
            ),
          )
        )
      ],
    );
  }

}
相关推荐
还鮟1 小时前
CTF Web的数组巧用
android
小蜜蜂嗡嗡2 小时前
Android Studio flutter项目运行、打包时间太长
android·flutter·android studio
aqi002 小时前
FFmpeg开发笔记(七十一)使用国产的QPlayer2实现双播放器观看视频
android·ffmpeg·音视频·流媒体
zhangphil4 小时前
Android理解onTrimMemory中ComponentCallbacks2的内存警戒水位线值
android
你过来啊你4 小时前
Android View的绘制原理详解
android
瓜子三百克6 小时前
十、高级概念
flutter
移动开发者1号7 小时前
使用 Android App Bundle 极致压缩应用体积
android·kotlin
移动开发者1号7 小时前
构建高可用线上性能监控体系:从原理到实战
android·kotlin
ii_best12 小时前
按键精灵支持安卓14、15系统,兼容64位环境开发辅助工具
android
美狐美颜sdk12 小时前
跨平台直播美颜SDK集成实录:Android/iOS如何适配贴纸功能
android·人工智能·ios·架构·音视频·美颜sdk·第三方美颜sdk