前言: 列表是移动端经常使用的一种视图展示方式,在Flutter中提供了ListView和GridView来实现列表和网格的布局展示
移动端数据量比较大时,我们都是通过列表来展示,比如 商品数据、聊天列表、通讯录、朋友圈等
在IOS中我们可以通过UITableView和UICollectionView来实现
在Flutter中我们也有对应的Widget,那就是ListView
ListView可以沿一个方向(垂直或水平方向,默认是垂直方向)来排列其所有子Widget。
一种最简单的使用方式是直接将所有需要排列的子Widget放在ListView的children属性中即可。
大概的意思: 默认构造函数显式接受List
flutter想要实现滚动效果,必须使用滚动的Widget
ListView({
Key? key,
Axis scrollDirection = Axis.vertical, //设置滑动方向
bool reverse = false, //控制数据显示的顺序
ScrollController? controller,
bool? primary,
ScrollPhysics? physics,
bool shrinkWrap = false,
EdgeInsetsGeometry? padding,
this.itemExtent,// 设置Item的高度,不设置表示默认包裹内容
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
double? cacheExtent,
List<Widget> children = const <Widget>[],//Item
int? semanticChildCount,
...
})
代码演练:
class GYHomeContent extends StatelessWidget {
final TextStyle textStyle = TextStyle(fontSize: 20, color: Colors.redAccent);
@override
Widget build(BuildContext context) {
//实现圆角图片
return ListView(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
child: Text("人的一切痛苦,本质上都是对自己无能的愤怒。", style: textStyle),
color: Colors.orange,
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
child: Text("人活在世界上,不可以有偏差;而且多少要费点劲儿,才能把自己保持到理性的轨道上。",
style: textStyle),color: Colors.green,),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
child: Text("我活在世上,无非想要明白些道理,遇见些有趣的事。", style: textStyle),color: Colors.red,),
),
],
);
}
}
在开发中我们经常看见一种列表:有一个图标或图片(Icon),有一个标题(Title),有一个子标题(Subtitle),还有尾部一个图标(Icon)。
示例代码:
class GYHomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
//实现圆角图片
return ListView(
children: [
ListTile(
leading: Icon(Icons.people, size: 36,),
title: Text("联系人"),
subtitle: Text("联系人信息"),
trailing: Icon(Icons.arrow_forward_ios),
),
ListTile(
leading: Icon(Icons.email, size: 36,),
title: Text("邮箱"),
subtitle: Text("邮箱地址信息"),
trailing: Icon(Icons.arrow_forward_ios),
),
ListTile(
leading: Icon(Icons.message, size: 36,),
title: Text("消息"),
subtitle: Text("消息详情信息"),
trailing: Icon(Icons.arrow_forward_ios),
),
ListTile(
leading: Icon(Icons.map, size: 36,),
title: Text("地址"),
subtitle: Text("地址详情信息"),
trailing: Icon(Icons.arrow_forward_ios),
)
]
);
}
}
代码演练:
class GYHomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
//实现圆角图片
return ListView(
// 100 : 表示需要创建Item的个数, index: Item的下标
children: List.generate(100, (index) {
return ListTile(
leading: Icon(
Icons.people,
size: 36,
),
title: Text("联系人"),
subtitle: Text("联系人信息 ${index + 1}"),
trailing: Icon(Icons.arrow_forward_ios),
);
}));
}
}
ListView默认是垂直方向滚动,我们也可以修改滚动方式,实现水平滚动
class GYHomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
//实现圆角图片
return ListView(
scrollDirection: Axis.horizontal,
// 100 : 表示需要创建Item的个数, index: Item的下标
children: List.generate(100, (index) {
return ListTile(
leading: Icon(
Icons.people,
size: 36,
),
title: Text("联系人"),
// subtitle: Text("联系人信息 ${index + 1}"),
// trailing: Icon(Icons.arrow_forward_ios),
);
}));
}
}
官网描述:
该构造方法创建ListView,适合Item比较多的情况, 并且是在ListView会在真正需要的时候去创建子Widget,而不是一开始就全部初始化好。
ListView.builder该方法有两个重要的参数:
itemBuilder
:列表项创建的方法。当列表滚动到对应位置的时候,ListView会自动调用该方法来创建对应的子Widget。类型是IndexedWidgetBuilder,是一个函数类型itemCount
:表示列表项的数量,如果为空,则表示ListView为无限列表。class GYHomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: 100,
itemExtent: 50,
itemBuilder: (BuildContext ctx, int index) {
return Text(
"Hello World: $index",
style: TextStyle(fontSize: 20),
);
});
}
}
ListView.separated
可以生成列表项之间的分割器,它比ListView.builder
多了一个separatorBuilder
参数,该参数是一个分割器生成器。
ListView.separated
没有参数itemExtent
,所以不能够设置Item的高度, 只能自适应高度示例代码:
class GYHomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView.separated(
itemBuilder: (BuildContext ctx, int index) {
return Text(
"Hello World: $index",
style: TextStyle(fontSize: 20),
);
},
separatorBuilder: (BuildContext ctx, int index){
return Divider(
color: Colors.red,
height: 50, //设置分割线区域高度
thickness: 10,//设置分割线的高度
indent: 15, //设置分割线开始的距离
endIndent: 15//设置分割线结束的距离,
);
},
itemCount: 100
);
}
}
cacheExtent
: 预加载高度,就设置高度越高,可能预加载的Item就越多GridView用于展示多列的展示,在开发中也非常常见,比如直播App中的主播列表、电商中的商品列表等等。
在Flutter中我们可以使用GridView来实现,使用方式和ListView也比较相似。
源码:
GridView({
Key? key,
Axis scrollDirection = Axis.vertical,
bool reverse = false,
ScrollController? controller,
bool? primary,
ScrollPhysics? physics,
bool shrinkWrap = false,
EdgeInsetsGeometry? padding,
required this.gridDelegate, //必须传递一个参数,不能为null
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
double? cacheExtent,
List<Widget> children = const <Widget>[],
int? semanticChildCount,
DragStartBehavior dragStartBehavior = DragStartBehavior.start,
Clip clipBehavior = Clip.hardEdge,
ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
String? restorationId,
})
右上述源码可知,默认默认构造器方法中的参数列表中,有一个必传参数this.gridDelegate
,该参数用于控制交叉轴的item数量或者宽度,参数的类型是SliverGridDelegate
,但是该类是一个抽象类
我们查看该抽象类的子类如下:
除了SliverGridDelegateWithFixedCrossAxisCount
和SliverGridDelegateWithMaxCrossAxisExtent
这两个子类示例外,其它的子类都是_
开头属于私有子类
const SliverGridDelegateWithFixedCrossAxisCount({
required this.crossAxisCount, // 交叉轴的item个数
this.mainAxisSpacing = 0.0, // 主轴的间距
this.crossAxisSpacing = 0.0, // 交叉轴的间距
this.childAspectRatio = 1.0, // 子Widget的宽高比
})
crossAxisCount
:我们只需要提供Item的个数,,然后Fluterr会根据屏幕的宽度自动计算出每个Item的宽度
mainAxisSpacing
: 主轴方向Item和Item之间的距离
crossAxisSpacing
:交叉轴轴方向Item和Item之间的距离
childAspectRatio
: 这里不能够设置Item的明确的宽和高(没有提供相关参数),只能设置该参数来确定Item的宽度
示例代码:
class GYHomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GridView(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
),
children:List.generate(100, (index) {
return Container(
color: Color.fromRGBO(Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1),
child: Center(child: Text("Item:$index",style: TextStyle(fontSize: 24),)),
);
}) ,
);
}
}
源码
const SliverGridDelegateWithMaxCrossAxisExtent({
required this.maxCrossAxisExtent,//交叉轴Item最大的宽度
this.mainAxisSpacing = 0.0,
this.crossAxisSpacing = 0.0,
this.childAspectRatio = 1.0,
this.mainAxisExtent, // 主轴Item最大的高度
})
代码示例:
class GYHomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GridView(
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 200,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
mainAxisExtent: 200
),
children:List.generate(100, (index) {
return Container(
color: Color.fromRGBO(Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1),
child: Center(child: Text("Item:$index",style: TextStyle(fontSize: 24),)),
);
}) ,
);
}
}
maxCrossAxisExten
的值是200
,但是实际的运行结果Item的宽度是肯定没有200的,其实这里maxCrossAxisExten
设置是只是Item的最大宽度,是一个范围值,并不是一个明确的值,所以Flutter会根据你设置的值,可能会调整布局,最终显示的Item的宽可能不是你想要的值。如果你觉得上面配置delegate
比较麻烦,不想使用delegate
,这里有其它的构造方法
GridView.count()
: 构造方法创建相当于设置delegate为SliverGridDelegateWithFixedCrossAxisCount
GridView.extent()
:构造方法相当于设置delegate为SliverGridDelegateWithMaxCrossAxisExtent
和ListView
一样,使用构造函数会一次性创建所有的子Widget
,会带来性能问题,所以我们可以使用GridView.build
来交给GridView
自己管理需要创建的子Widget
。
示例代码:
class GYHomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
),
itemCount: 100,
itemBuilder: (BuildContext ctx, int index) {
return Container(
color: Color.fromRGBO(Random().nextInt(256), Random().nextInt(256),
Random().nextInt(256), 1),
child: Center(
child: Text(
"Item:$index",
style: TextStyle(fontSize: 24),
)),
);
});
}
}
itemCount
: 如果该属性没有设置,那么就是无限滚动,表示有无限个Item我们考虑一个这样的布局:一个滑动的视图中包括一个标题视图(HeaderView),一个列表视图(ListView),一个网格视图(GridView)。
我们怎么可以让它们做到统一的滑动效果呢?使用前面的滚动是很难做到的。
Flutter中有一个可以完成这样滚动效果的Widget:CustomScrollView,可以统一管理多个滚动视图。
在CustomScrollView中,每一个独立的,可滚动的Widget被称之为Sliver。
补充:Sliver可以翻译成裂片、薄片,你可以将每一个独立的滚动视图当做一个小裂片。
通过查看源码我们知道不管是ListView还是GridView都是继承自BoxScrollView
,而BoxScrollView
继承自ScrollView
,而ScrollView
继承自StateLessWidget
之前我们说过,想知道flutter最终渲染的是那个组件,我们可以查看build方法:
我们可以看到本质最终创建一个Scrollable
的组件,然后调用了buildSlivers()
方法获取sliver
,而slivers
里面才是我们真正可以滚动的东西
接下来我们查看buildSlivers()
方法,可以发现该方法是一个抽象方法
@protected
List<Widget> buildSlivers(BuildContext context);
抽象方法子类肯定是需要实现, 那么我们可以查看BoxScrollView
是否实现了该方法:
BoxScrollView
中的buildChildLayout
是一个抽象法方法, ListView和GridView实现了这个抽象方法@override
Widget buildChildLayout(BuildContext context) {
if (itemExtent != null) {
return SliverFixedExtentList(
delegate: childrenDelegate,
itemExtent: itemExtent!,
);
}
return SliverList(delegate: childrenDelegate);
}
@override
Widget buildChildLayout(BuildContext context) {
return SliverGrid(
delegate: childrenDelegate,
gridDelegate: gridDelegate,
);
}
我们发现该方法返回的都是 SliverXXX对象,我们在结合ListView和GridView
的初始化方法
通过初始化方法可以看到 ,ListView
和GridView
本质上其实是创建了对应的sliver对象
当你希望一个列表中既有ListView
式的滚动,又有GridView
的滚动, 那么使用BoxScrollView
是实现不了的, 要想实现这种效果, 那么就需要CustomScrollView
因为我们需要把很多的Sliver
放在一个CustomScrollView
中,所以CustomScrollView
有一个slivers
属性,里面让我们放对应的一些Sliver
:
const CustomScrollView({
Key? key,
Axis scrollDirection = Axis.vertical,
bool reverse = false,
ScrollController? controller,
bool? primary,
ScrollPhysics? physics,
ScrollBehavior? scrollBehavior,
bool shrinkWrap = false,
Key? center,
double anchor = 0.0,
double? cacheExtent,
this.slivers = const <Widget>[], //存放sliver的数组
int? semanticChildCount,
DragStartBehavior dragStartBehavior = DragStartBehavior.start,
ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
String? restorationId,
Clip clipBehavior = Clip.hardEdge,
})
SliverList
:类似于我们之前使用过的ListView
;
SliverFixedExtentList
:类似于SliverList
,只是可以设置滚动的高度;
SliverGrid
:类似于我们之前使用过的GridView
;
SliverPadding
:设置Sliver
的内边距,因为可能要单独给Sliver
设置内边距;
SliverAppBar
:添加一个AppBar
,通常用来作为CustomScrollView
的HeaderView
;
SliverSafeArea
:设置内容显示在安全区域(比如不让齐刘海挡住我们的内容)
使用CustomScrollView和SliverList的方式来实现ListView的效果
SliverList的初始化源码:
const SliverList({
Key? key,
required SliverChildDelegate delegate,
})
SliverChildDelegate
是一个抽象类,有两个子类分别是SliverChildBuilderDelegate
和SliverChildListDelegate
,前者创建就类似ListView.build()
的构造方法,后者类似ListView()
的默认构造方法SliverChildListDelegate
代码演练:class SliverListDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return CustomScrollView(
slivers: [
SliverList(
delegate: SliverChildListDelegate(
List.generate(100, (index) {
return ListTile(
leading: Icon(
Icons.people,
size: 36,
),
title: Text("联系人"),
subtitle: Text("联系人信息 ${index + 1}"),
trailing: Icon(Icons.arrow_forward_ios),
);
}),
))
],
);
}
}
SliverChildBuilderDelegate
的代码演练class SliverListDemo2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return CustomScrollView(
slivers: [
SliverList(
delegate: SliverChildBuilderDelegate((BuildContext ctx, int index) {
return ListTile(
leading: Icon(
Icons.people,
size: 36,
),
title: Text("联系人"),
subtitle: Text("联系人信息 ${index + 1}"),
trailing: Icon(Icons.arrow_forward_ios),
);
}, childCount: 100))
],
);
}
}
代码演练:
class SliverDemo4 extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return CustomScrollView(
slivers: [
SliverFixedExtentList(
delegate: SliverChildBuilderDelegate((BuildContext ctx, int index) {
return ListTile(
leading: Icon(
Icons.people,
size: 36,
),
title: Text("联系人"),
subtitle: Text("联系人信息 ${index + 1}"),
trailing: Icon(Icons.arrow_forward_ios),
);
}),
itemExtent: 100//设置Item的高度
)
],
);
}
}
class SliverDemo3 extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return CustomScrollView(
slivers: [
SliverGrid(
//这里和listView的使用是一样
delegate: SliverChildBuilderDelegate((BuildContext ctx, int int) {
return Container(
color: Color.fromARGB(255, Random().nextInt(256),
Random().nextInt(256), Random().nextInt(256)));
}, childCount: 100),
// 这里除了SliverGridDelegateWithFixedCrossAxisCount 还有 SliverGridDelegateWithMaxCrossAxisExtent
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
))
],
);
}
}
上面提过一个问题,我们希望增加屏幕四周的边距,如果使用Padding
组件的话,顶部的滑动会有一个边距问题,这里使用SliverPadding
可以解决这个问题
这个组件的默认高度就是 AppBar
的高读,如果我们不设置主题内的appBar
属性,使用SliverAppBar
组件我们也会出现一个导航栏
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
// appBar: AppBar(
// title: Text("图片widget"),
// ),
body: CustomScrollView(
slivers: [
SliverAppBar(
expandedHeight: 200, //如果不设置就默认是appBar的高度
title: Text("SliverAppBar"),
pinned: true,
flexibleSpace: FlexibleSpaceBar(
title: Text("Hello World"),
background: Image.asset("images/test.jpeg", fit: BoxFit.cover,),
),
),
SliverPadding(
padding: EdgeInsets.all(20),
sliver: SliverGrid(
//这里和listView的使用是一样
delegate: SliverChildBuilderDelegate((BuildContext ctx, int int) {
return Container(
color: Color.fromARGB(255, Random().nextInt(256),
Random().nextInt(256), Random().nextInt(256)));
}, childCount: 100),
// 这里除了SliverGridDelegateWithFixedCrossAxisCount 还有 SliverGridDelegateWithMaxCrossAxisExtent
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
)),
)
],
)));
}
}
如果我们没有appBar
属性的话,我们的内容可能会被顶部留海挡住,那么怎么解决这个问题,flutter中有一个安全区域组件safeArea
表示在安全区域显示内容
使用safeArea
确实可以使我们的内容不被留海阻挡,但是我们发现,当列表像上滑动的时候,发现内容被安全区域阻挡了。这里Flutter
提供了SliverSafeArea
组件来解决这个问题
SliverSafeArea
: 可以让内容滚到安全区域里面, safeArea
则表示任何东西都不能进入安全区域现在我们有如下需求,我们界面中有一个头部视图、网格列表、水平列表
class SliverDemo6 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CustomScrollView(
slivers: [
//头部视图
SliverAppBar(
expandedHeight: 300,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
title: Text("Hello World"),
background: Image.asset(
"images/test.jpeg",
fit: BoxFit.cover,
),
),
),
//网格视图
SliverGrid(
//这里和listView的使用是一样
delegate: SliverChildBuilderDelegate((BuildContext ctx, int int) {
return Container(
color: Color.fromARGB(255, Random().nextInt(256),
Random().nextInt(256), Random().nextInt(256)));
}, childCount: 6),
// 这里除了SliverGridDelegateWithFixedCrossAxisCount 还有 SliverGridDelegateWithMaxCrossAxisExtent
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
)),
//水平列表
SliverList(
delegate: SliverChildBuilderDelegate((BuildContext ctx, int index) {
return ListTile(
title: Text("联系人"),
subtitle: Text("联系人信息 ${index + 1}"),
trailing: Icon(Icons.arrow_forward_ios),
);
}, childCount: 10))
],
);
}
}
对于滚动的视图,我们经常需要监听它的一些滚动事件,在监听到的时候去做对应的一些事情。
比如视图滚动到底部时,我们可能希望做上拉加载更多;
比如滚动到一定位置时显示一个回到顶部的按钮,点击回到顶部的按钮,回到顶部;
比如监听滚动什么时候开始,什么时候结束;
在Flutter中监听滚动相关的内容由两部分组成:ScrollController和ScrollNotification。
在Flutter中,Widget并不是最终渲染到屏幕上的元素(真正渲染的是RenderObject),因此通常这种监听事件以及相关的信息并不能直接从Widget中获取,而是必须通过对应的Widget的Controller来实现。
ListView、GridView
的组件控制器是ScrollController
,我们可以通过它来获取视图的滚动信息,并且可以调用里面的方法来更新视图的滚动位置。
另外,通常情况下,我们会根据滚动的位置来改变一些Widget的状态信息,所以ScrollController
通常会和StatefulWidget
一起来使用,并且会在其中控制它的初始化、监听、销毁等事件
我们来做一个案例,当滚动到1000位置的时候,显示一个回到顶部的按钮
jumpTo(double offset)
、animateTo(double offset,...)
:这两个方法用于跳转到指定的位置,它们不同之处在于,后者在跳转时会执行一个动画,而前者不会。控制器监听滑动代码示例:
class ScrollViewListenDemo extends StatefulWidget {
const ScrollViewListenDemo({Key? key}) : super(key: key);
@override
_ScrollViewListenDemoState createState() => _ScrollViewListenDemoState();
}
class _ScrollViewListenDemoState extends State<ScrollViewListenDemo> {
//滑动的控制器
ScrollController _scrollController = ScrollController();
//是否显示按钮 ,便宜量大于1000时才显示按钮
bool isShowFolatButton = false;
@override
void initState() {
// TODO: implement initState
//监听滑动
_scrollController.addListener(() {
print("List 在滑动-----${_scrollController.offset}");
setState(() {
isShowFolatButton = _scrollController.offset >= 1000 ;
});
});
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("滑动监听"),
),
body: ListView.builder(
itemBuilder: (BuildContext ctx, int index) {
return Text(
"Item: $index",
style: TextStyle(fontSize: 24),
);
},
itemCount: 100,
itemExtent: 50,
controller: _scrollController,
),
floatingActionButton: isShowFolatButton ? FloatingActionButton(
child: Icon(Icons.arrow_upward),
onPressed: (){
setState(() {
//回到顶端
_scrollController.animateTo(0, duration: Duration(seconds: 1), curve: Curves.easeIn);
});
},
) : null,
);
}
}
如果我们希望监听什么时候开始滚动,什么时候结束滚动,这个时候我们可以通过NotificationListener
。
NotificationListener
是一个Widget,模板参数T是想监听的通知类型,如果省略,则所有类型通知都会被监听,如果指定特定类型,则只有该类型的通知会被监听。NotificationListener
需要一个onNotification
回调函数,用于实现监听处理逻辑。true
时,则冒泡终止,事件停止向上传播,如果不返回或者返回值为false
时,则冒泡继续。案例: 列表滚动, 并且在中间显示滚动进度
class ScrollviewNotificationDemo extends StatefulWidget {
const ScrollviewNotificationDemo({Key? key}) : super(key: key);
@override
_ScrollviewNotificationDemoState createState() => _ScrollviewNotificationDemoState();
}
class _ScrollviewNotificationDemoState extends State<ScrollviewNotificationDemo> {
int _progress = 0;
@override
Widget build(BuildContext context) {
return NotificationListener(
onNotification: (ScrollNotification notification){
if (notification is ScrollStartNotification) {
print("列表开始滑动*****");
} else if (notification is ScrollUpdateNotification) {
print("列表正在滑动中------");
// 当前滚动的位置和总长度
final currentPixel = notification.metrics.pixels;
final totalPixel = notification.metrics.maxScrollExtent;
double progress = currentPixel / totalPixel;
setState(() {
_progress = (progress * 100).toInt();
});
} else if (notification is ScrollEndNotification) {
print("列表滑动结束********");
}
return true;
},
child: Stack(
alignment: Alignment.bottomRight,
children: [
ListView.builder(
itemBuilder: (BuildContext ctx, int index) {
return Text(
"Item: $index",
style: TextStyle(fontSize: 24),
);
},
itemCount: 100,
itemExtent: 50,
),
Positioned(
right: 30,
bottom: 30,
child: CircleAvatar(
radius: 30,
child: Text("$_progress%"),
backgroundColor: Colors.black54,
),
)
],
),
);
}
}