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

 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 中的一员的 keyfinal Key center;/// 锚点,取值[0,1],和 zero 的相对位置,比如 0.5 代表 zero 被放到了 Viewport.height / 2 处final double anchor;/// 滚动的累计值,确切的说是 viewport 从什么地方开始显示final ViewportOffset offset;/// 缓存区域,也就是相对有头尾需要预加载的高度final double cacheExtent;/// children widgetList<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.infinitydouble? 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.scrollOffsetthis.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 {/// 最大高度@overridedouble get maxExtent => 100;/// 最小高度@overridedouble get minExtent => 50;/// shrinkOffset: 当前 sliver 顶部越过屏幕顶部的距离/// overlapsContent: 下方是否还有 content 显示@overrideWidget build(BuildContext context, double shrinkOffset, bool overlapsContent) {return your widget);}/// 是否需要刷新@overridebool 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 {@overrideWidget 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.mzph.cn/news/640507.shtml

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

相关文章

软考系分之计算机网络规划设计、综合布线、RAID和网络存储等

文章目录 1、概要2、网络的三层模型3、综合布线系统4、廉价磁盘冗余阵列&#xff08;RAID&#xff09;5、网络存储6、总结 1、概要 本篇重点介绍计算机网络中的网络规划设计、综合布线、RAID和网络存储。 2、网络的三层模型 三层模型分为核心层、汇聚层和接入层&#xff0c;接…

【C++修行之道】竞赛常用库函数(sort,min和max函数,min_element和max_element、nth_element)

目录 一、sort 1.1sort简介 语法 参数 功能 适用容器 1.2sort的用法 1.3自定义比较函数 示例 1265蓝桥题 —— 排序 二、min和max函数 三、min_element和max_element 497蓝桥题 —— 成绩分析 四、nth_element 一、sort 1.1sort简介 sort函数包含在头文件<a…

Vue3组件库开发 之Button(2) 未完待续

Vue3组件库开发 之Button(1) 中新建项目&#xff0c;但未安装成功ESLINT 安装ESLINT npm install eslint vite-plugin-eslint --save-dev 安装eslint后&#xff0c;组件文件出现错误提示 添加第三方macros &#xff0c;虽然不是官网但很多开发者都是vue3开发人员 安装macros…

C++大学教程(第九版)6.29素数

题目 (素数)素数是只能被1和自已整除的整数。例如,235和7是素数而468和9不是素数 a)编写一个函数&#xff0c;确定一个数是否是素数。 b)在程序中使用这个函数&#xff0c;该程序确定和打印2 ~10000之间的所有素数。在确信已找到所有的素数之前&#xff0c;实际需测试这些数中…

基于PSO-BP神经网络的风电功率预测(MATLAB)

作品简介 &#xff1a;关注公众号“电击小子程高兴的MATLAB小屋”获取优惠 主要内容 该模型将粒子群算法与BP神经网络结合用于BP神经网络的训练&#xff0c;即优化BP网络中的连接权值和各项阈值&#xff0c;然后利用神经网络分布式并行处理优势、自适应学习能力以及较好的…

c++中的包装器 function

文章目录 前言包装器 function及模板的低效性修复问题 前言 C提供了多个包装器&#xff08;wrapper&#xff0c;也叫适配器adapter&#xff09;。这些对象用于给其他编程接口提供更一致或更合适的接口。 bind1st和bind2ed就是两个适配器&#xff0c;它们让接受两个参数的函数…

【.NET Core】多线程之线程池(ThreadPool)详解(二)

【.NET Core】多线程之线程池&#xff08;ThreadPool&#xff09;详解&#xff08;二&#xff09; 在上一篇《【.NET Core】多线程之线程池&#xff08;ThreadPool&#xff09;详解&#xff08;一&#xff09;》中我们详细讲解了&#xff0c;线程池概念&#xff0c;如何应用及…

[嵌入式软件][启蒙篇][仿真平台] STM32F103实现定时器

[嵌入式软件][启蒙篇][仿真平台] STM32F103实现串口输出输入、ADC采集 文章目录 一、定时器(1) 简介STM32定时器计算公式 (2) 示例代码&#xff08;基本定时功能&#xff09;(3) 仿真效果 &#xff08;基本定时功能&#xff09;(4) 示例代码&#xff08;PWM 呼吸灯&#xff09…

【GitHub项目推荐--12306 抢票助手 python】【转载】

这个项目名很干脆&#xff0c;不知道以为是 12306 网站的源码&#xff0c;其实不是这是全 GitHub最德高望重的抢票小助手&#xff0c;功能一直在更新&#xff0c;且现已支持 Python 3.6 以上版本。 开源地址&#xff1a;https://github.com/testerSunshine/12306

利用GPU加速自定义风格图像生成-利用GPU加速结合了ControlNet/ Lora的Stable Diffusion XL

点击链接完成注册&#xff0c;参加本次在线研讨会 https://www.nvidia.cn/webinars/sessions/?session_id240124-31319 随着AI技术的发展, 数字内容创建业务也变得越来越火热。生成式AI模型的发布, 让我们看到了人工智能在各行各业的潜力。您只需要用语言简单描述自己希望看…

【Java】Maven的安装与配置

初识Maven Maven是专门用于管理和构建Java项目的工具&#xff0c;它的主要功能有&#xff1a; 提供了一套标准化的项目结构 提供了一套标准化的构建流程&#xff08;编译&#xff0c;测试&#xff0c;打包&#xff0c;发布……&#xff09; 提供了一套依赖管理机制 标准化的…

质疑鸿蒙系统的是什么人群?为什么要杠

《HarmonyOSNEXT星空版》已堵住大部分人质疑的嘴。就在本月1月18&#xff0c;华为鸿蒙生态千帆启航仪式正式&#xff0c;HarmonyOSNEXT鸿蒙星河版发布&#xff1a;全面自研。 架构层面&#xff0c;HarmonyOSNEXT不依赖传统的Unix内核和Linux内核&#xff0c;而是实现了AI大模型…

《高教学刊》是什么级别的期刊?是正规期刊吗?是核心期刊吗?

​标题解答 1、《高教学刊》是什么级别的期刊&#xff1f; 省级 2、《高教学刊》是核心期刊吗&#xff1f; 不是&#xff0c;只是封面有核心字样 《高教学刊》刊发高等教育教学与高教理论研究成果&#xff0c;交流高校教学与建设的改革措施和实践经验&#xff0c;探索高等教…

每天五分钟计算机视觉:掌握迁移学习使用技巧

本文重点 随着深度学习的发展,迁移学习已成为一种流行的机器学习方法,它能够将预训练模型应用于各种任务,从而实现快速模型训练和优化。然而,要想充分利用迁移学习的优势,我们需要掌握一些关键技巧。本文将介绍这些技巧,帮助您更好地应用迁移学习技术。 迁移学习的关键…

高客单价企业必读:私域运营趋势分析与实操技巧

一、深入挖掘&#xff1a;场景洞察的新维度 当我们收到销售的群发信息时&#xff0c;通常会感到被打扰或骚扰&#xff0c;这是因为这些信息通常是基于广泛的受众群体发送的&#xff0c;缺乏针对个体消费者的定制化和个性化。这种缺乏个性化的沟通方式很容易被消费者视为不必要…

Temu活动库存设置多少合适,Temu活动要押金吗?-站斧浏览器

Temu活动库存设置多少合适&#xff1f; 对于Temu活动库存的设置&#xff0c;并没有固定的标准。其设置应根据商品的特性、市场需求、以及您的销售目标等因素进行综合考虑。然而&#xff0c;以下是一些建议&#xff0c;可以帮助您做出决策&#xff1a; 商品特性&#xff1a;某…

C++中命名空间、缺省参数、函数重载

目录 1.命名空间 2.缺省参数 3.函数重载 1.命名空间 在C中定义命名空间我们需要用到namespace关键字&#xff0c;后面跟上命名空间的名字&#xff0c;结构框架有点类似结构体&#xff08;如图所示&#xff09; 上面的代码我一一进行讲解&#xff1a; 1.我们先来说第三行和main函…

MAXWELL

MAXWELL 一、maxwell是什么 maxwell 官网地址&#xff1a;http://maxwells-daemon.io/ 因为官网是纯英文的&#xff0c;倒是不难懂&#xff0c;但总觉得写的略粗糙&#xff08;也可能笔者英文水平确实拉胯&#xff0c;有待提高&#xff09;。所以还是自己百度了一下。 当my…

测试必须要知道的四个主要阶段

&#x1f525; 交流讨论&#xff1a;欢迎加入我们一起学习&#xff01; &#x1f525; 资源分享&#xff1a;耗时200小时精选的「软件测试」资料包 &#x1f525; 教程推荐&#xff1a;火遍全网的《软件测试》教程 &#x1f4e2;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1…

力扣hot100 反转链表 指针 递归 一题多解

Problem: 206. 反转链表 文章目录 思路&#x1f496; 迭代 双指针&#x1f496; 递归 思路 &#x1f468;‍&#x1f3eb; 大佬题解 &#x1f496; 迭代 双指针 ⏰ 时间复杂度: O ( n ) O(n) O(n) &#x1f30e; 空间复杂度: O ( 1 ) O(1) O(1) /*** Definition for …