赞
踩
我们介绍过 Flutter 有两种布局模型:
RenderBox
的盒模型布局。Sliver
( RenderSliver
) 按需加载列表布局。之前我们主要了解了盒模型布局组件,下面学习基于Sliver
的布局组件。
通常可滚动组件的子组件可能会非常多、占用的总高度也会非常大;如果要一次性将子组件全部构建出将会非常昂贵!为此,Flutter中提出一个Sliver
(中文为“薄片”的意思)概念,Sliver
可以包含一个或多个子组件。Sliver
的主要作用是配合:加载子组件并确定每一个子组件的布局和绘制信息,如果 Sliver
可以包含多个子组件时,通常会实现按需加载模型。
只有当 Sliver
出现在视口中时才会去构建它,这种模型也称为“基于Sliver的列表按需加载模型”。可滚动组件中有很多都支持基于Sliver
的按需加载模型,如ListView
、GridView
,但是也有不支持该模型的,如SingleChildScrollView
。
Flutter 中的可滚动组件主要由三个角色组成:Scrollable
、Viewport
和 Sliver
:
Scrollable
:用于处理滑动手势,确定滑动偏移,滑动偏移变化时构建 Viewport
。Viewport
:显示的视窗,即列表的可视区域;Sliver
:视窗里显示的元素。具体布局过程:
Scrollable
监听到用户滑动行为后,根据最新的滑动偏移构建 Viewport
。Viewport
将当前视口信息和配置信息通过 SliverConstraints
传递给 Sliver
。Sliver
中对子组件(RenderBox
)按需进行构建和布局,然后确认自身的位置、绘制等信息,保存在 geometry
中(一个 SliverGeometry
类型的对象)。比如有一个 ListView
,大小撑满屏幕,假设它有 100
个列表项(都是RenderBox
)且每个列表项高度相同,结构如图所示:
图中白色区域为设备屏幕,也是 Scrollable
、 Viewport
和 Sliver
所占用的空间,三者所占用的空间重合,父子关系为:Sliver
父组件为 Viewport
,Viewport
的 父组件为 Scrollable
。注意ListView
中只有一个 Sliver
,在 Sliver
中实现了子组件(列表项)的按需加载和布局。
其中顶部和底部灰色的区域为 cacheExtent
,它表示预渲染的高度,需要注意这是在可视区域之外,如果 RenderBox
进入这个区域内,即使它还未显示在屏幕上,也是要先进行构建的,预渲染是为了后面进入 Viewport
的时候更丝滑。cacheExtent
的默认值是 250
,在构建可滚动列表时我们可以指定这个值,这个值最终会传给 Viewport
。
用于处理滑动手势,确定滑动偏移,滑动偏移变化时构建 Viewport
,我们看一下其关键的属性:
Scrollable({
...
this.axisDirection = AxisDirection.down,
this.controller,
this.physics,
required this.viewportBuilder,
})
属性 | 说明 |
---|---|
axisDirection |
滚动方向 |
physics |
此属性接受一个ScrollPhysics 类型的对象,它决定可滚动组件如何响应用户操作,比如用户滑动完抬起手指后,继续执行动画;或者滑动到边界时,如何显示。 默认情况下,Flutter会根据具体平台分别使用不同的 ScrollPhysics 对象,应用不同的显示效果,如当滑动到边界时,继续拖动的话,在 iOS 上会出现弹性效果,而在 Android 上会出现微光效果。如果你想在所有平台下使用同一种效果,可以显式指定一个固定的 ScrollPhysics ,Flutter SDK中包含了两个ScrollPhysics 的子类,他们可以直接使用: 1) ClampingScrollPhysics :列表滑动到边界时将不能继续滑动,通常在Android 中 配合 GlowingOverscrollIndicator (实现微光效果的组件) 使用。2) BouncingScrollPhysics :iOS 下弹性效果。 |
controller |
此属性接受一个ScrollController 对象。ScrollController 的主要作用是控制滚动位置和监听滚动事件。默认情况下,Widget树中会有一个默认的 PrimaryScrollController ,如果子树中的可滚动组件没有显式的指定controller ,并且primary 属性值为true 时(默认就为true ),可滚动组件会使用这个默认的PrimaryScrollController 。这种机制带来的好处是父组件可以控制子树中可滚动组件的滚动行为,例如, Scaffold 正是使用这种机制在iOS中实现了点击导航栏回到顶部的功能。 |
viewportBuilder |
构建 Viewport 的回调。当用户滑动时,Scrollable 会调用此回调构建新的 Viewport ,同时传递一个 ViewportOffset 类型的 offset 参数,该参数描述 Viewport 应该显示那一部分内容。注意重新构建 Viewport 并不是一个昂贵的操作,因为 Viewport 本身也是 Widget ,只是配置信息,Viewport 变化时对应的 RenderViewport 会更新信息,并不会随着 Widget 进行重新构建。 |
主轴和纵轴
在可滚动组件的坐标描述中,通常将滚动方向称为主轴,非滚动方向称为纵轴。由于可滚动组件的默认方向一般都是沿垂直方向,所以默认情况下主轴就是指垂直方向,水平方向同理。
Viewport
比较简单,用于渲染当前视口中需要显示 Sliver
。
Viewport({
Key? key,
this.axisDirection = AxisDirection.down,
this.crossAxisDirection,
this.anchor = 0.0,
required ViewportOffset offset, // 用户的滚动偏移
this.center, // 类型为Key,表示从什么地方开始绘制,默认是第一个元素
this.cacheExtent, // 预渲染区域
// 该参数用于配合解释cacheExtent的含义,也可以为主轴长度的乘数
this.cacheExtentStyle = CacheExtentStyle.pixel,
this.clipBehavior = Clip.hardEdge,
List<Widget> slivers = const <Widget>[], // 需要显示的 Sliver 列表
})
需要注意的是:
offset
:该参数为 Scrollabel
构建 Viewport
时传入,它描述了 Viewport
应该显示那一部分内容。cacheExtent
和 cacheExtentStyle
:CacheExtentStyle
是一个枚举,有 pixel
和 viewport
两个取值。
cacheExtentStyle
值为 pixel
时,cacheExtent
的值为预渲染区域的具体像素长度;cacheExtentStyle
值为 viewport
时,cacheExtent
的值是一个乘数,表示有几个 viewport
的长度,最终的预渲染区域的像素长度为:cacheExtent * viewport
的积, 这在每一个列表项都占满整个 Viewport
时比较实用,这时 cacheExtent
的值就表示前后各缓存几个页面。Sliver
主要作用是对子组件进行构建和布局,比如 ListView
的 Sliver
需要实现子组件(列表项)按需加载功能,只有当列表项进入预渲染区域时才会去对它进行构建和布局、渲染。
Sliver
对应的渲染对象类型是 RenderSliver
,RenderSliver
和 RenderBox
的相同点是都继承自 RenderObject
类,不同点是在布局的时候约束信息不同。RenderBox
在布局时父组件传递给它的约束信息对应的是 BoxConstraints
,只包含最大宽高的约束;而 RenderSliver
在布局时父组件(列表)传递给它的约束是对应的是 SliverConstraints
。
几乎所有可滚动组件在构造时都能指定的通用属性有:
scrollDirection
(滑动的主轴)reverse
(滑动方向是否反向,指阅读方向的反方向,取决于语言环境)controller
physics
cacheExtent
这些属性最终会透传给对应的 Scrollable
和 Viewport
。
Scrollbar
是一个Material风格的滚动指示器(滚动条),如果要给可滚动组件添加滚动条,只需将Scrollbar
作为可滚动组件的任意一个父级组件即可,如:
Scrollbar(
child: SingleChildScrollView(
...
),
);
Scrollbar
和CupertinoScrollbar
都是通过监听滚动通知来确定滚动条位置的。CupertinoScrollbar
是 iOS 风格的滚动条,如果你使用的是Scrollbar
,那么在iOS平台它会自动切换为CupertinoScrollbar
。
SingleChildScrollView
类似于Android中的ScrollView
,它只能接收一个子组件,定义如下:
SingleChildScrollView({
this.scrollDirection = Axis.vertical, // 滚动方向,默认是垂直方向
this.reverse = false,
this.padding,
bool primary,
this.physics,
this.controller,
this.child,
})
除了前面介绍过的可滚动组件的通用属性外,这里重点关注primary
属性:
widget
树中默认的PrimaryScrollController
(MaterialApp
组件树中已经默认包含一个 PrimaryScrollController
了);scrollDirection
值为Axis.vertical
)并且没有指定controller
时,primary
默认为true
。需要注意的是,通常SingleChildScrollView
只应在期望的内容不会超过屏幕太多时使用,这是因为SingleChildScrollView
不支持基于 Sliver
的延迟加载模型,所以如果预计视口可能包含超出屏幕尺寸太多的内容时,那么使用SingleChildScrollView
将会非常昂贵(性能差),此时应该使用一些支持Sliver
延迟加载的可滚动组件,如ListView
。
下面是一个使用SingleChildScrollView
将大写字母 A-Z
沿垂直方向显示的例子:
class SingleChildScrollViewTestRoute extends StatelessWidget { @override Widget build(BuildContext context) { String str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; return Scrollbar( // 显示进度条 child: SingleChildScrollView( padding: EdgeInsets.all(16.0), child: Center( child: Column( //动态创建一个List<Widget> children: str.split("") //每一个字母都用一个Text显示,字体为原来的两倍 .map((c) => Text(c, textScaleFactor: 2.0,)) .toList(), ), ), ), ); } }
效果:
ListView
是最常用的可滚动组件之一,它可以沿一个方向线性排布所有子组件,并且它也支持列表项懒加载(在需要时才会创建)。
我们看看ListView
的默认构造函数定义:
ListView({ ... // 可滚动 widget 公共参数 Axis scrollDirection = Axis.vertical, // 滑动方向,Axis.horizontal水平列表Axis.vertical垂直列表 bool reverse = false, // 滑动方向是否反向 ScrollController? controller, // 控制可滚动组件的滚动 bool? primary, // 是否使用 widget 树中默认的PrimaryScrollController ScrollPhysics? physics, // 如滑动到边界时效果 EdgeInsetsGeometry? padding, // 内边距 // ListView 各个构造函数的共同参数 double? itemExtent, Widget? prototypeItem, // 列表项原型 bool shrinkWrap = false, bool addAutomaticKeepAlives = true, bool addRepaintBoundaries = true, double? cacheExtent, // 预渲染区域长度 // 列表项元素 List<Widget> children = const <Widget>[], })
上面参数分为两组:第一组是可滚动组件的公共参数,前面已经介绍过,不再赘述;第二组是ListView
各个构造函数(ListView
有多个构造函数)的共同参数,我们重点来看看这些参数:
itemExtent
:该参数如果不为null
,则会强制children
的“长度”为itemExtent
的值;这里的“长度”是指滚动方向上子组件的长度,也就是说如果滚动方向是垂直方向,则itemExtent
代表子组件的高度;如果滚动方向为水平方向,则itemExtent
就代表子组件的宽度。
在ListView
中,指定itemExtent
比让子组件自己决定自身长度会有更好的性能,这是因为指定itemExtent
后,滚动系统可以提前知道列表的长度,而无需每次构建子组件时都去再计算一下,尤其是在滚动位置频繁变化时(滚动系统需要频繁去计算列表高度)。
prototypeItem
:如果我们知道列表中的所有列表项长度都相同但不知道具体是多少,这时我们可以指定一个列表项,该列表项被称为 prototypeItem
(列表项原型)。指定 prototypeItem
后,可滚动组件会在 layout 时计算一次它延主轴方向的长度,这样也就预先知道了所有列表项的延主轴方向的长度,所以和指定 itemExtent
一样,指定 prototypeItem
会有更好的性能。
注意:itemExtent
和prototypeItem
互斥,不能同时指定它们。
shrinkWrap
:该属性表示是否根据子组件的总长度来设置ListView
的长度,默认值为false
。默认情况下,ListView
会在滚动方向尽可能多的占用空间。
注意:当ListView
在一个无边界(滚动方向上)的容器中时,shrinkWrap
必须为true
。
addAutomaticKeepAlives
:该属性我们将在介绍 PageView 组件时详细解释。
addRepaintBoundaries
:该属性表示是否将列表项(子组件)包裹在RepaintBoundary
组件中。RepaintBoundary
可以先简单理解为它是一个”绘制边界“,将列表项包裹在RepaintBoundary
中可以避免列表项不必要的重绘,但是当列表项重绘的开销非常小(如一个颜色块,或者一个较短的文本)时,不添加RepaintBoundary
反而会更高效。如果列表项自身来维护是否需要添加绘制边界组件,则此参数应该指定为 false
。
注意:上面这些参数并非
ListView
特有,其他可滚动组件也可能会拥有这些参数,它们的含义是相同的。
默认构造函数有一个children
参数,它接受一个Widget
列表(List<Widget>
)。这种方式适合只有少量的子组件数量已知且比较少的情况,反之则应该使用ListView.builder
按需动态构建列表项。
注意:虽然这种方式将所有
children
一次性传递给ListView
,但子组件仍然是在需要时才会加载(build、布局、绘制),也就是说通过默认构造函数构建的ListView
也是基于Sliver
的列表懒加载模型。
下面是一个例子:
ListView(
shrinkWrap: true,
padding: const EdgeInsets.all(20.0),
children: <Widget>[
const Text('I\'m dedicating every day to you'),
const Text('Domestic life was never quite my style'),
const Text('When you smile, you knock me out, I fall apart'),
const Text('And I thought I was so smart'),
],
);
可以看到,虽然使用默认构造函数创建的列表也是懒加载的,但我们还是需要提前将 Widget 创建好,等到真正需要加载的时候才会对 Widget 进行布局和绘制。
ListView.builder
适合列表项比较多或者列表项不确定的情况,下面看一下ListView.builder
的核心参数列表:
ListView.builder({
// ListView公共参数已省略
...
required IndexedWidgetBuilder itemBuilder,
int itemCount,
...
})
itemBuilder
:它是列表项的构建器,类型为IndexedWidgetBuilder
,返回值为一个widget
。当列表滚动到具体的index
位置时,会调用该构建器构建列表项。
itemCount
:列表项的数量,如果为null
,则为无限列表。
下面看一个例子:
ListView.builder(
itemCount: 100,
itemExtent: 50.0, // 强制高度为 50.0
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text("$index"));
}
);
效果:
ListView.separated
可以在生成的列表项之间添加一个分割组件,它比ListView.builder
多了一个separatorBuilder
参数,该参数是一个分割组件生成器。
下面我们看一个例子:奇数行添加一条蓝色下划线,偶数行添加一条绿色下划线。
class ListView3 extends StatelessWidget { @override Widget build(BuildContext context) { // 下划线widget预定义以供复用 Widget divider1 = Divider(color: Colors.blue,); Widget divider2 = Divider(color: Colors.green); return ListView.separated( itemCount: 100, itemBuilder: (BuildContext context, int index) { // 列表项构造器 return ListTile(title: Text("$index")); }, separatorBuilder: (BuildContext context, int index) { // 分割器构造器 return index%2==0?divider1:divider2; }, ); } }
效果:
前面说过,给列表指定 itemExtent
或 prototypeItem
会有更高的性能,所以当我们知道列表项的高度都相同时,强烈建议指定 itemExtent
或 prototypeItem
。
下面看一个示例:
class FixedExtentList extends StatelessWidget { const FixedExtentList({ Key? key}) : super(key: key); @override Widget build(BuildContext context) { return ListView.builder( prototypeItem: ListTile(title: Text("1")), // itemExtent: 56, itemBuilder: (context, index) { // LayoutLogPrint是一个自定义组件,在布局时可以打印当前上下文中父组件给子组件的约束信息 return LayoutLogPrint( tag: index, child: ListTile(title: Text("$index")), ); }, ); } }
因为列表项都是一个 ListTile
,高度相同,但是我们不知道 ListTile
的高度是多少,所以指定了prototypeItem
,运行后,控制台打印:
flutter: 0: BoxConstraints(w=428.0, h=56.0)
flutter: 1: BoxConstraints(w=428.0, h=56.0)
flutter: 2: BoxConstraints(w=428.0, h=56.0)
...
可见 ListTile
的高度是 56
,所以我们指定 itemExtent
为 56
也是可以的。但是还是建议优先指定原型,这样的话在列表项布局修改后,仍然可以正常工作(前提是每个列表项的高度相同)。
如果本例中不指定 itemExtent
或 prototypeItem
,我们看看控制台日志信息:
flutter: 0: BoxConstraints(w=428.0, 0.0<=h<=Infinity)
flutter: 1: BoxConstraints(w=428.0, 0.0<=h<=Infinity)
flutter: 2: BoxConstraints(w=428.0, 0.0<=h<=Infinity)
...
可以发现,列表不知道列表项的具体高度,高度约束变为 0.0
到 Infinity
。
ListView
内部组合了 Scrollable
、Viewport
和 Sliver
,需要注意:
ListView
中的列表项组件都是 RenderBox
,并不是 Sliver, 这个一定要注意。
一个 ListView
中只有一个Sliver
,对列表项进行按需加载的逻辑是 Sliver
中实现的。
ListView
的 Sliver
默认是 SliverList
,如果指定了 itemExtent
,则会使用SliverFixedExtentList
;如果 prototypeItem
属性不为空,则会使用 SliverPrototypeExtentList
,无论是是哪个,都实现了子组件的按需加载模型。
假设我们要从数据源异步分批拉取一些数据,然后用ListView展示,当我们滑动到列表末尾时,判断是否需要再去拉取数据,如果是,则去拉取,拉取过程中在表尾显示一个loading,拉取成功后将数据插入列表;如果不需要再去拉取,则在表尾提示"没有更多"。
代码如下:
import 'package:flutter/material.dart'; import 'package:english_words/english_words.dart'; import 'package:flutter/rendering.dart'; class InfiniteListView extends StatefulWidget { @override _InfiniteListViewState createState() => _InfiniteListViewState(); } class _InfiniteListViewState extends State<InfiniteListView> { static const loadingTag = "##loading##"; //表尾标记 var _words = <String>[loadingTag]; @override void initState() { super.initState(); _retrieveData(); } @override Widget build(BuildContext context) { return ListView.separated( itemCount: _words.length, itemBuilder: (context, index) { if (_words[index] == loadingTag) { // 如果到了表尾 if (_words.length - 1 < 100) { // 不足100条,继续获取数据 _retrieveData(); // 获取数据 return Container( // 显示加载 loading padding: const EdgeInsets.all(16.0), alignment: Alignment.center, child: SizedBox(width: 24.0, height: 24.0, child: CircularProgressIndicator(strokeWidth: 2.0), ), ); } else { // 已经加载了100条数据,不再获取数据。 return Container( alignment: Alignment.center, padding: EdgeInsets.all(16.0), child: Text("没有更多了",style: TextStyle(color: Colors.grey)), ); } } return ListTile(title: Text(_words[index])); // 显示单词列表项 }, separatorBuilder: (context, index) => Divider(height: .0), ); } // 模拟网络请求 void _retrieveData() { Future.delayed(Duration(seconds: 2)).then((e) { setState(() { // 重新构建列表 _words.insertAll( _words.length - 1, // 每次插入到 loadingTag 之前 generateWordPairs().take(20).map((e) => e.asPascalCase).toList(), // 每次生成20个单词 ); }); }); } }
效果:
很多时候我们需要给列表添加一个固定表头,比如我们想实现一个商品列表,需要在列表顶部添加一个“商品列表”标题,期望的效果如图所示:
我们按照之前经验,写出如下代码:
@override
Widget build(BuildContext context) {
return Column(children: <Widget>[
ListTile(title:Text("商品列表")),
ListView.builder(itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text("$index"));
}),
]);
}
然后运行,发现并没有出现我们期望的效果,相反触发了一个异常;
Error caught by rendering library, thrown during performResize()。
Vertical viewport was given unbounded height ...
从异常信息中我们可以看到是因为ListView
高度边界无法确定引起,所以解决的办法也很明显,我们需要给ListView
指定边界,我们通过SizedBox
指定一个列表高度看看是否生效:
... //省略无关代码
SizedBox(
height: 400, // 指定列表高度为400
child: ListView.builder(
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text("$index"));
},
),
),
...
效果:
可以看到,现在没有触发异常并且列表已经显示出来了,但是我们的手机屏幕高度要大于 400
,所以底部会有一些空白。那如果我们要实现列表铺满除表头以外的屏幕空间应该怎么做?直观的方法是我们去动态计算,用屏幕高度减去状态栏、导航栏、表头的高度即为剩余屏幕高度,代码如下:
... //省略无关代码
SizedBox(
// Material设计规范中状态栏、导航栏、ListTile高度分别为24、56、56
height: MediaQuery.of(context).size.height - 24 - 56 - 56,
child: ListView.builder(itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text("$index"));
}),
)
...
效果:
可以看到,我们期望的效果实现了,但是这种方法并不优雅,如果页面布局发生变化,比如表头布局调整导致表头高度改变,那么剩余空间的高度就得重新计算。
那么有什么方法可以自动拉伸ListView
以填充屏幕剩余空间的方法吗?当然有!答案就是Flex
。前面已经介绍过在弹性布局中,可以使用Expanded
自动拉伸组件大小,并且我们也说过Column
是继承自Flex
的,所以我们可以直接使用Column + Expanded
来实现,代码如下:
@override
Widget build(BuildContext context) {
return Column(children: <Widget>[
ListTile(title:Text("商品列表")),
Expanded(
child: ListView.builder(itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text("$index"));
}),
),
]);
}
Expanded
的 flex
参数默认为 1
,所以上面代码中,Expanded
会占满 Column
中除 ListTile
以外的剩余空间,运行效果跟开头期望的图一致。
它需要实现一个SliverChildDelegate
用来给 ListView
生成列表项组件,通常很少使用,下面是 API 文档中的一个示例:
class MyListView extends StatefulWidget { const MyListView({ super.key}); @override State<MyListView> createState() => _MyListViewState(); } class _MyListViewState extends State<MyListView> { List<String> items = <String>['1', '2', '3', '4', '5']; void _reverse() { setState(() { items = items.reversed.toList(); }); } @override Widget build(BuildContext context) { return Scaffold( body: SafeArea( child: ListView.custom( childrenDelegate: SliverChildBuilderDelegate( (BuildContext context, int index) { return KeepAlive( data: items[index], key: ValueKey<String>(items[index]), ); }, childCount: items.length, findChildIndexCallback: (Key key) { final ValueKey<String> valueKey = key as ValueKey<String>; final String data = valueKey.value; return items.indexOf(data); }), ), ), bottomNavigationBar: BottomAppBar( child: Row( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ TextButton( onPressed: () => _reverse(), child: const Text('Reverse items'), ), ], ), ), ); } } class KeepAlive extends StatefulWidget { const KeepAlive({ required Key key, required this.data, }) : super(key: key); final String data; @override State<KeepAlive> createState() => _KeepAliveState(); } class _KeepAliveState extends State<KeepAlive> with AutomaticKeepAliveClientMixin { @override bool get wantKeepAlive => true; @override Widget build(BuildContext context) { super.build(context); return Text(widget.data); } }
这个 ListView
使用自定义 SilverChildBuilderDelegate
来支持子元素的重排序。
AnimatedList
和 ListView
的功能大体相似,不同的是, AnimatedList
可以在列表中插入或删除节点时执行一个动画,在需要添加或删除列表项的场景中会提高用户体验。
AnimatedList
是一个 StatefulWidget
,它对应的 State
类型为 AnimatedListState
,添加和删除元素的方法位于 AnimatedListState
中:
void insertItem(int index, {
Duration duration = _kDuration });
void removeItem(int index, AnimatedListRemovedItemBuilder builder, {
Duration duration = _kDuration }) ;
下面我们看一个示例:实现下面这样的一个列表,点击底部 + 按钮时向列表追加一个列表项;点击每个列表项后面的删除按钮时,删除该列表项,添加和删除时分别执行指定的动画,运行效果如图所示:
初始的时候有5个列表项,先点击了 + 号按钮,会添加一个 6,添加过程执行渐显动画。然后点击了 4 后面的删除按钮,删除的时候执行了一个渐隐+收缩的合成动画。
下面是实现代码:
class AnimatedListRoute extends StatefulWidget { const AnimatedListRoute({ Key? key}) : super(key: key); @override State createState() => _AnimatedListRouteState(); } class _AnimatedListRouteState extends State<AnimatedListRoute> { var data = <String>[]; int counter = 5; final globalKey = GlobalKey<AnimatedListState>(); @override void initState() { for (var i = 0; i < counter; i++) { data.add('${ i + 1}'); } super.initState(); } @override Widget build(BuildContext context) { return Stack( children: [ AnimatedList( key: globalKey, initialItemCount: data.length, itemBuilder: ( BuildContext context, int index, Animation<double> animation, ) { //添加列表项时会执行渐显动画 return FadeTransi
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。