Flutter开发之滚动Widget
Posted 梦想家-mxj
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Flutter开发之滚动Widget相关的知识,希望对你有一定的参考价值。
移动端数据量比较大时,我们都是通过列表来进行展示的,比如商品数据、聊天列表、通信录、朋友圈等。
在android中,我们可以使用ListView或RecyclerView来实现,在ios中,我们可以通过UITableView来实现。
在Flutter中,我们也有对应的列表Widget,就是ListView。
一、ListView
1.1、ListView的基本使用
ListView可以沿一个方向(垂直或水平方向,默认是垂直方向)来排列其所有子Widget。
一种最简单的使用方式是直接将所有需要排列的子Widget放在ListView的children属性中即可。
我们直接使用ListView进行演练:
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.2、ListTitle的使用
在开发中,我们经常见到一种列表,有一个图标或图片(Icon),有一个标题(Title),有一个子标题(Subtitle),还有尾部一个图标(Icon)。
我们使用ListTitle来实现:
class MyHomeBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView(
children: <Widget>[
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.3、垂直方向滚动
我们可以通过设置 scrollDirection 参数来控制视图的滚动方向。
我们通过下面的代码实现一个水平滚动的内容:
class MyHomeBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView(
scrollDirection: Axis.horizontal,
itemExtent: 200,
children: <Widget>[
Container(color: Colors.red, width: 200),
Container(color: Colors.green, width: 200),
Container(color: Colors.blue, width: 200),
Container(color: Colors.purple, width: 200),
Container(color: Colors.orange, width: 200),
],
);
}
}
需要注意的是,我们给Container设置width,否则它没有宽度,不能正常显示;我们给ListView设置一个itemExtent,该属性会设置滚动方向上每个item所占据的宽度。
2.1、ListView.build
通过构造函数中的children传入所有的子Widget有一个问题:默认会创建出所有的子Widget。
但是对于用户来说,一次性构建出所有的Widget并不会有什么差异,但是对于我们的程序来说会产生性能问题,而且会增加首屏的渲染时间。
我们可以ListView.build来构建子Widget,提供性能。
2.1.1、ListView.build基本使用
ListView.build适用于子Widget比较多的场景,该构造函数将创建子Widget交给了一个抽象的方法,交给ListView进行管理,ListView会在真正需要的时候去创建子Widget,而不是一开始就全部初始化好。
ListView.build方法有两个重要参数:
- itemBuilder:列表项创建的方法。当列表滚动到对应位置的时候,ListView会自动调用该方法来创建对应的子Widget。类型是IndexedWidgetBuilder,是一个函数类型。
- itemCount:表示列表项的数量,如果为空,则表示ListView为无限列表。
class MyHomeBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: 100,
itemExtent: 80,
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text("标题$index"), subtitle: Text("详情内容$index"));
}
);
}
}
2.1.2、ListView.build动态数据
之前搞了一个test.json文件,里面的数据如下:
{
"status": "0000",
"message": "success",
"data": {
"title": {
"id": "001",
"name" : "蔬菜"
},
"content": [
{
"id": "001",
"value":"白菜"
},
{
"id": "002",
"value":"萝卜"
},
{
"id": "003",
"value":"黄瓜"
},
{
"id": "004",
"value":"西红柿"
},
{
"id": "005",
"value":"茄子"
},
{
"id": "006",
"value":"冬瓜"
}
]
}
}
又写了一个model:
import 'dart:async' show Future;
import 'package:flutter/services.dart' show rootBundle;
import 'dart:convert';
class ContentModel{
String status;
String message;
Data data;
ContentModel.fromMap(Map<String,dynamic> json){
this.status = json["status"];
this.message = json["message"];
var data = json["data"];
this.data = Data.fromMap(data["title"],data['content']);
}
}
Future<List<contentItem>> getContents() async{
var jsonString = await rootBundle.loadString('json/test.json');
final jsonResult = json.decode(jsonString);
Map map = new Map<String, dynamic>.from(jsonResult);
var data = jsonResult["data"];
List<contentItem> contents = new List();
for(Map<String,dynamic>map in data["content"]){
contents.add(contentItem.fromMap(map));
}
return contents;
}
class Data{
Datatitle title;
List <contentItem> contents;
Data.fromMap(Map<String,dynamic> title,List<dynamic> content){
this.title = Datatitle.fromMap(title);
var contents = content;
this.contents = content.map((item) {
return contentItem.fromMap(item);
}).toList();
}
}
class Datatitle{
String id;
String name;
Datatitle.fromMap(Map<String,dynamic>title){
this.id = title['id'];
this.name = title['name'];
}
}
class contentItem{
String id;
String value;
contentItem.fromMap(Map<String,dynamic>title){
this.id = title['id'];
this.value = title['value'];
}
}
现在我们用json数据来展示一个列表。
现在我们进行一个思考🤔:是否依然使用StatelessWidget?
显然不可以的,因为当前我们的数据是异步加载的,刚开始界面并不会展示数据(没有数据),后面从JSON中加载出来数据(有数据)后,再次展示加载的数据(这里是有状态的变化的,从无数据,到有数据的变化)。因此,我们需要使用StatefulWidget来管理组件
具体代码:
class MyHomeBody8 extends StatefulWidget{
@override
State<StatefulWidget> createState() {
return MyHomeBodyState();
}
}
class MyHomeBodyState extends State<MyHomeBody8> {
List<contentItem> contents = [];
// 在初始化状态的方法中加载数据
@override
void initState() {
getContents().then((contents) {
print('获取的数据 : $contents');
setState(() {
this.contents = contents;
});
});
super.initState();
}
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: contents.length,
itemBuilder: (BuildContext context, int index) {
return Padding(
padding: EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
ListTile(
leading: Icon(Icons.message, size: 36,),
title: Text(contents[index].id),
subtitle: Text(contents[index].value),
),
],
),
);
},
);
}
}
效果图如下:
2.2 、 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
);
}
}
效果图如下:
二、 GridView组件
GridView组件用于展示多列数据,在开发中也比较常见,比如直播APP中主播列表、电商APP中的商品列表等。
在flutter中我们可以使用GridView来实现,使用方式和ListView也比较相似。
1、 GridView构造函数
一种使用GridView的方式就是使用构造函数来创建,和ListView对比有一个特殊的参数:gridDelegate。
gridDelegate用于控制交叉轴的item数量或者宽度,需要传入的类型是SliverGridDelegate,但是它是一个抽象类,所以我们需要传入它的子类:
SliverGridDelegateWithFixedCrossAxisCount
SliverGridDelegateWithMaxCrossAxisExtent
SliverGridDelegateWithFixedCrossAxisCount
SliverGridDelegateWithFixedCrossAxisCount({
@requireddouble crossAxisCount, // 交叉轴的item个数
double mainAxisSpacing = 0.0, // 主轴的间距
double crossAxisSpacing = 0.0, // 交叉轴的间距
double childAspectRatio = 1.0, // 子Widget的宽高比
})
一个实例演练:
class MyGridCountDemo extends StatelessWidget {
List<Widget> getGridWidgets() {
return List.generate(20, (index) {
return Container(
color: Colors.cyan,
alignment: Alignment(0, 0),
child: Text("item$index", style: TextStyle(fontSize: 20, color: Colors.white)),
);
});
}
@override
Widget build(BuildContext context) {
return GridView(
padding: EdgeInsets.all(10),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
childAspectRatio: 1.0
),
children: getGridWidgets(),
);
}
}
效果如下:
SliverGridDelegateWithMaxCrossAxisExtent
SliverGridDelegateWithMaxCrossAxisExtent({
double maxCrossAxisExtent, // 交叉轴的item宽度
double mainAxisSpacing = 0.0, // 主轴的间距
double crossAxisSpacing = 0.0, // 交叉轴的间距
double childAspectRatio = 1.0, // 子Widget的宽高比
})
一个实例演练
class MyGridExtentDemo extends StatelessWidget {
List<Widget> getGridWidgets() {
return List.generate(20, (index) {
return Container(
color: Colors.deepPurple,
alignment: Alignment(0, 0),
child: Text("item$index", style: TextStyle(fontSize: 20, color: Colors.white)),
);
});
}
@override
Widget build(BuildContext context) {
return GridView(
padding: EdgeInsets.all(10),
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 200,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
childAspectRatio: 1.0
),
children: getGridWidgets(),
);
}
}
效果如下:
前面两种方式也可以不设置delegate
可以分别使用:GridView.count构造函数和GridView.extent构造函数实现相同的效果。
使用GridView.count构造函数
class MyGridCountDemo1 extends StatelessWidget {
List<Widget> getGridWidgets() {
return List.generate(20, (index) {
return Container(
color: Colors.cyan,
alignment: Alignment(0, 0),
child: Text("item$index", style: TextStyle(fontSize: 20, color: Colors.white)),
);
});
}
@override
Widget build(BuildContext context) {
return GridView.count(padding: EdgeInsets.all(10),crossAxisCount: 3,mainAxisSpacing: 10,crossAxisSpacing: 10,childAspectRatio: 1.0,children:
getGridWidgets()
,);
}
}
其效果与SliverGridDelegateWithFixedCrossAxisCount一致。
使用GridView.extent构造函数
class MyGridExtentDemo1 extends StatelessWidget {
List<Widget> getGridWidgets() {
return List.generate(20, (index) {
return Container(
color: Colors.deepPurple,
alignment: Alignment(0, 0),
child: Text("item$index", style: TextStyle(fontSize: 20, color: Colors.white)),
);
});
}
@override
Widget build(BuildContext context) {
return GridView.extent(
padding: EdgeInsets.all(10),
maxCrossAxisExtent: 200,
crossAxisSpacing: 10,
childAspectRatio: 1.0,
mainAxisSpacing: 10,
children: getGridWidgets(),
);
}
}
其效果与SliverGridDelegateWithMaxCrossAxisExtent一致。
2、GridView.build
和ListView一样,使用构造函数会一次性创建所有的子Widget,会带来性能问题,所以我们可以使用GridView.build来交给GridView自己管理需要创建的子Widget。
和上面的ListView.build一样:
class MyHomeBodyGridViewBuild extends StatefulWidget{
@override
State<StatefulWidget> createState() {
return _GridViewBuildDemoState();
}
}
class _GridViewBuildDemoState extends State<MyHomeBodyGridViewBuild> {
List<contentItem> contents = [];
// 在初始化状态的方法中加载数据
@override
void initState() {
getContents().then((contents) {
print('获取的数据 : $contents');
setState(() {
this.contents = contents;
});
});
super.initState();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: GridView.builder(
shrinkWrap: true,
physics: ClampingScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 1,
crossAxisSpacing: 10,
childAspectRatio: 1.5
),
itemCount: contents.length,
itemBuilder: (BuildContext context, int index) {
return Container(
child: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(
color: Colors.cyanAccent,
child: ListTile(
leading: Icon(Icons.message, size: 36,),
title: Text(contents[index].id),
subtitle: Text(contents[index].value),
),
)
],
),
);
}
),
);
}
}
具体效果如下:
三、Slivers组件
我们考虑一个这样的布局:一个滑动的视图中包括一个标题视图(HeaderView),一个列表视图(ListView),一个网格视图(GridView)。我们怎么可以让它们做到统一的滑动效果呢?使用前面的滚动是很难做到的。Flutter中有一个可以完成这样滚动效果的Widget:CustomScrollView,可以统一管理多个滚动视图。
在CustomScrollView中,每一个独立的,可滚动的Widget被称之为Sliver。Sliver可以翻译成裂片、薄片,你可以将每一个独立的滚动视图当做一个小裂片。
1、Slivers的基本使用
因为我们需要把很多的Sliver放在一个CustomScrollView中,所以CustomScrollView有一个slivers属性,里面让我们放对应的一些Sliver:
- SliverList:类似于我们之前使用过的ListView;
- SliverFixedExtentList:类似于SliverList只是可以设置滚动的高度;
- SliverGrid:类似于我们之前使用过的GridView;
- SliverPadding:设置Sliver的内边距,因为可能要单独给Sliver设置内边距;
- SliverAppBar:添加一个AppBar,通常用来作为CustomScrollView的HeaderView;
- SliverSafeArea:设置内容显示在安全区域(比如不让齐刘海挡住我们的内容)
我们简单演示一下:SliverGrid+SliverPadding+SliverSafeArea的组合
class HomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CustomScrollView(
slivers: <Widget>[
SliverSafeArea(
sliver: SliverPadding(
padding: EdgeInsets.all(8),
sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
alignment: Alignment(0, 0),
color: Colors.orange,
child: Text("item$index"),
);
},
childCount: 20
),
),
),
)
],
);
}
}
效果图如下:
2、Slivers的组合使用
将SliverAppBar+SliverGrid+SliverFixedExtentList做出如下界面:
class HomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return showCustomScrollView();
}
Widget showCustomScrollView() {
return new CustomScrollView(
slivers: <Widget>[
const SliverAppBar(
expandedHeight: 250.0,
flexibleSpace: FlexibleSpaceBar(
title: Text('Slivers'),
background: Image(
image: NetworkImage(
"https://tva1.sinaimg.cn/large/006y8mN6gy1g72j6nk1d4j30u00k0n0j.jpg",
),
fit: BoxFit.cover,
),
),
),
new SliverGrid(
gridDelegate: new SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 200.0,
mainAxisSpacing: 10.0,
crossAxisSpacing: 10.0,
childAspectRatio: 4.0,
),
delegate: new SliverChildBuilderDelegate(
(BuildContext context, int index) {
return new Container(
alignment: Alignment.center,
color: Colors.teal[100 * (index % 9)],
child: new Text('grid item $index'),
);
},
childCount: 5,
),
),
SliverFixedExtentList(
itemExtent: 50.0,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return new Container(
alignment: Alignment.center,
color: Colors.lightBlue[100 * (index % 9)],
child: new Text('list item $index'),
);
},
childCount: 10
),
),
],
);
}
}
效果如下:
四、监听滚动事件
对于滚动的视图,我们经常需要监听它的一些滚动事件,在监听到的时候去做对应的一些事情。比如视图滚动到底部时,我们可能希望做上拉加载更多;比如滚动到一定位置时显示一个回到顶部的按钮,点击回到顶部的按钮,回到顶部;比如监听滚动什么时候开始,什么时候结束。
在Flutter中监听滚动相关的内容由两部分组成:ScrollController和ScrollNotification。
1、ScrollController
在Flutter中,Widget并不是最终渲染到屏幕上的元素(真正渲染的是RenderObject),因此通常这种监听事件以及相关的信息并不能直接从Widget中获取,而是必须通过对应的Widget的Controller来实现。
ListView、GridView的组件控制器是ScrollController,我们可以通过它来获取视图的滚动信息,并且可以调用里面的方法来更新视图的滚动位置。
另外,通常情况下,我们会根据滚动的位置来改变一些Widget的状态信息,所以ScrollController通常会和StatefulWidget一起来使用,并且会在其中控制它的初始化、监听、销毁等事件。
我们来做一个案例,当滚动到1000位置的时候,显示一个回到顶部的按钮:
- jumpTo(double offset)、animateTo(double offset,…):这两个方法用于跳转到指定的位置,它们不同之处在于,后者在跳转时会执行一个动画,而前者不会。
- ScrollController间接继承自Listenable,我们可以根据ScrollController来监听滚动事件。
class MyHomePage extends StatefulWidget {
@override
State<StatefulWidget> createState() => MyHomePageState();
}
class MyHomePageState extends State<MyHomePage> {
ScrollController _controller;
bool _isShowTop = false;
@override
void initState() {
// 初始化ScrollController
_controller = ScrollController();
// 监听滚动
_controller.addListener(() {
var tempSsShowTop = _controller.offset >= 1000;
if (tempSsShowTop != _isShowTop) {
setState(() {
_isShowTop = tempSsShowTop;
});
}
});
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("ListView展示"),
),
body: ListView.builder(
itemCount: 100,
itemExtent: 60,
controller: _controller,
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text("item$index"));
}
),
floatingActionButton: !_isShowTop ? null : FloatingActionButton(
child: Icon(Icons.arrow_upward),
onPressed: () {
_controller.animateTo(0, duration: Duration(milliseconds: 1000), curve: Curves.ease);
},
),
);
}
}
2、NotificationListener
如果我们希望监听什么时候开始滚动,什么时候结束滚动,这个时候我们可以通过NotificationListener。
- NotificationListener是一个Widget,模板参数T是想监听的通知类型,如果省略,则所有类型通知都会被监听,如果指定特定类型,则只有该类型的通知会被监听。
- NotificationListener需要一个onNotification回调函数,用于实现监听处理逻辑。
- 该回调可以返回一个布尔值,代表是否阻止该事件继续向上冒泡,如果为true时,则冒泡终止,事件停止向上传播,如果不返回或者返回值为false 时,则冒泡继续。
案例: 列表滚动, 并且在中间显示滚动进度:
class MyHomeNotificationDemoState extends State<MyHomeNotificationDemo> {
int _progress = 0;
@override
Widget build(BuildContext context) {
return NotificationListener(
onNotification: (ScrollNotification notification) {
// 1.判断监听事件的类型
if (notification is ScrollStartNotification) {
print("开始滚动.....");
} else if (notification is ScrollUpdateNotification) {
// 当前滚动的位置和总长度
final currentPixel = notification.metrics.pixels;
final totalPixel = notification.metrics.maxScrollExtent;
double progress = currentPixel / totalPixel;
setState(() {
_progress = (progress * 100).toInt();
});
print("正在滚动:${notification.metrics.pixels} - ${notification.metrics.maxScrollExtent}");
} else if (notification is ScrollEndNotification) {
print("结束滚动....");
}
return false;
},
child: Stack(
alignment: Alignment(.9, .9),
children: <Widget>[
ListView.builder(
itemCount: 100,
itemExtent: 60,
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text("item$index"));
}
),
CircleAvatar(
radius: 30,
child: Text("$_progress%"),
backgroundColor: Colors.black54,
)
],
),
);
}
}
以上是关于Flutter开发之滚动Widget的主要内容,如果未能解决你的问题,请参考以下文章