Flutter学习-滚动的Widget
Posted GY-93
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Flutter学习-滚动的Widget相关的知识,希望对你有一定的参考价值。
Flutter学习-滚动的Widget
前言: 列表是移动端经常使用的一种视图展示方式,在Flutter中提供了ListView和GridView来实现列表和网格的布局展示
1. ListView
移动端数据量比较大时,我们都是通过列表来展示,比如 商品数据、聊天列表、通讯录、朋友圈等
在ios中我们可以通过UITableView和UICollectionView来实现
在Flutter中我们也有对应的Widget,那就是ListView
1.1 ListView()初始化
ListView可以沿一个方向(垂直或水平方向,默认是垂直方向)来排列其所有子Widget。
一种最简单的使用方式是直接将所有需要排列的子Widget放在ListView的children属性中即可。
官方文档对默认构造器的描述:
大概的意思: 默认构造函数显式接受List<Widget>的子元素。这个构造函数适用于只有少量子元素的列表视图,因为构造list需要为每个可能显示在列表视图中的子元素工作,而不仅仅是那些实际可见的子元素。(不管在屏幕上是不是可见的,都会把Item创建好,而不是现实Item的时候在创建)
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,
...
})
1.1.1 基础使用
代码演练:
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,),
),
],
);
}
}
效果图:
1.1.2 ListTitle
在开发中我们经常看见一种列表:有一个图标或图片(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),
)
]
);
}
}
1.1.3 List.generate 快速创建Item
代码演练:
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),
);
}));
}
}
1.1.4 滚动方向
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),
);
}));
}
}
1.2 ListView.build() 初始化
官网描述:
该构造方法创建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),
);
});
}
}
1.3 ListView.separated
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就越多
2. GridView
GridView用于展示多列的展示,在开发中也非常常见,比如直播App中的主播列表、电商中的商品列表等等。
在Flutter中我们可以使用GridView来实现,使用方式和ListView也比较相似。
2.1 GridView()
源码:
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
这两个子类示例外,其它的子类都是_
开头属于私有子类
2.1.1 SliverGridDelegateWithFixedCrossAxisCount
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),)),
);
}) ,
);
}
}
2.1.2 SliverGridDelegateWithMaxCrossAxisExtent
源码
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的宽可能不是你想要的值。
2.2 GridView.count、GridView.extent
如果你觉得上面配置delegate
比较麻烦,不想使用delegate
,这里有其它的构造方法
GridView.count()
: 构造方法创建相当于设置delegate为SliverGridDelegateWithFixedCrossAxisCount
GridView.extent()
:构造方法相当于设置delegate为SliverGridDelegateWithMaxCrossAxisExtent
2.3 GridView.build
和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),
)),
);
});
}
}
- 注意:不管是ListView还是GridView,Item的点击需要我们自己添加收拾监听,构造器方法中并没有提供监听的属性(有点坑爹)
itemCount
: 如果该属性没有设置,那么就是无限滚动,表示有无限个Item
2.4 屏幕四周间距问题
- 如果希望两边也有间距的话,我们可以在外层包裹一个
Padding
,但是这里有一个问题,如果设置了上下的距离的话, 表格像上滑动的时候,会出现白色间距的问题
3. Slivers
我们考虑一个这样的布局:一个滑动的视图中包括一个标题视图(HeaderView),一个列表视图(ListView),一个网格视图(GridView)。
我们怎么可以让它们做到统一的滑动效果呢?使用前面的滚动是很难做到的。
Flutter中有一个可以完成这样滚动效果的Widget:CustomScrollView,可以统一管理多个滚动视图。
在CustomScrollView中,每一个独立的,可滚动的Widget被称之为Sliver。
补充:Sliver可以翻译成裂片、薄片,你可以将每一个独立的滚动视图当做一个小裂片。
3.1 ListView、GridView的本质
通过查看源码我们知道不管是ListView还是GridView都是继承自BoxScrollView
,而BoxScrollView
继承自ScrollView
,而ScrollView
继承自StateLessWidget
之前我们说过,想知道flutter最终渲染的是那个组件,我们可以查看build方法:
我们可以看到本质最终创建一个Scrollable
的组件,然后调用了buildSlivers()
方法获取sliver
,而slivers
里面才是我们真正可以滚动的东西
接下来我们查看buildSlivers()
方法,可以发现该方法是一个抽象方法
@protected
List<Widget> buildSlivers(BuildContext context);
抽象方法子类肯定是需要实现, 那么我们可以查看BoxScrollView
是否实现了该方法:
- 通过上述代码我们可以得出一个结论:BoxScrollView只有一个sliver
BoxScrollView
中的buildChildLayout
是一个抽象法方法, ListView和GridView实现了这个抽象方法- ListView该方法代码:
@override
Widget buildChildLayout(BuildContext context) {
if (itemExtent != null) {
return SliverFixedExtentList(
delegate: childrenDelegate,
itemExtent: itemExtent!,
);
}
return SliverList(delegate: childrenDelegate);
}
- GridView的该方法代码:
@override
Widget buildChildLayout(BuildContext context) {
return SliverGrid(
delegate: childrenDelegate,
gridDelegate: gridDelegate,
);
}
我们发现该方法返回的都是 SliverXXX对象,我们在结合ListView和GridView
的初始化方法
通过初始化方法可以看到 ,ListView
和GridView
本质上其实是创建了对应的sliver对象
当你希望一个列表中既有ListView
式的滚动,又有GridView
的滚动, 那么使用BoxScrollView
是实现不了的, 要想实现这种效果, 那么就需要CustomScrollView
3.2 Slivers的基本使用
因为我们需要把很多的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 = ScrollViewKeyboardDismissBehaviorFlutter学习-滚动的Widget