Flutter 滚动布局:sliver模型

这篇具有很好参考价值的文章主要介绍了Flutter 滚动布局:sliver模型。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

一、滚动布局

Flutter中可滚动布局基本都来自Sliver模型,原理和安卓传统UI的ListView、RecyclerView类似,滚动布局里面的每个子组件的样式往往是相同的,由于组件占用内存较大,所以在内存上我们可以缓存有限个组件,滚动布局时仅仅刷新组件的数据,来达到滚动布局存放无限个子组件的目标。另一方面,内容被渲染到了lazy widget里,也就是SliverList and SliverGrid (以及它们的变种 SliverFixedExtentList 和 SliverAnimatedGrid)。这些组件确保只有列表中可见部分会被布局和渲染。ListView 和 GridView只是CustomScrollView 和 SliverList 或 SliverGrid组合在一起,方便我们使用的一个封装组件而已。它们允许我们直接使用box widget。而CustomScrollView的slivers属性明确要求我们使用sliver widget。但是sliver widget也只是box widget的包裹,后者才是真正用于渲染的。按照列表内容的差异,我们可以将scrolling widget分为以下三类:

Flutter 滚动布局:sliver模型,flutter

 sliver widget是可滚动列表中的一部分,它是面向viewport进行布局的。

二、ViewPort

  • ViewPort 是一个显示窗口,它内部可包含多个 Sliver;
  • ViewPort 的宽高是确定的,它内部 Slivers 的宽高之和是可以大于自身的宽高的;
  • ViewPort 为了提高性能采用懒加载机制,它只会绘制可视区域内容 Widget。

ViewPort 有一些重要属性:

class Viewport extends MultiChildRenderObjectWidget {
  /// 主轴方向
  final AxisDirection axisDirection;
  /// 纵轴方向
  final AxisDirection crossAxisDirection;
  /// center 决定 viewport 的 zero 基准线,也就是 viewport 从哪个地方开始绘制,默认是第一个 sliver
  /// center 必须是 viewport slivers 中的一员的 key
  final Key center;
  
/// 锚点,取值[0,1],和 zero 的相对位置,比如 0.5 代表 zero 被放到了 Viewport.height / 2 处
  final double anchor;
  /// 滚动的累计值,确切的说是 viewport 从什么地方开始显示
  final ViewportOffset offset;
  /// 缓存区域,也就是相对有头尾需要预加载的高度
  final double cacheExtent;
  /// children widget
  List<Widget> slivers;
  }

我们看到center是一个key,表示从哪个sliver开始绘制,绘制的起点是一条zero基准线。这条基准线相对于视口的位置叫anchor。视口相对于整个滚动列表的位置叫offset。如果子元素进入了视口上下cacheExtent的区域就会被预先加载。

Flutter 滚动布局:sliver模型,flutter

以上图为例,center=sliver1,anchor=0.2,此时sliver的top=0.2 * viewport.height,所以前面刚好能展示sliver0,也就是浅蓝色区域会滚到整个列表的开始位置。再比如,center=sliver1,anchor=0.4,此时sliver0上面会空出一条sliver大小的空间。

三、SliverConstraints

和 Box 布局使用 BoxConstraints 作为约束类似,Sliver 布局采用 SliverConstraints 作为约束,但相对于 Box 要复杂的多,可以理解为 SliverConstraints 描述了 Viewport 和它内部的 Slivers 之间的布局信息:

class SliverConstraints extends Constraints {
    //主轴方向
    AxisDirection? axisDirection;
    //Sliver 新数据沿主轴的哪个方向插入?枚举类型,正向或反向
    GrowthDirection? growthDirection;
    //用户滑动方向
    ScrollDirection? userScrollDirection;
    //当前Sliver理论上(可能会固定在顶部)已经滑出可视区域的总偏移
    double? scrollOffset;
    //当前Sliver之前的Sliver占据的总高度,因为列表是懒加载,如果不能预估时,该值为double.infinity
    double? precedingScrollExtent;
    //上一个 sliver 覆盖当前 sliver 的大小,通常在 sliver 是 pinned/floating
    //或者处于列表头尾时有效。
    double? overlap;
    //当前Sliver在Viewport中的最大可以绘制的区域。
    //绘制如果超过该区域会比较低效(因为不会显示)
    double? remainingPaintExtent;
    //纵轴的长度;如果列表滚动方向是垂直方向,则表示列表宽度。
    double? crossAxisExtent;
    //纵轴方向
    AxisDirection? crossAxisDirection;
    //Viewport在主轴方向的长度;如果列表滚动方向是垂直方向,则表示列表高度。
    double? viewportMainAxisExtent;
    //Viewport 预渲染区域的起点[-Viewport.cacheExtent, 0]
    double? cacheOrigin;
    //Viewport加载区域的长度,范围:
    //[viewportMainAxisExtent,viewportMainAxisExtent + Viewport.cacheExtent*2]
    double? remainingCacheExtent;
}

Flutter 滚动布局:sliver模型,flutter

cacheOrigin

在Viewport 预渲染区域中的起点,位于[-Viewport.cacheExtent, 0]之间

overlap

上图中的 sliver1 会被 SliverAppBar(pinned = true)遮住,遮住的大小就是 overlap,此时 overlap 会一直大于 0,如果设置像 iOS bouncing 那样的滑动效果,那么当 list 滚动到顶部继续滑动的时候 overlap 会小于 0(此刻并没有东西遮盖 sliver1,而是 sliver1 的 top 和 viewport 的 top 有间距)。

remainingPaintExtent

当前 Sliver 在 Viewport 中的最大可以绘制的区域。当viewport对sliver布局的时候,会将remainingPaintExtent减去这个sliver的paintExtent后,作为传入下一个sliver的remainingPaintExtent。remainingPaintExtent的初始值是viewportMainAxisExtent。

由此,我们可以计算当前 sliver 距离顶部的距离:

//如果大于0,表示当前 sliver 距离顶部的高度为 topOffset。若已经到达顶部或超出顶部,则该值始终等于 0。此时超出的距离可参考 scrollOffset。
topOffset = viewportMainAxisExtent - remainingPaintExtent;

当前sliver无论实际需要绘制多大的区域,最终能绘制到viewport的大小,不会超过remainingPaintExtent:

// paintExtent 通常需要这样处理一下,避免超出 remainingPaintExtent
paintExtent = min(paintExtent, constraints.remainingPaintExtent);
scrollOffset

scrollOffset 属性表示滚动滑出Viewport边界的距离,类似于 web 的 scrollTop 属性。一般来说表示组件的上边界离开 viewport 顶部的长度,未到达顶部之前都是 0。

SliverGeometry

Viewport 通过 SliverConstraints 告知它内部的 sliver 自己的约束信息,比如还有多少空间可用、offset 等,那么Sliver 则通过 SliverGeometry 反馈给 Viewport 需要占用多少空间量。

const SliverGeometry({
  //Sliver在主轴方向预估长度,大多数情况是固定值,用于计算sliverConstraints.scrollOffset
  this.scrollExtent = 0.0, 
  this.paintExtent = 0.0, // 可视区域中的绘制长度
  this.paintOrigin = 0.0, // 绘制的坐标原点,相对于自身布局位置
  //在 Viewport中占用的长度;如果列表滚动方向是垂直方向,则表示列表高度。
  //范围[0,paintExtent]
  double? layoutExtent, 
  this.maxPaintExtent = 0.0,//最大绘制长度
  this.maxScrollObstructionExtent = 0.0,
  double? hitTestExtent, // 点击测试的范围
  bool? visible,// 是否显示
  //是否会溢出Viewport,如果为true,Viewport便会裁剪
  this.hasVisualOverflow = false,
  //scrollExtent的修正值:layoutExtent变化后,为了防止sliver突然跳动(应用新的layoutExtent)
  //可以先进行修正。
  this.scrollOffsetCorrection,
  double? cacheExtent, // 在预渲染区域中占据的长度
}) 
paintOrigin

当该值小于 0 时,当前 sliver 的整体起始位置会向上偏移 paintOrigin.abs() 的长度。如果每次下拉 x 的长度,paintOrigin 也向上移动 x 的距离,则 sliver 相对静止,由此可实现 pinned 效果。

layoutExtent

布局时占用的高度,取值范围 [0, paintExtent]。即 layoutExtent 须小于等于 paintExtent (当前绘制的高度,一般是等于)。
当 layoutExtent 小于 paintExtent 时,则一部分高度会被下一个 sliver 顶上。

示例 2:

说明:蓝色块高度是 100,但是占据高度只有 30,导致红色块向上顶了 70的高度。紫色部分是下方的红色与上方的蓝色重叠的区域

Flutter 滚动布局:sliver模型,flutter

visible

当 visible 为 false 时会影响子节点的显示。在示例中,只占据空间(占据高度 layoutExtent),而不显示界面。即不影响布局。

效果:

说明:中间 30 的高度为原蓝色块占据的空间

 Flutter 滚动布局:sliver模型,flutter

scrollExtent

可滚动的范围。一般来说,对于 ListView,在 sliver 上边界滚动到顶部之前 paintExtent 等于 layoutExtent 都等于 scrollExtent,到达顶部后慢慢变小,直到变为 0。而 scrollExtent 一直不变。

注:如果 layoutExtent 不慢慢变小,即保持不变并且大于 0,则在当前 sliver 滚动到顶部后还可以继续滚动 scrollExtent 的长度(除非 scrollExtent 也等于 0),然后再执行下一个 sliver 的滚动。

示例 3:

说明:由于 layoutExtent 与 scrollExtent 都一直不变,并且不等于 0。蓝色 sliver 向上滚动到 Viewport 顶部后,还可以继续滚动 100 的高度,当这 100 也滚完了,下一个 sliver 才开始滚动。

Flutter 滚动布局:sliver模型,flutter

Sliver 布局过程

RenderViewport 在 layout 它内部的 slivers 的过程如下:

Flutter 滚动布局:sliver模型,flutter

这个 layout 过程是一个自上而下的线性过程:

  • 给 sliver1 输入 SliverConstrains1 并且得到输出结果(SliverGeometry1) ,
  • 根据 SliverGeometry1 重新生成一个新的 SliverConstrains2 输入给 sliver2 得到 SliverGeometry2
  • 直至最后一个 sliver 具体的过程可以查看 RenderViewport 的 layoutChildSequence 方法。

Slivers

Flutter 提供了很多的 Sliver 组件,下面我们主要说一下它们的作用是什么:

SliverAppBar

类似于 android 中 CollapsingToolbarLayout,可以根据滑动做伸缩布局,并提供了 actions,bottom 等提高效率的属性。

Flutter 滚动布局:sliver模型,flutter

SliverList / SliverGrid

用法和 ListView / GridView 基本一致。 此外,ListView = SliverList + Scrollable,也就是说 SliverList 不具备处理滑动事件的能力,所以它必须配合 CustomScrollView 来使用。

SliverFixedExtentList

它比 SliverList 多了修饰词 FixedExtent,意思是它的 item 在主轴方向上具有固定的高度/宽度。

设计它的原因是在 item 高度/宽度全都一样的场景下使用,它的效率比 SliverList 高,因为它不用通过 item 的 layout 过程就可以知道每个 item 的范围。

在使用的时候必须传入 itemExtent:

SliverFixedExtentList(
  itemExtent: 50.0,
  delegate: SliverChildBuilderDelegate(
	...
        );
    },
  ),
)
SliverPersistentHeader

Flutter 滚动布局:sliver模型,flutter

SliverPersistentHeader 是一个可以固定/悬浮的 header,它可以设置在列表的任意位置,显示的内容需要设置 SliverPersistentHeaderDelegate。

SliverPersistentHeader(
          pinned: true,
          delegate: ...,
)

 SliverPersistentHeaderDelegate 是一个抽象类,我们需要自己实现它,它的实现很简单,只有四个必须要实现的成员:

class CustomDelegate extends SliverPersistentHeaderDelegate {
  /// 最大高度
  @override
  double get maxExtent => 100;
  /// 最小高度
  @override
  double get minExtent => 50;

  /// shrinkOffset: 当前 sliver 顶部越过屏幕顶部的距离
  /// overlapsContent: 下方是否还有 content 显示
  @override
  Widget build(
      BuildContext context, double shrinkOffset, bool overlapsContent) {
        return your widget
    );
  }
  /// 是否需要刷新
  @override
  bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) {
    return maxExtent != oldDelegate.maxExtent ||
        minExtent != oldDelegate.minExtent;
  }
}

在实际运用中沉浸式的设计是很常见的,使用 SliverPersistentHeaderDelegate 可以轻松的实现沉浸式的效果:

Flutter 滚动布局:sliver模型,flutter

它的实现原理就是根据 shrinkOffset 动态调整状态栏的样式和标题栏的颜色,实现代码见下面的 沉浸式 Header。

SliverToBoxAdapter

将 BoxWidget 转变为 Sliver:由于 CustomScrollView 只能接受 Sliver 类型的 child,所以很多常用的 Widget 无法直接添加到 CustomScrollView 中,此时只需要将 Widget 用 SliverToBoxAdapter 包裹一下就可以了。 最常见的使用就是 SliverList 不支持横向模式,但是又无法直接将 ListView 直接添加到 CustomScrollView 中,此时用 SliverToBoxAdapter 包裹一下:

CustomScrollView(
      slivers: <Widget>[
        SliverToBoxAdapter(
          child: _buildHorizonScrollView(),
        ),
      ],
    ));

 Widget _buildHorizonScrollView() {
    return Container(
      height: 50,
      child: ListView.builder(
          scrollDirection: Axis.horizontal,
          primary: false,
          shrinkWrap: true,
          itemCount: 15,
          itemBuilder: (context, index) {
            return Container(
              color: ColorUtils.randomColor(),
              width: 50,
              height: 50,
            );
          }),
    );
  } 
SliverPadding

可以用在 CustomScrollView 中的 Padding。 需要注意的是不要用它来包裹 SliverPersistentHeader ,因为它会使 SliverPersistentHeader 的 pinned 失效,如果 SliverPersistentHeader 非要使用 Padding 效果,可以在 delegate 内部使用 Padding。

  • wrong code:
SliverPadding(
            padding: EdgeInsets.symmetric(horizontal: 16),
            sliver: SliverPersistentHeader(
              pinned: true,
              floating: false,
              delegate: Delegate(),
            ),
          )
  • correct code:
class Delegate extends SliverPersistentHeaderDelegate {
  @override
  Widget build(
          BuildContext context, double shrinkOffset, bool overlapsContent) =>
      Padding(
        padding: EdgeInsets.symmetric(horizontal: 16),
        child: Container(
          color: Colors.yellow,
        ),
      );
  ...
}
SliverSafeArea

用法和 SafeArea 一致。

SliverFillRemaining

可以填充屏幕剩余控件的 Sliver。

参考文献:

Flutter - 循序渐进 Sliver - 掘金
flutter —— 布局原理与约束 (Sliver 布局)文章来源地址https://www.toymoban.com/news/detail-817350.html

到了这里,关于Flutter 滚动布局:sliver模型的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Flutter 笔记 | Flutter 布局组件

    布局类组件都会包含一个或多个子组件,布局类组件都是直接或间接继承 SingleChildRenderObjectWidget 和 MultiChildRenderObjectWidget 的Widget,它们一般都会有一个 child 或 children 属性用于接收子 Widget。 不同的布局类组件对子组件排列(layout)方式不同,如下表所示: Widget 说明 用途

    2024年02月06日
    浏览(53)
  • Flutter开发笔记:Flutter 布局相关组件

    Flutter开发笔记 Flutter 布局与布局组件 - 文章信息 - Author: Jack Lee (jcLee95) Visit me at: https://jclee95.blog.csdn.net Email: 291148484@163.com. Shenzhen China Address of this article: https://blog.csdn.net/qq_28550263/article/details/131419782 【介绍】:本文介绍Flutter 布局相关组件。 Flutter 中提供了丰富的原生布局组

    2024年02月11日
    浏览(56)
  • Flutter 笔记 | Flutter 核心原理(三)布局(Layout )过程

    Layout(布局)过程主要是确定每一个组件的布局信息(大小和位置),Flutter 的布局过程如下: 父节点向子节点传递约束(constraints)信息,限制子节点的最大和最小宽高。 子节点根据约束信息确定自己的大小(size)。 父节点根据特定布局规则(不同布局组件会有不同的布

    2024年02月09日
    浏览(89)
  • Flutter(六)可滚动组件

    Flutter 中的可滚动主要由三个角色组成:Scrollable、Viewport 和 Sliver: Scrollable :用于处理滑动手势,根据滑动偏移构建 Viewport 。 Viewport:显示的视窗,即列表的可视区域; Sliver:视窗里显示的元素。 具体布局过程: Scrollable 监听到滑动后,根据滑动偏移构建 Viewport ,Viewpor

    2024年02月07日
    浏览(34)
  • 滚动菜单 flutter

    想实现这个功能: 下面的代码可以实现:

    2024年02月09日
    浏览(38)
  • Flutter笔记:滚动之-无限滚动与动态加载的实现

    Flutter笔记 无限滚动与动态加载的实现 作者 : 李俊才 (jcLee95):https://blog.csdn.net/qq_28550263 邮箱 : 291148484@163.com 本文地址 :https://blog.csdn.net/qq_28550263/article/details/133342307 本文还有另外一个版本,基于GetX简单状态管理状态。地址为:https://jclee95.blog.csdn.net/article/details/13336

    2024年02月07日
    浏览(44)
  • Flutter基础布局

     Column相当于Android原生的LinearLayout线性布局。 主要代码: mainAxisAlignment和crossAxisAlignment属性用于控制排列位置与控件之间距离。 完整代码:  主要代码: Row和Column相对应,Row横向依次排列容器内的内容,Column纵向依次排列容器内的内容。属性介绍参考Column。 完整代码: E

    2024年02月13日
    浏览(85)
  • Flutter学习之旅 -网格布局

    GridView列表三种形式 可以通过 GridView.count 实现网格布局 可以通过 GridView.extent 实现网格布局 可以通过 GridView.builder 实现网格布局 参数:可以通过 SliveGridDelegateWithFiexdCrossAxisCount 来设置 GridView.count 参数: 可以通过 SliveGridDelegateWithMaxCrossAxisExtent 来设置 GridView.extent 常用属性 名称

    2024年02月03日
    浏览(55)
  • flutter 常用布局方式

    单子 Widget是指只有一个子组件的Widget,这样布局Widget有三个:Container、Padding、Center Container,是一种允许在其内部添加其他控件的控件,也是 UI 框架中的一个常见概念。 在 Flutter 中,Container 本身可以单独作为控件存在(比如单独设置背景色、宽高),也可以作为其他控件的

    2024年02月08日
    浏览(40)
  • Flutter 布局详解

    本文主要介绍了Flutter布局相关的内容,对相关知识点进行了梳理,并从实际例子触发,进一步讲解该如何去进行布局。 Flutter 布局详解 Flutter 布局(一)- Container详解 Flutter 布局(二)- Padding、Align、Center详解 Flutter 布局(三)- FittedBox、AspectRatio、ConstrainedBox详解 Flutter 布局

    2024年02月04日
    浏览(56)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包