Flutter 滚动布局:sliver模型

news2024/9/22 21:23:41

一、滚动布局

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分为以下三类:

 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的区域就会被预先加载。

以上图为例,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;
}

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的高度。紫色部分是下方的红色与上方的蓝色重叠的区域

visible

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

效果:

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

 

scrollExtent

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

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

示例 3:

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

Sliver 布局过程

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

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

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

Slivers

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

SliverAppBar

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

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

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 可以轻松的实现沉浸式的效果:

它的实现原理就是根据 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 布局)

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

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

相关文章

说说你对选择排序的理解?如何实现?应用场景?

一、是什么 选择排序&#xff08;Selection sort&#xff09;是一种简单直观的排序算法&#xff0c;无论什么数据进去都是 O(n) 的时间复杂度&#xff0c;所以用到它的时候&#xff0c;数据规模越小越好 其基本思想是&#xff1a;首先在未排序的数列中找到最小(or最大)元素&a…

工业计算机应用——AGV自动导引车行业

工业计算机在AGV行业的应用 自动导引车(AGV)是现代物流系统中的重要组成部分,能够在无人操作的情况下自动完成货物的搬运和运输。随着工业自动化的发展,工业计算机在AGV行业的应用越来越广泛,为AGV系统的智能化和高效化提供了有力支持。 一、工业计算机在AGV行业的应用场…

ICC2:如何优化网表中的assign语法

更多学习内容请关注「拾陆楼」知识星球 拾陆楼知识星球入口 问题来自星球提问: ICC2中有两种解决方法: 1) set_app_options -name opt.port.eliminate_verilog_assign -value true 工具优化时自己插buffer解决 2) change_name -hier -rule verilog 需要注意的是: 第一个opti…

Git学习笔记(第6章):GitHub操作(远程库操作)

目录 6.1 远程库操作 6.1.1 创建远程库 6.1.2 命名远程库 6.1.3 本地库推送到远程库(push) 6.1.4 远程库拉取到本地库(pull) 6.1.5 远程库克隆到本地库(clone) 6.2 团队内协作 6.3 跨团队协作 6.4 SSH免密登录 6.1 远程库操作 命令 作用 git remote -v 查看所有远程…

《吐血整理》进阶系列教程-拿捏Fiddler抓包教程(16)-Fiddler如何充当第三者,再识AutoResponder标签-上篇

1.简介 Fiddler充当第三者&#xff0c;主要是通过AutoResponder标签在客户端和服务端之间&#xff0c;Fiddler抓包&#xff0c;然后改包&#xff0c;最后发送。AutoResponder这个功能可以算的上是Fiddler最实用的功能&#xff0c;可以让我们修改服务器端返回的数据&#xff0c…

MySQL-SQL-DCL

DCL-介绍 DCL-管理用户 1、查询用户 2、创建用户 3、修改用户密码 4、删除用户 注意&#xff1a; DCL-权限控制 1、查询权限 2、授予权限 授予全部权限 3、撤销权限 注意&#xff1a;

vue3 常见的路由传参无刷新修改当前路由url带参

无刷新修改当前路由url带参 //tabs切换部分 <el-tabs v-model"activeName" class"demo-tabs" tab-click"handleClick"><el-tab-pane v-for"(item,index) in tagList" :label"item.title" :name"item.name…

基于springboot酒店预订系统

开发工具&#xff1a;IDEA 服务器&#xff1a;Tomcat9.0&#xff0c; jdk1.8 项目构建&#xff1a;maven 数据库&#xff1a;mysql5.7 前端技术&#xff1a;AdminLTEjQueryvue.jselementuijsp 服务端技术&#xff1a;springbootmybatis 本系统功能包括&#xff1a; 一、…

展锐T618_虎贲T618紫光展锐安卓核心板规格参数

基于紫光展锐八核T618平台的纯国产化方案&#xff0c;采用了开放的智能Android操作系统&#xff0c;并集成了4G网络、2.5G5G双频WIFI(可支持1*1 MIMO)、BLUETOOTH近距离无线传输技术以及GNSS无线定位技术。用户可以根据特定场合的需求&#xff0c;选择合适的嵌入式ARM核心模块&…

司铭宇老师:二手房营销培训课程:二手房销售技巧和话术

二手房营销培训课程&#xff1a;二手房销售技巧和话术 在房地产市场中&#xff0c;二手房销售一直是一个挑战性极强的领域。由于面对的客户群体多样&#xff0c;房源条件各异&#xff0c;销售人员必须具备高超的销售技巧和精湛的话术能力。本文将通过实战案例分析&#xff0c;深…

手握中下牌型如何赢掼蛋?

中下牌型的特点&#xff1a;牌不是很好&#xff0c;炸弹少&#xff0c;但是牌型种类较多&#xff0c;并且有上手牌&#xff0c;适合配合对方&#xff0c;发动进攻。手握此类牌型&#xff0c;可采取协同进攻、全面防御、顺势突围三种方式来扭转双下局面。 一、协同进攻 此时应当…

Spark UI中 Shuffle Exchange 和 BroadcastExchange 中的 dataSize 值为什么不一样

背景 Spark 3.5 最近在看Spark UI 上的一些指标看到一个很有意思的东西, 相邻的Shuffle Exechange 和 BroadcastExechange 中的 datasize 居然不一样&#xff0c; 前者为 765KB, 后者为 64.5MB。差别还不少&#xff0c;中间就增加了一个 AQEShuffleRead 计划 结论 Shuffle E…

2023年春秋杯网络安全联赛冬季赛 Writeup

文章目录 Webezezez_phppicup Misc谁偷吃了外卖modules明文混淆 Pwnnmanagerbook Reupx2023 CryptoCF is Crypto Faker 挑战题勒索流量Ezdede 可信计算 Web ezezez_php 反序列化打redis主从复制RCE&#xff1a;https://www.cnblogs.com/xiaozi/p/13089906.html <?php c…

C++入门学习(十一)字符型

C中的字符型可以表示ASCII码中的所有字符&#xff0c;包括字母、数字、标点符号等。 ASCII码是一种用于编码字符的编码系统&#xff0c;它使用不同的数值来表示不同的字符。ASCII码使用7位或8位二进制数来表示每个字符&#xff0c;因此可以表示128或256个不同的字符。 在ASCI…

ubuntu使用docker compose一键部署项目

1、将前面手动部署的容器和镜像全部删除 docker rmi hmall (hmall镜像名) docker rmi image_id rmi 是删除多个 rm是删除一个 2、执行命令 docker compose up -d http://192.168.79.129:18080/search.html 访问安装成功&#xff01; 该ip是虚拟机ubuntu的ip 3、docker-compos…

【AI的未来 - AI Agent系列】【MetaGPT】6. 用ActionNode重写技术文档助手

文章目录 0. 前置推荐阅读1. 重写WriteDirectory Action1.1 实现WriteDirectory的ActionNode&#xff1a;DIRECTORY_WRITE1.2 将 DIRECTORY_WRITE 包进 WriteDirectory中 2. 重写WriteContent Action2.1 思考重写方案2.2 实现WriteContent的ActionNode2.3 改写WriteContent Act…

【Kafka】开发实战和Springboot集成kafka

目录 消息的发送与接收生产者消费者 SpringBoot 集成kafka服务端参数配置 消息的发送与接收 生产者 生产者主要的对象有&#xff1a; KafkaProducer &#xff0c; ProducerRecord 。 其中 KafkaProducer 是用于发送消息的类&#xff0c; ProducerRecord 类用于封装Kafka的消息…

机器学习_正则化、欠拟合和过拟合

文章目录 正则化欠拟合和过拟合正则化参数 正则化 机器学习中的正则化是在损失函数里面加惩罚项&#xff0c;增加建模的模糊性&#xff0c;从而把捕捉到的趋势从局部细微趋势&#xff0c;调整到整体大概趋势。虽然一定程度上地放宽了建模要求&#xff0c;但是能有效防止过拟合…

【GitHub项目推荐--一个语音机器人项目】【转载】

推荐一个腾讯大佬开源的语音对话机器人&#xff1a;wukong-robot &#xff0c;悟空机器人在 GitHub 上斩获 3.2K 的 Star。 这是一个简单灵活的中文语音对话机器人项目&#xff0c;目的是让中国的开发者也能快速打造个性化的智能音箱&#xff0c;同时&#xff0c;该项目还是第…

科技云报道:金融大模型落地,还需跨越几重山?

科技云报道原创。 时至今日&#xff0c;大模型的狂欢盛宴仍在持续&#xff0c;而金融行业得益于数据密集且有强劲的数字化基础&#xff0c;从一众场景中脱颖而出。 越来越多的公司开始布局金融行业大模型&#xff0c;无论是乐信、奇富科技、度小满、蚂蚁这样的金融科技公司&a…