阅读 323

【Flutter 基础】 视图布局(二)List Widget

这是我参与更文挑战的第6天,活动详情查看: 更文挑战

注:本文从个人公众号(岛前屿端)中迁移重新发布

Flutter 是谷歌的移动 UI 框架,可以从单个代码库快速的为移动端(iOS & Android)、Web、桌面端、嵌入式设备上构建高质量的原生用户界面和应用程序。


在 Flutter 视图布局(一)中文章结束时留下了一个问题,大家有尝试去实现吗?

如果大家认真看文章的话,这并不是很难的东西。

当然如果有配合 github 项目的代码来看的话,一定会发现我也已经将实现好的代码也更新上去了,可以作为实现参考。

好,那么我们就废话不多说,这次我们就来说道说道 ListBodyListView 这两个常用的布局 List Widget

在此之前我们还是要说说 Flutter 的包管理方式,因为这是开发中必不可少的绕不开的一部分。

包管理方式

在 MyApp 项目目录下有个 pubspec.yaml 文件,这个文件主要是 Flutter 用于管理外部依赖项

YAML 是一个标记性语言,它对大小写敏感,由于不像其他类型文件的数据格式拥有明显的父、子级标记而是默认使用空格缩进 (2个空格) 代表层级,比如用 “- ” (中划线+空格) 来表示列表。

当然,在默认的文件中也有示例说明,这就需要你自己去打开文件看一看啦。

在默认的文件情况下我们可以看到一级分类由以下类型组成。现在我们从上到下来分别解释一下这些东西到底是干什么的:

  1. name 项目名称
  2. description 简介
  3. version 版本号
  4. environment 环境,表示 SDK 版本
  5. dependencies 依赖项
  6. dev_dependencies 开发依赖项
  7. flutter 所需资源文件引入

然后现在我们先在 dependencies 中加入 english_words,这个英文单词的包主要是用于后续的例子中,可以先考虑引入。

english_words: ^3.1.0
复制代码

在添加完新的依赖包后,当你进行保存时 VS Code 会自动进行依赖包的更新和下载,还是比较方便的,就不需要手动进行更新命令了。

当然也可以手动执行命令(最终作用是相同的)

flutter packages get
# or
flutter pub get
复制代码

ok,接下来我们可以说说 ListBody 和 ListView 了

ListBody

我们先来看看 ListBody 的源码部分

image.png
(ListBody 源码部分)

这不看都不知道,相比 Row、Column 来说简直是太简单了:

  • Axis mainAxis 主轴,默认垂直方向,即 y 轴
  • bool reverse 是否反向/颠倒顺序的
  • List<Widget> children 子元素列表 Widget 类型

都看到这了,才 三个属性 ,那还等什么?当然是上手就干啊!

(我的嘴角微微上翘,手指在键盘上噼里啪啦一通乱舞……)

image.png

我的内心毫无波澜甚至还有些想笑

看着代码完成了,也没有明显报错,这很OK,召唤控制台 - 输入 - R 命令

兴奋的等待……

image.png

发生了什么?

这是怎么回事?发生了什么事?!

冷静一下不要慌,让我们来看看源码。

image.png
(ListBody 源码注释部分)

看完之后发现,原来 ListBody 是一个可以设定轴方向的 多子元素列表,但是需要一个可以强制范围的容器来装载它。而且这是一个很少不能够直接使用的 Widget,如果需要的话应该优先选择 ListView,因为它有相同的布局方式以及提供了滚动行为。

(摸着下巴若有所思)OK,那我们就来把他放在 ListView 下。

image.png
(ListBody 放在 ListView 下)

这样就没什么问题了!

ListView

关于 ListView 还是要先认真看下源码,这次可不能那么鲁莽。

image.png
(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 就是将列表的渲染方式是否是反向,垂直方向从底部开始,水平方向从右边开始。

image.png
(reverse 反向渲染/颠倒列表)

controller

关于滚动事件,如果真要说的话,那么篇幅就太长了,所以这里暂时不讲,后续会将一些 Widget 的事件 整理出来。

如果各位少侠们有兴趣的,可以先看看这个滚动事件参考:

book.flutterchina.club/chapter6/sc…

api.flutter.dev/flutter/wid…

primary 与 controller 互斥,当 controller 定义了事件且 primarytrue 时 则会 喜提满屏红。

image.png
(primary 与 controller 互斥)

在源码中有这样一段:

如果 primary 为 true 则 controller 必须为 null,controller 滚动事件,与 primary 互斥。

image.png (源码)

addAutomaticKeepAlives

是否将子项都装在 AutomaticKeepAlive 中,默认为 true

image.png (addAutomaticKeepAlives 源码部分说明)

简单来说(翻译一下),通常列表是懒惰的,将子元素装在 AutomaticKeepAlive 中,以便其子级元素可以使用 KeepAliveNotification 来保留状态,否则它们在屏幕外将被回收。

如果需要手动维护子元素的子级元素那么就必须 禁用此功能(false) (以及 addRepaintBoundaries 设为 false)。

再简单来说,就是子元素可以超出屏幕之外还继续保留,但是这个状态的保留由框架负责。如果你需要自己决定如何保留子元素的状态,那么就把 addAutomaticKeepAlives 和 addRepaintBoundaries 关了自己写去吧(Flutter 可不伺候你)

addRepaintBoundaries

是否将子项都装在 RepaintBoundary 中,默认为 true

image.png (addRepaintBoundaries 源码部分说明)

简单来说(翻译一下),通常在可滚动列表的容器中子项都会被装在重绘边界之内,以便列表在滚动时不需要将它们进行重绘。如果是简单的子项内容 (纯色块或者短文本) ,则关闭 addRepaintBoundaries(false) 让其重绘子项可能会更有效率。

简单来说,不能再简单了,请少侠们自己思考。

addSemanticIndexes

是否将子项都装在 IndexedSemantics 中,默认依然为 true

image.png
(addSemanticIndexes 源码部分说明)

cacheExtent

在视图可见区域之外有一个区域 (即垂直是上下部分,水平是左右部分) ,用于缓存滚动即进入可见区域的子元素。

image.png

进入此缓存区域的子项在即使未在可见视图内也是可见的,即是进入可见区域后就会被布局渲染,cacheExtent 主要是用于描述该区域所延伸的大小

physics

physics 主要是 滚动的物理效果

  • ClampingScrollPhysics 默认的钳位效果
  • BouncingScrollPhysics 回弹的物理效果
  • FixedExtentScrollPhysics 拨轮式的物理效果
  • AlwaysScrollableScrollPhysics 始终可以滚动效果
  • NeverScrollableScrollPhysics 禁止滚动效果

AlwaysScrollableScrollPhysicsNeverScrollableScrollPhysics 就不用演示效果了,毕竟这个意思和 CSS 中 overflow 的 scroll 和 hidden 一个意思。

ClampingScrollPhysics

ClampingScrollPhysics 我也不知道为什么要用 Clamping,可能是像钳子一样拥有最大张合度吧。在默认情况下,如果列表子元素不足以超出可视范围则不会产生可滚动行为。如超出可视范围则到达列表尽头时会停留并有水波样式出现。

640.gif
(ClampingScrollPhysics 的滚动效果)

BouncingScrollPhysics

BouncingScrollPhysics 的话就是大家都熟悉的回弹效果了,当操作列表到达可视范围尽头时还可以继续超出一定的空间,当失去焦点后回到尽头的位置,这样就能给予用户一个良好的使用体验。一般来说都会在下拉刷新上拉加载这样的场景里使用。

640 (1).gif
(BouncingScrollPhysics 的滚动效果)

BouncingScrollPhysics

BouncingScrollPhysics 的滚动效果

FixedExtentScrollPhysics 是类似拨轮的效果,怎么说呢,这个用文字还真不好描述效果,看一张实物图大概就能理解了。

image.png

拨轮日期印章(图片来自网络)

640 (2).gif
(FixedExtentScrollPhysics 的滚动效果)

无限滚动例子

以上就是 ListView 属性的使用说明了,但是你可能会问了

哎呀,这些子元素你写那么多不现实啊,真正使用到的时候肯定都是按需生成的,不然如果有很多子元素不可能都 copy paste 一遍吧?

很好,我很佩服你提问的勇气!不过没关系,Flutter 当然也知道这个问题,那么我们就来看看它有哪些相关的方法可以使用。

不用多说,我们还是来先看源码。

image.png
(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,那我们就来看看代码是如何实现的。

640 (3).gif
(ListView.builder 实现动态生成子元素)

itemCount 设置为 null 时就可以实现无限下拉列表。少侠们可以在代码中尝试修改一下看看效果。

ListView.separated

首先还是要翻译一下源码里是怎么解释这方法的:

使用了两个 indexedWidgetBuilder 来处理子元素,itembuilder 是按需生成子元素separatorbuilder 是根据子元素来生成子元素之间的分隔符元素。此构造函数只能适用于子级数量确定的列表视图。

Ok,那我们就来看看代码是如何实现的。

640 (4).gif
(ListView.builder 实现动态生成子元素)

其实 separatedbuilder 差别并不大,这里我只做了简单的修改就实现了分割线

ListView.custom

没错还是要翻译一下源码里是怎么解释这方法的:

构造函数接受一个 sliverChildDelegate,它提供自定义子模型其他方面的功能。例如:sliverchildDelegate 可以控制用于估计实际不可见子级大小的算法

ListView.custom 要实现起来的话较为麻烦,但还是可以简单实现一下。

主要实现方式有 SliverChildListDelegate 列表方式 和 SliverChildBuilderDelegate 编码方式。

image.png
(SliverChildListDelegate 列表方式实现)

image.png
(SliverChildBuilderDelegate 编码方式实现)

最终效果的话,少侠们,可以自己更新修改代码尝试哟。


最后

ListView Widget 的内容其实并不难,列表的使用都有对应的场景,只要熟悉了列表的渲染特征后,碰见相应的场景自然就不用纠结到底使用哪一个更合适了。其中的难点还是在于 ListView.custom 的实现上,他需要你自己去实现列表相关的所有东西监听滚动渲染子元素的方式销毁子元素等等。

最后总结

Flutter 基本上为你考虑了一些相关场景使用的实现,所以可以很方便的使用这些内容,但是考虑过细自然也就会觉得需要了解的内容就过多。

就单单从“列表”来看,大致和其他语言的实现是相似的,了解其中常用的属性即可正常使用。

配合文章一同食用的代码已同步更新到 Github 地址:github.com/linxsbox/my…

参考

文章分类
前端
文章标签