06_Flutter自定义锚点分类列表

news2024/10/6 8:36:50

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

}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1648053.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Python进阶之-jinja2详解

✨前言&#xff1a; &#x1f31f;什么是jinja2&#xff1f; Jinja2 是一个强大的 Python 模版引擎&#xff0c;主要用于生成HTML或其他文本文件。这个库非常适合开发动态网站和Web应用的视图层&#xff0c;因为它支持逻辑操作如循环和条件判断&#xff0c;还可以继承和重用模…

信息系统项目管理师0091:项目经理的能力(6项目管理概论—6.3项目经理的角色—6.3.3项目经理的能力)

点击查看专栏目录 文章目录 6.3.3项目经理的能力1.概述2.项目管理技能3.战略和商务管理技能4.领导力技能5.领导力与管理记忆要点总结6.3.3项目经理的能力 1.概述 项目经理需要重点关注三个方面的关键技能包括项目管理、战略和商务、领导力

linux进阶篇:Nginx反向代理原理与案例详解

Linux服务搭建篇&#xff1a;Nginx反向代理原理与案例详解 一、什么是正向代理 举个栗子&#xff1a; 我们在校外、公司外&#xff0c;是访问不到学校、公司的内网的&#xff0c;但是我们想要访问内网资源时&#xff0c;会用到VPN。而一般内网会存在一个VPN服务器&#xff0c…

微信开发者工具Cannot read property ‘getCloudAPI‘ of undefined

运行项目时一开始是正常的&#xff0c;然后就遇到工具报错&#xff1a;Error: Fatal: unexpected loadSdkSubPackage case&#xff0c;尝试了更新开发工具、重新拉取代码后依然无效 解决方案&#xff1a; 最新版开发者工具&#xff0c;基础库用最新的2.31.0就出现这个错误 降…

【计算机科学速成课】笔记四

文章目录 19.内存&存储介质课程引出——内存与存储器的区别纸带存储磁芯存储磁带、磁鼓存储磁盘&#xff08;硬盘&#xff09;存储软盘存储光盘存储&#xff08;CD&DVD&#xff09;固态硬盘存储 20.文件系统课程引出——文件格式.txt文本文件.wav 音频文件.bmp位图文件…

Golang | Leetcode Golang题解之第67题二进制求和

题目&#xff1a; 题解&#xff1a; func addBinary(a string, b string) string {ans : ""carry : 0lenA, lenB : len(a), len(b)n : max(lenA, lenB)for i : 0; i < n; i {if i < lenA {carry int(a[lenA-i-1] - 0)}if i < lenB {carry int(b[lenB-i-1…

今天看到一个有意思的问题:个人网站被恶意大量访问,怎么办(文末附GPT指令优化)

目录 问题描述 一、GPT 3.5 二、通义千问 三、讯飞星火 四、文心一言 五、Kimi 六、智谱清言 个人分析&#xff1a; 问题描述 大家好&#xff01;我的个人网站每天晚上7点30到11点被固定的十几个IP大量下载exe&#xff0c;造成网站带宽不够&#xff0c;怎么办! 已经把…

谷歌发布 HEAL 架构,4 步评估医学 AI 工具是否公平

如果把维持健康状态想象成一场赛跑&#xff0c;并不是所有人都能够站在统一起跑线上&#xff0c;有的人能够平稳的跑完全程&#xff0c;有的人即使跌倒也能够在第一时间获得帮助&#xff0c;但是有些人可能因为经济条件、居住地、教育水平、种族或其他因素而面临更多障碍。 「…

AHB---数据总线

1. 数据总线 为了实现AHB系统&#xff0c;需要独立的读写数据总线。虽然推荐的最小数据总线宽度被指定为32位&#xff0c;但这可以根据数据总线宽度进行更改。 数据总线包含以下部分&#xff1a; HWDATAHRDATAEndianness&#xff08;字节序&#xff09; 1.1 HWDATA 在写传输…

2024年最新 CKA 导航页

1. Dokcer 基础相关 Docker 、 Docker-Compose 安装教程Docker基础知识、相关概念以及基本使用命令Docker 一句话删除所有镜像/容器 2. CKA 相关学习 CKA&#xff08;Certified Kubernetes Administrator&#xff09;是由 Cloud Native Computing Foundation&#xff08;CNC…

嵌入式C语言教程:实现声音监测系统

声音监测在许多应用中都十分重要&#xff0c;如噪声控制、安全系统、和智能家居控制。 本教程将介绍如何在STM32微控制器上使用模数转换器&#xff08;ADC&#xff09;和声音传感器实现实时声音监测系统。 一、开发环境准备 硬件要求 微控制器&#xff1a;STM32F746NG&…

美国站群服务器在站群管理中的防护和数据安全保障?

美国站群服务器在站群管理中的防护和数据安全保障? 美国站群服务器的防护和数据安全保障是站群管理中的关键问题。站群服务器位于美国&#xff0c;通常由专业的服务器提供商管理和维护&#xff0c;其安全性受到多方面因素的影响。 美国站群服务器在站群管理中的防护和数据安全…

《深入解析WIndows操作系统》第10章读书笔记

1、大页面和小页面&#xff1a;虚拟地址空间被划分成以页面为单位&#xff0c;这是因为硬件内存管理单元在页面的粒度上&#xff0c;将虚拟地址转译为物理地址。Windows支持两种页面尺寸&#xff1a;大页面和小页面&#xff0c;根据处理器体系结构不同&#xff0c;实际尺寸值有…

Springai入门

一、概述 1.1发展历史 1.2大模型 大模型&#xff0c;是指具有大规模参数和复杂计算结构的机器学习模型。这些模型通常由深度神经网络构建而成&#xff0c;拥有数十亿甚至数千亿个参数。其设计目的在于提高模型的表达能力和预测性能&#xff0c;以应对更加复杂的任务和数据&…

Guer完成对Gallium Semi的GaN产品组合的收购

预计到2024年6月&#xff0c;完整的射频氮化镓产品组合将完成整合 总部位于美国的Guerrilla RF (GUER)已完成对镓半导体公司GaN功率放大器和前端模块的全部收购。 自2024年4月26日起&#xff0c;GUER收购了Gallium Semiconductor先前发布的所有组件以及正在开发的新内核。此外…

2024蓝桥杯RSA-Theorem

方法1&#xff1a;直接使用工具yafu解题 yafu的使用方法 安装&#xff1a;解压后直接使用即可&#xff0c;在文件包内&#xff0c;执行命令终端&#xff0c;输入命令行 1、如果数比较小&#xff0c;进入该文件的目录后可以直接使用: yafu-x64 factor(n) 如果是powershell&…

本地存储和cookie之间的区别是什么?

本地存储和cookie是两种在web开发中常用的客户端存储技术&#xff0c;它们都可以用来在用户的浏览器中存储数据&#xff0c;但是它们之间有一些重要的区别。 本地存储是HTML5引入的一种客户端存储技术&#xff0c;它允许开发者在用户的浏览器中存储大量的数据&#xff0c;包括…

MySQL 中的HASH详解

MySQL中的哈希索引&#xff08;Hash Index&#xff09;是一种特殊的数据库索引类型&#xff0c;它利用哈希表&#xff08;Hash Table&#xff09;的数据结构来存储索引项。哈希表通过哈希函数&#xff08;Hash Function&#xff09;将索引列的值转化为一个固定长度的哈希码&…

【资源分享】Origin2022Pro免费安装下载

::: block-1 “时问桫椤”是一个致力于为本科生到研究生教育阶段提供帮助的不太正式的公众号。我们旨在在大家感到困惑、痛苦或面临困难时伸出援手。通过总结广大研究生的经验&#xff0c;帮助大家尽早适应研究生生活&#xff0c;尽快了解科研的本质。祝一切顺利&#xff01;—…

机器人系统ros2-开发实践06-将静态坐标系广播到 tf2(Python)-定义机器人底座与其传感器或非移动部件之间的关系

发布静态变换对于定义机器人底座与其传感器或非移动部件之间的关系非常有用。例如&#xff0c;最容易推断激光扫描仪中心框架中的激光扫描测量结果。 1. 创建包 首先&#xff0c;我们将创建一个用于本教程和后续教程的包。调用的包learning_tf2_py将依赖于geometry_msgs、pyth…