一、ListView组件
在Android中,我们可以使用ListView
或RecyclerView
来实现,在iOS中,我们可以通过UITableView
来实现。
在Flutter中,我们也有对应的列表Widget,就是ListView
。
1.1 ListView基础
ListView的内部继承顺序:
ListView
extendsBoxScrollView
—> extendsScrollView
—> extendsStatelessWidget
在ListView中,有4种构造方法:
-
ListView<Widget>
,适合于具有少量子元素的列表视图 -
ListView.builder
,利用IndexedWidgetBuilder
来按需构造.适合于具有大量子视图的列表视图,构建器只对那些实际可见的子视图调用 -
ListView.separated
,采用两个IndexedWidgetBuilder:itemBuilder
根据需要构建子项separatorBuilder
类似地构建出现在子项之间的分隔符子项。适用于具有固定数量的子控件的列表视图 -
ListView.custom
,使用SliverChildDelegate
构造,它提供了定制子模型的其他方面的能力。 例如,SliverChildDelegate
可以控制用于估计实际上不可见的孩子的大小的算法
1.1.1 ListView<Widget>
ListView可以沿一个方向(垂直或水平方向,默认是垂直方向)来排列其所有子Widget
最简单的使用方式是直接将所有需要排列的子Widget放在ListView的children属性
class MyHomeBody extends StatelessWidget {
final TextStyle textStyle = TextStyle(fontSize: 20, color: Colors.redAccent);
@override
Widget build(BuildContext context) {
return ListView(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: Text("人的一切痛苦,本质上都是对自己无能的愤怒。", style: textStyle),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text("人活在世界上,不可以有偏差;而且多少要费点劲儿,才能把自己保持到理性的轨道上。", style: textStyle),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text("我活在世上,无非想要明白些道理,遇见些有趣的事。", style: textStyle),
)
],
);
}
}
1.1.1.1 ListTile的使用
类似通讯录的列表,我们可以通过ListTile类实现
class ListViewDemo extends StatelessWidget {
const ListViewDemo({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ListView(
scrollDirection: Axis.vertical,
children: List.generate(255, (index) {
return ListTile(
leading: Icon(Icons.favorite),
trailing: Icon(Icons.pets),
title: Text("联系人 ${index + 1}"),
subtitle: Text("联系人电话号码"),
);
})
);
}
}
1.1.1.2 比较重要的属性 scrollDirection、 itemExtent
scrollDirection
:控制列表的滚动方向
itemExtent
:设置每一个item的高度(如果是Axis.horizontal,则为宽度)
reverse
:翻转属性,默认为false(从最底部开始排列)
更多的看源码,尝试。不做过多介绍
通过上面的两个示例,想必你已知晓。默认会创建出所有的childWidget,这样无疑会增加性能的开销. 对于更多数量未知的情况,并不适用
1.1.2 ListView.builder
ListView.builder方法有两个重要的参数:
- itemBuilder(必传) 按需构造
- itemCount 数量
class ListViewBuilderDemo extends StatelessWidget {
const ListViewBuilderDemo({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ListView.builder(
itemBuilder:(BuildContext context, int index){
return ListTile(title: Text("标题$index"), subtitle: Text("详情内容$index"));
},
itemCount: 20,
itemExtent: 30,
);
}
}
1.1.3 ListView.separated
ListView.separated
可以生成列表项之间的分割器,它除了比ListView.builder
多了一个separatorBuilder
参数,该参数是一个分割器生成器
示例:奇数行添加一条蓝色下划线,偶数行添加一条红色下划线
class MySeparatedDemo extends StatelessWidget {
Divider blueColor = Divider(color: Colors.blue);
Divider redColor = Divider(color: Colors.red);
@override
Widget build(BuildContext context) {
return ListView.separated(
itemBuilder: (BuildContext context, int index) {
return ListTile(
leading: Icon(Icons.people),
title: Text("联系人${index+1}"),
subtitle: Text("联系人电话${index+1}"),
);
},
separatorBuilder: (BuildContext context, int index) {
return index % 2 == 0 ? redColor : blueColor;
},
itemCount: 100
);
}
}
示例2:在指定区域内,以Icon为分隔器
class ListViewSeparatedDemo extends StatelessWidget {
const ListViewSeparatedDemo({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
height: 300,
child: ListView.separated(
itemBuilder: (BuildContext ctx, int index) {
return Container(
height: 40,
child: Padding(
padding: const EdgeInsets.only(left: 20),
child: Text(
"helloworld $index",
style: TextStyle(fontSize: 20),
),
),
);
},
separatorBuilder: (BuildContext ctx, int index) {
return Icon(Icons.pets,size: 40,);
},
itemCount: 100,
),
);
}
}
二、GridView组件
在iOS中,我们可以通过UICollectionView
来实现多列。在Flutter中也有对应的列表Widget,就是GridView
,使用方式和ListView也比较相似。
2.1 GridView基础
GridView的内部继承顺序:
GridView
extendsBoxScrollView
—> extendsScrollView
—> extendsStatelessWidget
可以对比得知,GridView与ListView继承于BoxScrollView,所以在很多方面二者是极其相似的
在GridView中,有4种构造方法:
-
GridView<Widget>
,相对于ListView多gridDelegate
这个非常特殊的参数 -
GridView.count
,GridView.extent
(类比上面,可以不用设置delegate) -
GridView.builder
, -
GridView.custom
,
2.1.1 GridView<Widget>
gridDelegate
:控制交叉轴的item数量或者宽度,需要传入的类型是SliverGridDelegate
SliverGridDelegate
是一个抽象类,我们找到它的两个子类:
-
SliverGridDelegateWithFixedCrossAxisCount
控制交叉轴的item数量 -
SliverGridDelegateWithMaxCrossAxisExtent
控制交叉轴的item的最大宽度
SliverGridDelegateWithFixedCrossAxisCount
:包含参数
@required this.crossAxisCount,//
this.mainAxisSpacing = 0.0,
this.crossAxisSpacing = 0.0,
this.childAspectRatio = 1.0,
SliverGridDelegateWithMaxCrossAxisExtent
:包含参数
@required this.maxCrossAxisExtent,
this.mainAxisSpacing = 0.0,
this.crossAxisSpacing = 0.0,
this.childAspectRatio = 1.0,
代码演示:
//SliverGridDelegateWithMaxCrossAxisExtent示例
class GridViewDelegateDemo extends StatelessWidget {
const GridViewDelegateDemo({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
child: GridView(
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 200,
childAspectRatio: 1.5,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
),
children: List.generate(100, (index) {
return Container(
color: Color.fromARGB(255, Random().nextInt(256),
Random().nextInt(256), Random().nextInt(256)),
);
}),
),
);
}
}
//SliverGridDelegateWithFixedCrossAxisCount示例
class GridViewDemo extends StatelessWidget {
const GridViewDemo({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 5),
child: GridView(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 15 / 9.0,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
),
children: List.generate(100, (index) {
return Container(
color: Color.fromARGB(255, Random().nextInt(256),
Random().nextInt(256), Random().nextInt(256)),
);
}),
),
);
}
}
2.1.2 GridView.count, GridView.extent
👆上面这两个构造函数,有这对应的简写方式,即GridView.count
, GridView.extent
构造函数内部实现了对应的delegate
没有什么好讲的,直接上代码:
class GridViewCountDemo extends StatelessWidget {
const GridViewCountDemo({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
child: GridView.count(
crossAxisCount: 3,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
childAspectRatio: 0.9,
children: List.generate(100, (index) {
return Container(
color: Color.fromARGB(255, Random().nextInt(256),
Random().nextInt(256), Random().nextInt(256)),
);
}),
),
);
}
}
class GridViewExtentDemo extends StatelessWidget {
const GridViewExtentDemo({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
child: GridView.extent(
maxCrossAxisExtent: 200,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
childAspectRatio: 0.9,
children: List.generate(100, (index) {
return Container(
color: Color.fromARGB(255, Random().nextInt(256),
Random().nextInt(256), Random().nextInt(256)),
);
}),
));
}
}
2.1.3 GridView.builder
类似ListView.builder,可以使用GridView.build
来交给GridView自己管理需要创建的子Widget,降低性能消耗
class GrideViewBuilderDemo extends StatelessWidget {
const GrideViewBuilderDemo({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
),
itemBuilder: (BuildContext ctx, int index) {
return Container(
height: 40,
child: Padding(
padding: const EdgeInsets.only(left: 20),
child: Text(
"helloworld $index",
style: TextStyle(fontSize: 20),
),
),
);
});
}
}
2.1.4 GridView.custom
在源码中,我们可以看到上面的构造方法,设置了SliverChildListDelegate,而GridView.custom
则是需要自己去设置
class GrideViewCustomDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GridView.custom(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
),
childrenDelegate: SliverChildListDelegate(
List.generate(100, (index) {
return Container(
color: Color.fromARGB(255, Random().nextInt(256),
Random().nextInt(256), Random().nextInt(256))
);
}),
addAutomaticKeepAlives: false,
),
);
}
}
三、Slivers(裂片)
设想一下平常很常见的视图布局:一个滑动的视图中包括一个标题视图(HeaderView),一个列表视图(ListView),一个网格视图(GridView),如何让它们做到统一滑动呢?
Flutter中有一个可以完成这样滚动效果的Widget:CustomScrollView
,可以统一管理多个滚动视图。
在CustomScrollView
中,每一个独立的,可滚动的Widget被称之为Sliver。
3.1 Slivers的使用
需要通过CustomScrollView
来管理Slivers通过slivers
属性,放数量不定的Sliver:
Sliver的种类:
-
SliverList
:类似于我们之前使用过的ListView; -
SliverGrid
:类似于我们之前使用过的GridView; -
SliverFixedExtentList
:类似于SliverList,只是可以设置item的高度; -
SliverAppBar
:添加一个AppBar,包裹Slive,作为CustomScrollView的HeaderView;
给Sliver修改一些显示区域布局:
-
SliverPadding
:包裹Slive,设置Sliver的内边距; -
SliverSafeArea
:包裹Slive,设置内容显示安全区域(比如不让齐刘海挡住我们的内容)
示例:
class SliverSingleDemo extends StatelessWidget {
const SliverSingleDemo({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return CustomScrollView(
slivers: [
SliverSafeArea(
sliver: SliverPadding(
padding: EdgeInsets.only(top: 10,left: 10,right: 10),
sliver: SliverGrid(
delegate: SliverChildBuilderDelegate(
(BuildContext ctx, int index) {
return Container(
height: 40,
color: Color.fromARGB(255, Random().nextInt(256),
Random().nextInt(256), Random().nextInt(256)),
child: Padding(
padding: const EdgeInsets.only(left: 20),
child: Text(
"helloworld $index",
style: TextStyle(fontSize: 20),
),
),
);
},
childCount: 100,
),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisSpacing: 10,
mainAxisSpacing: 10,
crossAxisCount: 5,
childAspectRatio: 1.5),
),
),
)
],
);
}
}
示例:SliverAppBar + SliverGrid + SliverFixedExtentList + SliverPadding+SliverSafeArea
class MutiSliverDemo extends StatelessWidget {
const MutiSliverDemo({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return CustomScrollView(
slivers: [
SliverAppBar(
pinned: true,//悬停效果
expandedHeight: 300,//高度
flexibleSpace: FlexibleSpaceBar(//灵活的headview
title: Text('Sliver demo'),
background: Image.network('https://tva1.sinaimg.cn/large/006y8mN6gy1g72j6nk1d4j30u00k0n0j.jpg',fit: BoxFit.cover,),
),
),
SliverGrid(delegate: SliverChildBuilderDelegate(
(BuildContext ctx, int index) {
return Container(
alignment: Alignment.center,
color: Colors.teal[100 * (index % 9)],
/*color: Color.fromARGB(255, Random().nextInt(256),
Random().nextInt(256), Random().nextInt(256)),*/
child: new Text('grid item $index'),
);
},
childCount: 10,
), gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 200.0,
mainAxisSpacing: 10.0,
crossAxisSpacing: 10.0,
childAspectRatio: 4.0,),
),
SliverFixedExtentList(
itemExtent: 50,
delegate: SliverChildBuilderDelegate(
(BuildContext ctx, int index) {
return Container(
alignment: Alignment.center,
color: Colors.lightBlue[100 * (index % 9)],
child: new Text('list item $index'),
);
},
childCount: 20,
))
],
);
}
}
SliverAppBar有很多属性,有时间可以自己查看源码及官方文档,尝试一下
四、监听滚动事件
在Flutter中监听滚动相关的内容由两部分组成:ScrollController
和ScrollNotification
。
4.1 ScrollController
- 在Flutter中,Widget并不是最终渲染到屏幕上的元素(渲染的是RenderObject),因此通常这种监听事件以及相关的信息并不能直接从Widget中获取,而是必须通过对应的Widget的Controller来实现。
- 通常情况下,根据滚动的位置来改变一些Widget的状态信息,ScrollController通常会和StatefulWidget一起来使用,并且会在其中控制它的初始化、监听、销毁等事件。
- ScrollController间接继承自Listenable,我们可以根据ScrollController来监听滚动事件。
- 手动设置offset通过以下两个方法:
-
jumpTo(double offset)
、animateTo(double offset,...)
:这两个方法用于跳转到指定的位置,不同之处: 后者在跳转时会执行一个动画,而前者不会。
案例1:当滚动到1000位置的时候,显示一个回到顶部的按钮
class ZQHomePage extends StatefulWidget {
@override
_ZQHomePageState createState() => _ZQHomePageState();
}
class _ZQHomePageState extends State<ZQHomePage> {
ScrollController _controller = ScrollController(initialScrollOffset: 300);
bool _isShowFloatButton = false;
@override
void initState() {
super.initState();
_controller.addListener(() {
print("监听滚动。。。。${_controller.offset}");
setState(() {
_isShowFloatButton = _controller.offset >= 1000;
});
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('列表测试'),
),
body: ListView.builder(
controller: _controller,
itemBuilder: (BuildContext ctx, int index) {
return ListTile(
leading: Icon(Icons.pets),
title: Text("联系人$index"),
);
},
itemCount: 300,
),
floatingActionButton: _isShowFloatButton ? FloatingActionButton(
child: Icon(Icons.arrow_upward),
onPressed: () {
//controller.jumpTo(0);
_controller.animateTo(0, duration: Duration(milliseconds: 300), curve: Curves.easeIn);
},
) : null,
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
floatingActionButtonAnimator: FloatingActionButtonAnimator.scaling,
);
}
4.2 NotificationListener
通过NotificationListener
,可以监听什么时候开始滚动,什么时候结束滚动
-
NotificationListener
是一个Widget,模板参数T是想监听的通知类型,如果省略,则所有类型通知都会被监听,如果指定特定类型,则只有该类型的通知会被监听。 -
NotificationListener
需要一个onNotification
回调函数,用于实现监听处理逻辑。 - 该回调可以返回一个布尔值,代表是否阻止该事件继续向上冒泡,如果为
true
时,则冒泡终止,事件停止向上传播,如果不返回或者返回值为false
时,则冒泡继续。
案例: 列表滚动, 并且在中间显示滚动进度
class ZQNewHomePage extends StatefulWidget {
@override
_ZQNewHomePageState createState() => _ZQNewHomePageState();
}
class _ZQNewHomePageState extends State<ZQNewHomePage> {
ScrollController _controller = ScrollController(initialScrollOffset: 300);
bool _isShowFloatButton = false;
int _progress = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('列表测试'),
),
body: NotificationListener(
onNotification: (ScrollNotification notification){
if(notification is ScrollStartNotification){
print("开始滚动..");
}else if(notification is ScrollEndNotification){
print("结束滚动..");
}else if(notification is ScrollUpdateNotification){
print("正在滚动..");
// 当前滚动的位置和总长度
final currentPixel = notification.metrics.pixels;
final totalPixel = notification.metrics.maxScrollExtent;
double progress = currentPixel / totalPixel;
setState(() {
_isShowFloatButton = notification.metrics.pixels >= 1000;
_progress = (progress * 100).toInt();
});
print("当前滚动位置:${notification.metrics.pixels}");
print("总滚动位置:${notification.metrics.maxScrollExtent}");
}
return true;
},
child: Stack(
alignment: Alignment.center,
children:[
ListView.builder(
controller: _controller,
itemBuilder: (BuildContext ctx, int index) {
return ListTile(
leading: Icon(Icons.pets),
title: Text("联系人$index"),
);
},
itemCount: 300,
),
CircleAvatar(
radius: 30,
child: Text("$_progress%"),
backgroundColor: Colors.black54,
)
],
),
),
floatingActionButton: _isShowFloatButton ? FloatingActionButton(
child: Icon(Icons.arrow_upward),
onPressed: () {
//controller.jumpTo(0);
_controller.animateTo(0, duration: Duration(milliseconds: 300), curve: Curves.easeIn);
},
) : null,
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
floatingActionButtonAnimator: FloatingActionButtonAnimator.scaling,
);
}
@override
void dispose() {
// TODO: implement dispose
super.dispose();
_controller.dispose();
}
}