当前位置:   article > 正文

Flutter 之 CustomScrollView & Slivers_flutter customscrollview

flutter customscrollview

1. CustomScrollView

ListView、GridView、PageView 都是一个完整的可滚动组件,所谓完整是指它们都包括Scrollable 、 Viewport 和 Sliver。假如我们想要在一个页面中,同时包含多个可滚动组件,且使它们的滑动效果能统一起来,比如:我们想将已有的两个沿垂直方向滚动的 ListView 成一个 ListView ,这样在第一ListView 滑动到底部时能自动接上第二 ListView,如果尝试写一个 demo:

  1. class MSTwoListViewDemo1 extends StatelessWidget {
  2. @override
  3. Widget build(BuildContext context) {
  4. var listView = ListView.builder(
  5. itemCount: 20,
  6. itemBuilder: (_, index) => ListTile(title: Text('$index')),
  7. );
  8. return Column(
  9. children: [
  10. Expanded(child: listView),
  11. Divider(color: Colors.red, thickness: 5),
  12. Expanded(child: listView),
  13. ],
  14. );
  15. }
  16. }

运行效果如下:

页面中有两个 ListView,各占可视区域一半高度,虽然能够显式出来,但每一个 ListView 只会响应自己可视区域中滑动,实现不了我们想要的效果。之所以会这样的原因是两个 ListView 都有自己独立的 Scrollable 、 Viewport 和 Sliver。

既然如此,我们自己创建一个共用的 Scrollable 和 Viewport 对象,然后再将两个 ListView 对应的 Sliver 添加到这个共用的 Viewport 对象中就可以实现我们想要的效果了。如果这个工作让开发者自己来做无疑是比较麻烦的,因此 Flutter 提供了一个 CustomScrollView 组件来帮助我们创建一个公共的 Scrollable 和 Viewport ,然后它的 slivers 参数接受一个 Sliver 数组,这样我们就可以使用CustomScrollView 方面的实现我们期望的功能了

代码如下:

  1. class MSTwoListViewDemo2 extends StatelessWidget {
  2. @override
  3. Widget build(BuildContext context) {
  4. // SliverFixedExtentList 是一个 Sliver,它可以生成高度相同的列表项。
  5. // 再次提醒,如果列表项高度相同,我们应该优先使用SliverFixedExtentList
  6. // 和 SliverPrototypeExtentList,如果不同,使用 SliverList.
  7. SliverFixedExtentList listSliver = SliverFixedExtentList(
  8. delegate: SliverChildBuilderDelegate((ctx, index) {
  9. return ListTile(
  10. title: Text("$index"),
  11. );
  12. }, childCount: 10),
  13. itemExtent: 56,
  14. );
  15. return CustomScrollView(
  16. slivers: [listSliver, listSliver],
  17. );
  18. }
  19. }

运行效果如下:

注意
我们看源码发现,CustomScrollView构造函数让我们提供的Sliver是一个Widget,但如果我们提供一个普通的Widget,是会崩溃的。
在之前的学习中,我们知道ListView的Sliver是SliverFixedExtentList、SliverPrototypeExtentList、SliverList,GridView的Sliver是SliverGrid。

综上,CustomScrollView 的主要功能是提供一个公共的的 Scrollable 和 Viewport,来组合多个 Sliver,CustomScrollView 的结构如图

加粗是必加参数(只列举常用属性)

CustomScrollView参数类型说明
physicsScrollPhysics滑动类型:
BouncingScrollPhysics() 拉到最底部有回弹效
ClampingScrollPhysics() 包裹内容不会回弹
NeverScrollableScrollPhysics() 滑动禁止
primarybool当条目不足时 true可以滚动
cacheExtentint缓存条目(预加载条目)
scrollDirectionAxis滚动方向:
Axis.vertical
Axis. horizontal
sliversList<Widget>子Widget
shrinkWrapbooltrue反向滑动AppBar


注意:

CustomScrollView中不能直接使用ListView或GridView,因为CustomScrollView本身是一个可滑动组件,ListView或GridView也是可滑动组件,可滑动组件里面嵌套可滑动组件会冲突.
 

2. Flutter 中常用的 Sliver

Sliver名称功能对应的可滚动组件
SliverList列表ListView
SliverFixedExtentList高度固定的列表ListView,指定itemExtent时
SliverAnimatedList添加/删除列表项可以执行动画AnimatedList
SliverGrid网格GridView
SliverPrototypeExtentList根据原型生成高度固定的列表ListView,指定prototypeItem 时
SliverFillViewport包含多个子组件,每个都可以填满屏幕PageView

除了和列表对应的 Sliver 之外还有一些用于对 Sliver 进行布局、装饰的组件,它们的子组件必须是 Sliver,我们列举几个常用的:

Sliver名称对应 RenderBox
SliverPaddingPadding
SliverVisibility、SliverOpacityVisibility、Opacity
SliverFadeTransitionFadeTransition
SliverLayoutBuilderLayoutBuilder

还有一些其它常用的 Sliver:

Sliver名称说明
SliverAppBar对应 AppBar,主要是为了在 CustomScrollView 中使用。
SliverToBoxAdapter一个适配器,可以将 RenderBox 适配为 Sliver,后面介绍。
SliverPersistentHeader滑动到顶部时可以固定住,后面介绍。

Sliver系列 Widget 比较多,我们不会一一介绍,读者只需记住它的特点,需要时再去查看文档即可。上面之所以说“大多数”Sliver都和可滚动组件对应,是由于还有一些如SliverPadding、SliverAppBar 等是和可滚动组件无关的,它们主要是为了结合CustomScrollView一起使用,这是因为CustomScrollView的子组件必须都是Sliver。

示例

  1. class MyApp extends StatelessWidget {
  2. @override
  3. Widget build(BuildContext context) {
  4. // 因为本路由没有使用 Scaffold,为了让子级Widget(如Text)使用
  5. // Material Design 默认的样式风格,我们使用 Material 作为本路由的根。
  6. return MaterialApp(
  7. home: Material(
  8. child: CustomScrollView(
  9. slivers: [
  10. // AppBar 包含一个导航栏
  11. SliverAppBar(
  12. pinned: true, // 滑动到顶端时会固定住
  13. expandedHeight: 250, // 扩展高度
  14. flexibleSpace: FlexibleSpaceBar(
  15. title: Text("Demo"),
  16. background: Image.asset("assets/images/fengjing4.png",
  17. fit: BoxFit.cover),
  18. ),
  19. ),
  20. SliverPadding(
  21. // 间距
  22. padding: EdgeInsets.symmetric(vertical: 8),
  23. sliver: SliverGrid(
  24. delegate: SliverChildBuilderDelegate(
  25. (BuildContext ctx, int index) {
  26. return Container(
  27. color: Colors.cyan[100 * (index % 9)],
  28. child: Text("Grid Item $index"),
  29. alignment: Alignment(0, 0),
  30. );
  31. },
  32. childCount: 20,
  33. ),
  34. gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
  35. crossAxisCount: 2,
  36. childAspectRatio: 1.5,
  37. mainAxisSpacing: 10,
  38. crossAxisSpacing: 10,
  39. ),
  40. ),
  41. ),
  42. SliverFixedExtentList(
  43. delegate: SliverChildBuilderDelegate(
  44. (BuildContext ctx, int index) {
  45. return Container(
  46. color: Colors.blue[100 * (index % 9)],
  47. child: Text("List Item $index"),
  48. alignment: Alignment(0, 0),
  49. );
  50. },
  51. childCount: 20,
  52. ),
  53. itemExtent: 56,
  54. ),
  55. ],
  56. ),
  57. ),
  58. );
  59. }
  60. }

代码分为三部分:

头部SliverAppBar:SliverAppBar对应AppBar,两者不同之处在于SliverAppBar可以集成到CustomScrollView。SliverAppBar可以结合FlexibleSpaceBar实现Material Design中头部伸缩的模型。
中间的SliverGrid:它用SliverPadding包裹以给SliverGrid添加补白。SliverGrid是一个两列,宽高比为1.5的网格,它有20个子组件。
底部SliverFixedExtentList:它是一个所有子元素高度都为56像素的列表。

运行效果如下

3. SliverToBoxAdapter

在实际布局中,我们通常需要往 CustomScrollView 中添加一些自定义的组件,而这些组件并非都有 Sliver 版本,为此 Flutter 提供了一个 SliverToBoxAdapter 组件,它是一个适配器:可以将 RenderBox 适配为 Sliver。比如我们想在列表顶部添加一个可以横向滑动的 PageView,可以使用 SliverToBoxAdapter 来配置:

  1. class SliverToBoxAdapterDemo extends StatelessWidget {
  2. @override
  3. Widget build(BuildContext context) {
  4. return MaterialApp(
  5. home: Material(
  6. child: CustomScrollView(
  7. slivers: [
  8. // 水平方向pageView
  9. SliverToBoxAdapter(
  10. child: SizedBox(
  11. height: 300,
  12. child: PageView(
  13. children: [
  14. Container(
  15. child: Center(child: Text("1")), color: Colors.red),
  16. Container(
  17. child: Center(child: Text("2")), color: Colors.green),
  18. ],
  19. ),
  20. ),
  21. ),
  22. // 垂直方向ListView
  23. SliverFixedExtentList(
  24. delegate: SliverChildListDelegate(
  25. List.generate(20, (index) {
  26. return Container(
  27. color: Colors.yellow[100 * (index % 9)],
  28. child: Text("$index"),
  29. );
  30. }),
  31. ),
  32. itemExtent: 50,
  33. ),
  34. ],
  35. ),
  36. ),
  37. );
  38. }
  39. }

效果如下:

注意,上面的代码是可以正常运行的,但是如果将 PageView 换成一个滑动方向和 CustomScrollView 一致的 ListView 则不会正常工作!原因是:CustomScrollView 组合 Sliver 的原理是为所有子 Sliver 提供一个共享的 Scrollable,然后统一处理指定滑动方向的滑动事件,如果 Sliver 中引入了其它的 Scrollable,则滑动事件便会冲突。上例中 PageView 之所以能正常工作,是因为 PageView 的 Scrollable 只处理水平方向的滑动,而 CustomScrollView 是处理垂直方向的,两者并未冲突,所以不会有问题,但是换一个也是垂直方向的 ListView 时则不能正常工作,最终的效果是,在ListView内滑动时只会对ListView 起作用,原因是滑动事件被 ListView 的 Scrollable 优先消费,CustomScrollView 的 Scrollable 便接收不到滑动事件了。

Flutter 中手势的冲突时,默认的策略是子元素生效

如果 CustomScrollView 有孩子也是一个完整的可滚动组件且它们的滑动方向一致,则 CustomScrollView 不能正常工作,要解决这个问题,可以使用 NestedScrollView

4. SliverPersistentHeader

SliverPersistentHeader 的功能是当滑动到 CustomScrollView 的顶部时,可以将组件固定在顶部。

需要注意, Flutter 中设计 SliverPersistentHeader 组件的初衷是为了实现 SliverAppBar,所以它的一些属性和回调在SliverAppBar 中才会用到。

SliverPersistentHeader 的定义

  1. const SliverPersistentHeader({
  2. Key? key,
  3. // 构造 header 组件的委托
  4. required SliverPersistentHeaderDelegate delegate,
  5. this.pinned = false, // header 滑动到可视区域顶部时是否固定在顶部
  6. this.floating = false, // 正文部分介绍
  7. })
  • floating 的做用是:pinned 为 false 时 ,则 header 可以滑出可视区域(CustomScrollView 的 Viewport)(不会固定到顶部),当用户再次向下滑动时,此时不管 header 已经被滑出了多远,它都会立即出现在可视区域顶部并固定住,直到继续下滑到 header 在列表中原来的位置时,header 才会重新回到原来的位置(不再固定在顶部)。 具体效果,我们后面会有示例

  • delegate是用于生成 header 的委托,类型为 SliverPersistentHeaderDelegate,它是一个抽象类,需要我们自己实现,定义如下:

  1. abstract class SliverPersistentHeaderDelegate {
  2. // header 最大高度;pined为 true 时,当 header 刚刚固定到顶部时高度为最大高度。
  3. double get maxExtent;
  4. // header 的最小高度;pined为true时,当header固定到顶部,用户继续往上滑动时,header
  5. // 的高度会随着用户继续上滑从 maxExtent 逐渐减小到 minExtent
  6. double get minExtent;
  7. // 构建 header。
  8. // shrinkOffset取值范围[0,maxExtent],当header刚刚到达顶部时,shrinkOffset 值为0,
  9. // 如果用户继续向上滑动列表,shrinkOffset的值会随着用户滑动的偏移增大,直到maxExtent。
  10. //
  11. // overlapsContent:一般不建议使用,在使用时一定要小心,后面会解释。
  12. Widget build(BuildContext context, double shrinkOffset, bool overlapsContent);
  13. // header 是否需要重新构建;通常当父级的 StatefulWidget 更新状态时会触发。
  14. // 一般来说只有当 Delegate 的配置发生变化时,应该返回false,比如新旧的 minExtent、maxExtent
  15. // 等其它配置不同时需要返回 true,其余情况返回 false 即可。
  16. bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate);
  17. // 下面这几个属性是SliverPersistentHeader在SliverAppBar中时实现floating、snap
  18. // 效果时会用到,平时开发过程很少使用到,读者可以先不用理会。
  19. TickerProvider? get vsync => null;
  20. FloatingHeaderSnapConfiguration? get snapConfiguration => null;
  21. OverScrollHeaderStretchConfiguration? get stretchConfiguration => null;
  22. PersistentHeaderShowOnScreenConfiguration? get showOnScreenConfiguration => null;
  23. }

可以看到,我们最需要关注的就是maxExtentminExtentpined为true 时,当 header 刚刚固定到顶部,此时会对它应用 maxExtent (最大高度);当用户继续往上滑动时,header 的高度会随着用户继续上滑从 maxExtent 逐渐减小到 minExtent。如果我们想让 header 高度固定,则将 maxExtentminExtent 指定为同样的值即可

为了构建 header 我们必须要定义一个类,让它继承自 SliverPersistentHeaderDelegate,这无疑会增加使用成本!为此,我们封装一个通用的委托构造器 SliverHeaderDelegate,通过它可以快速构建 SliverPersistentHeaderDelegate,实现如下:

  1. typedef MSCustomHeaderBuilder = Widget Function(
  2. BuildContext context, double shrinkOffset, bool overlapsContent);
  3. class MSCustomHeaderDelegate extends SliverPersistentHeaderDelegate {
  4. // MSCustomHeaderDelegate 构造函数
  5. MSCustomHeaderDelegate(
  6. {required this.maxHeight, required this.minHeight, required Widget child})
  7. : builder = ((ctx, shrinkOffset, overlapsContent) => child),
  8. assert(minHeight <= maxHeight && minHeight >= 0);
  9. // MSCustomHeaderDelegate fixHeight 高度相同 构造函数
  10. MSCustomHeaderDelegate.fixHeight(
  11. {required double height, required Widget child})
  12. : builder = ((context, shrinkOffset, overlapsContent) => child),
  13. maxHeight = height,
  14. minHeight = height,
  15. assert(height >= 0);
  16. // MSCustomHeaderDelegate builder 需要传入builder函数
  17. MSCustomHeaderDelegate.builder(
  18. {required this.maxHeight, required this.minHeight, required this.builder})
  19. : assert(minHeight <= maxHeight && minHeight >= 0);
  20. final double maxHeight;
  21. final double minHeight;
  22. final MSCustomHeaderBuilder builder;
  23. @override
  24. Widget build(
  25. BuildContext context, double shrinkOffset, bool overlapsContent) {
  26. Widget child = builder(context, shrinkOffset, overlapsContent);
  27. assert(() {
  28. if (child.key != null) {
  29. print('${child.key}: shrink: $shrinkOffset,overlaps:$overlapsContent');
  30. }
  31. return true;
  32. }());
  33. // 让 header 尽可能充满限制的空间;宽度为 Viewport 宽度,
  34. // 高度随着用户滑动在[minHeight,maxHeight]之间变化。
  35. return SizedBox.expand(child: child);
  36. }
  37. @override
  38. double get maxExtent => maxHeight;
  39. @override
  40. double get minExtent => minHeight;
  41. @override
  42. bool shouldRebuild(covariant MSCustomHeaderDelegate oldDelegate) {
  43. return oldDelegate.maxExtent != maxExtent ||
  44. oldDelegate.minExtent != minExtent;
  45. }
  46. }

示例1

  1. class MSPersistentHeaderRoute extends StatelessWidget {
  2. const MSPersistentHeaderRoute({Key? key}) : super(key: key);
  3. @override
  4. Widget build(BuildContext context) {
  5. return CustomScrollView(
  6. slivers: [
  7. SliverPersistentHeader(
  8. pinned: true, // 是否固定header
  9. delegate: MSCustomHeaderDelegate(
  10. maxHeight: 80, minHeight: 50, child: buildHeader(0)),
  11. ),
  12. buildSliverList(10),
  13. SliverPersistentHeader(
  14. pinned: true, // 是否固定header
  15. delegate: MSCustomHeaderDelegate.builder(
  16. maxHeight: 80,
  17. minHeight: 50,
  18. builder: (BuildContext context, double shrinkOffset,
  19. bool overlapsContent) {
  20. return buildHeader(1);
  21. },
  22. ),
  23. ),
  24. buildSliverList(10),
  25. SliverPersistentHeader(
  26. pinned: false, // 是否固定header
  27. delegate: MSCustomHeaderDelegate.fixHeight(
  28. height: 50, child: buildHeader(2)),
  29. ),
  30. buildSliverList(20),
  31. ],
  32. );
  33. }
  34. Widget buildSliverList([int count = 5]) {
  35. return SliverFixedExtentList(
  36. delegate: SliverChildBuilderDelegate((ctx, index) {
  37. return ListTile(title: Text("$index"));
  38. }, childCount: count),
  39. itemExtent: 50,
  40. );
  41. }
  42. Widget buildHeader(int tag) {
  43. return Container(
  44. key: ValueKey<String>("$tag"),
  45. child: Text("PersistentHeader $tag"),
  46. color: Colors.blue[200],
  47. alignment: Alignment.centerLeft,
  48. );
  49. }
  50. }

运行效果如下:

注意
我们说过 SliverPersistentHeader 的 builder 参数 overlapsContent 一般不建议使用,使用时要当心。因为按照 overlapsContent 变量名的字面意思,只要有内容和 Sliver 重叠时就应该为 true,但是如果我们在上面示例的 builder 中打印一下 overlapsContent 的值就会发现第一个 PersistentHeader 0 的 overlapsContent 值一直都是 false,而 PersistentHeader 1 则是正常的,如果我们再添加几个 SliverPersistentHeader ,发现新添加的也都正常。总结一下:当有多个 SliverPersistentHeader时,需要注意第一个 SliverPersistentHeader 的 overlapsContent 值会一直为 false

示例2

  1. class MSPersistentHeaderRoute extends StatelessWidget {
  2. const MSPersistentHeaderRoute({Key? key}) : super(key: key);
  3. @override
  4. Widget build(BuildContext context) {
  5. return CustomScrollView(
  6. slivers: [
  7. SliverPersistentHeader(
  8. pinned: false, // 是否固定header
  9. floating: true,
  10. delegate: MSCustomHeaderDelegate.fixHeight(
  11. height: 50, child: buildHeader(2)),
  12. ),
  13. buildSliverList(100),
  14. ],
  15. );
  16. }
  17. Widget buildSliverList([int count = 5]) {
  18. return SliverFixedExtentList(
  19. delegate: SliverChildBuilderDelegate((ctx, index) {
  20. return ListTile(title: Text("$index"));
  21. }, childCount: count),
  22. itemExtent: 50,
  23. );
  24. }
  25. Widget buildHeader(int tag) {
  26. return Container(
  27. key: ValueKey<String>("$tag"),
  28. child: Text("PersistentHeader $tag"),
  29. color: Colors.blue[200],
  30. alignment: Alignment.centerLeft,
  31. );
  32. }
  33. }

总结

  • CustomScrollView 组合 Sliver 的原理是为所有子 Sliver 提供一个共享的 Scrollable,然后统一处理指定滑动方向的滑动事件。
  • CustomScrollView 和 ListView、GridView、PageView 一样,都是完整的可滚动组件(同时拥有 Scrollable、Viewport、Sliver)。
  • CustomScrollView 只能组合 Sliver,如果有孩子也是一个完整的可滚动组件(通过 SliverToBoxAdapter 嵌入)且它们的滑动方向一致时便不能正常工作。

https://book.flutterchina.club/chapter6/custom_scrollview.html

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/繁依Fanyi0/article/detail/463148
推荐阅读
相关标签
  

闽ICP备14008679号