这是我参与更文挑战的第6天,活动详情查看: 更文挑战
注:本文从个人公众号(岛前屿端)中迁移重新发布
Flutter 是谷歌的移动 UI 框架,可以从单个代码库快速的为移动端(iOS & Android)、Web、桌面端、嵌入式设备上构建高质量的原生用户界面和应用程序。
在 Flutter 视图布局(一)中文章结束时留下了一个问题,大家有尝试去实现吗?
如果大家认真看文章的话,这并不是很难的东西。
当然如果有配合 github 项目的代码来看的话,一定会发现我也已经将实现好的代码也更新上去了,可以作为实现参考。
好,那么我们就废话不多说,这次我们就来说道说道 ListBody 和 ListView 这两个常用的布局 List Widget。
在此之前我们还是要说说 Flutter 的包管理方式,因为这是开发中必不可少的绕不开的一部分。
包管理方式
在 MyApp 项目目录下有个 pubspec.yaml 文件,这个文件主要是 Flutter 用于管理外部依赖项。
YAML 是一个标记性语言,它对大小写敏感,由于不像其他类型文件的数据格式拥有明显的父、子级标记而是默认使用空格缩进 (2个空格) 代表层级,比如用 “- ” (中划线+空格) 来表示列表。
当然,在默认的文件中也有示例说明,这就需要你自己去打开文件看一看啦。
在默认的文件情况下我们可以看到一级分类由以下类型组成。现在我们从上到下来分别解释一下这些东西到底是干什么的:
- name 项目名称
- description 简介
- version 版本号
- environment 环境,表示 SDK 版本
- dependencies 依赖项
- dev_dependencies 开发依赖项
- flutter 所需资源文件引入
然后现在我们先在 dependencies 中加入 english_words
,这个英文单词的包主要是用于后续的例子中,可以先考虑引入。
english_words: ^3.1.0
复制代码
在添加完新的依赖包后,当你进行保存时 VS Code 会自动进行依赖包的更新和下载,还是比较方便的,就不需要手动进行更新命令了。
当然也可以手动执行命令(最终作用是相同的)
flutter packages get
# or
flutter pub get
复制代码
ok,接下来我们可以说说 ListBody 和 ListView 了
ListBody
我们先来看看 ListBody 的源码部分
(ListBody 源码部分)
这不看都不知道,相比 Row、Column 来说简直是太简单了:
- Axis mainAxis 主轴,默认垂直方向,即 y 轴
- bool reverse 是否反向/颠倒顺序的
- List<Widget> children 子元素列表 Widget 类型
都看到这了,才 三个属性 ,那还等什么?当然是上手就干啊!
(我的嘴角微微上翘,手指在键盘上噼里啪啦一通乱舞……)
我的内心毫无波澜甚至还有些想笑
看着代码完成了,也没有明显报错,这很OK,召唤控制台 – 输入 – R 命令
兴奋的等待……
发生了什么?
这是怎么回事?发生了什么事?!
冷静一下不要慌,让我们来看看源码。
(ListBody 源码注释部分)
看完之后发现,原来 ListBody 是一个可以设定轴方向的 多子元素列表,但是需要一个可以强制范围的容器来装载它。而且这是一个很少不能够直接使用的 Widget,如果需要的话应该优先选择 ListView,因为它有相同的布局方式以及提供了滚动行为。
(摸着下巴若有所思)OK,那我们就来把他放在 ListView 下。
(ListBody 放在 ListView 下)
这样就没什么问题了!
ListView
关于 ListView 还是要先认真看下源码,这次可不能那么鲁莽。
(ListView 源码部分)
仔细一看,这属性还挺多。不着急,那我们分别都来看一看。
- Axis scrollDirection 滚动的方向,即轴方向,Axis.vertical 垂直方向 和 Axis.horizontal 水平方向,默认为垂直方向
- bool reverse 是否反向/颠倒顺序的,默认为 false,如为 true 则 垂直方向从底部开始,水平方向从右边开始
- bool primary 是否是主主要的滚动 Widget,默认为 false, 如果为 true 则 controller 必须为 null
- bool shrinkWrap 是否收缩滚动视图
- EdgeInsetsGeometry padding 顾名思义填充的内边距
- ScrollController controller 滚动事件,与 primary 互斥
- ScrollPhysics physics 滚动的行为方式
- bool addAutomaticKeepAlives
- bool addRepaintBoundaries
- bool addSemanticIndexes
- double cacheExtent
- int semanticChildCount
- DragStartBehaviordragStartBehavior
- List<Widget> children 子元素列表 Widget 类型
reverse
reverse 就是将列表的渲染方式是否是反向,垂直方向从底部开始,水平方向从右边开始。
(reverse 反向渲染/颠倒列表)
controller
关于滚动事件,如果真要说的话,那么篇幅就太长了,所以这里暂时不讲,后续会将一些 Widget 的事件 整理出来。
如果各位少侠小伙伴们有兴趣的,可以先看看这个滚动事件参考:
book.flutterchina.club/chapter6/sc…
primary 与 controller 互斥,当 controller 定义了事件且 primary 为 true 时 则会 喜提满屏红。
(primary 与 controller 互斥)
在源码中有这样一段:
如果 primary 为 true 则 controller 必须为 null,controller 滚动事件,与 primary 互斥。
(源码)
addAutomaticKeepAlives
是否将子项都装在 AutomaticKeepAlive 中,默认为 true。
(addAutomaticKeepAlives 源码部分说明)
简单来说(翻译一下),通常列表是懒惰的,将子元素装在 AutomaticKeepAlive 中,以便其子级元素可以使用 KeepAliveNotification 来保留状态,否则它们在屏幕外将被回收。
如果需要手动维护子元素的子级元素那么就必须 禁用此功能(false) (以及 addRepaintBoundaries 设为 false)。
再简单来说,就是子元素可以超出屏幕之外还继续保留,但是这个状态的保留由框架负责。如果你需要自己决定如何保留子元素的状态,那么就把 addAutomaticKeepAlives 和 addRepaintBoundaries 关了自己写去吧(Flutter 可不伺候你)。
addRepaintBoundaries
是否将子项都装在 RepaintBoundary 中,默认为 true。
(addRepaintBoundaries 源码部分说明)
简单来说(翻译一下),通常在可滚动列表的容器中子项都会被装在重绘边界之内,以便列表在滚动时不需要将它们进行重绘。如果是简单的子项内容 (纯色块或者短文本) ,则关闭 addRepaintBoundaries(false) 让其重绘子项可能会更有效率。
简单来说,不能再简单了,请少侠们自己思考。
addSemanticIndexes
是否将子项都装在 IndexedSemantics 中,默认依然为 true。
(addSemanticIndexes 源码部分说明)
cacheExtent
在视图可见区域之外有一个区域 (即垂直是上下部分,水平是左右部分) ,用于缓存滚动即进入可见区域的子元素。
进入此缓存区域的子项在即使未在可见视图内也是可见的,即是进入可见区域后就会被布局渲染,cacheExtent 主要是用于描述该区域所延伸的大小。
physics
physics 主要是 滚动的物理效果
- ClampingScrollPhysics 默认的钳位效果
- BouncingScrollPhysics 回弹的物理效果
- FixedExtentScrollPhysics 拨轮式的物理效果
- AlwaysScrollableScrollPhysics 始终可以滚动效果
- NeverScrollableScrollPhysics 禁止滚动效果
AlwaysScrollableScrollPhysics 和 NeverScrollableScrollPhysics 就不用演示效果了,毕竟这个意思和 CSS 中 overflow 的 scroll 和 hidden 一个意思。
ClampingScrollPhysics
ClampingScrollPhysics 我也不知道为什么要用 Clamping,可能是像钳子一样拥有最大张合度吧。在默认情况下,如果列表子元素不足以超出可视范围则不会产生可滚动行为。如超出可视范围则到达列表尽头时会停留并有水波样式出现。
(ClampingScrollPhysics 的滚动效果)
BouncingScrollPhysics
BouncingScrollPhysics 的话就是大家都熟悉的回弹效果了,当操作列表到达可视范围尽头时还可以继续超出一定的空间,当失去焦点后回到尽头的位置,这样就能给予用户一个良好的使用体验。一般来说都会在下拉刷新上拉加载这样的场景里使用。
(BouncingScrollPhysics 的滚动效果)
BouncingScrollPhysics
BouncingScrollPhysics 的滚动效果
FixedExtentScrollPhysics 是类似拨轮的效果,怎么说呢,这个用文字还真不好描述效果,看一张实物图大概就能理解了。
拨轮日期印章(图片来自网络)
(FixedExtentScrollPhysics 的滚动效果)
无限滚动例子
以上就是 ListView 属性的使用说明了,但是你可能会问了
哎呀,这些子元素你写那么多不现实啊,真正使用到的时候肯定都是按需生成的,不然如果有很多子元素不可能都 copy paste 一遍吧?
很好,我很佩服你提问的勇气!不过没关系,Flutter 当然也知道这个问题,那么我们就来看看它有哪些相关的方法可以使用。
不用多说,我们还是来先看源码。
(ListView 源码注释说明)
源码中说到 ListView 有4中设置子元素的方式:
- List<Widget>
- ListView.builder
- ListView.separated
- ListView.custom
第一种 List<Widget> 就不用多说了,我们常用的直接写在列表里的方式。另外的三种方式就需要我们编码去实现了。
需要编码的三个构造函数都拥有相同的属性这也是最常用的属性:
- padding 每个元素的边距
- itemCount 元素的数量,默认为 null 即无限
- itemBuilder 接受一个回调函数 参数为:BuildContext context, int index
ListView.builder
首先还是要翻译一下源码里是怎么解释这个方法的:
使用了 indexedWidgetBuilder 它可以按需生成子元素,此构造函数适用于列表需要大量或者无限子元素生成,因为其调用了元素生成器,所以仅在实际可视范围中显示。
Ok,那我们就来看看代码是如何实现的。
(ListView.builder 实现动态生成子元素)
当 itemCount 设置为 null 时就可以实现无限下拉列表。少侠们可以在代码中尝试修改一下看看效果。
ListView.separated
首先还是要翻译一下源码里是怎么解释这方法的:
使用了两个 indexedWidgetBuilder 来处理子元素,itembuilder 是按需生成子元素,separatorbuilder 是根据子元素来生成子元素之间的分隔符元素。此构造函数只能适用于子级数量确定的列表视图。
Ok,那我们就来看看代码是如何实现的。
(ListView.builder 实现动态生成子元素)
其实 separated 和 builder 差别并不大,这里我只做了简单的修改就实现了分割线。
ListView.custom
没错还是要翻译一下源码里是怎么解释这方法的:
构造函数接受一个 sliverChildDelegate,它提供自定义子模型其他方面的功能。例如:sliverchildDelegate 可以控制用于估计实际不可见子级大小的算法。
ListView.custom 要实现起来的话较为麻烦,但还是可以简单实现一下。
主要实现方式有 SliverChildListDelegate 列表方式 和 SliverChildBuilderDelegate 编码方式。
(SliverChildListDelegate 列表方式实现)
(SliverChildBuilderDelegate 编码方式实现)
最终效果的话,少侠们,可以自己更新修改代码尝试哟。
最后
ListView Widget 的内容其实并不难,列表的使用都有对应的场景,只要熟悉了列表的渲染特征后,碰见相应的场景自然就不用纠结到底使用哪一个更合适了。其中的难点还是在于 ListView.custom 的实现上,他需要你自己去实现列表相关的所有东西:监听滚动
、渲染子元素的方式
、销毁子元素
等等。
最后总结
Flutter 基本上为你考虑了一些相关场景使用的实现,所以可以很方便的使用这些内容,但是考虑过细自然也就会觉得需要了解的内容就过多。
就单单从“列表”来看,大致和其他语言的实现是相似的,了解其中常用的属性即可正常使用。
配合文章一同食用的代码已同步更新到 Github 地址:github.com/linxsbox/my…
参考
- 《Flutter实战》:book.flutterchina.club/chapter6/sc…
- Flutter 官网 API:api.flutter.dev/flutter/wid…