开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 4 天,点击查看活动详情
一、前言
通过前面了解了Flutter项目的基本流程以后,我们再通过展示一个列表熟悉Flutter控件,Android实现列表免不了ListView或者GridView,以及后面出来的功能强大的RecyclerView,对于列表控件,我们在定义好单个item的布局后,再去设置它的列表个数即可.
二、界面布局
对于这样的布局,我们或许暂时还不陌生,拆分成线性布局,纵向地排列为左边的一张图片和右边的一个容器.右边的一个容器再进行线性布局的拆分,顶部是一个多行展示的标题,下方是一个横向排列的容器,左边是图标,右边是浏览数.
三、ListView
ListView({
Axis scrollDirection = Axis.vertical,
ScrollController controller,
ScrollPhysics physics,
bool shrinkWrap = false,
EdgeInsetsGeometry padding,
this.itemExtent,
double cacheExtent,
List<Widget> children = const <Widget>[],
})
- scrollDirection--列表的滚动方向,可选值有Axis的horizontal和vertical,默认是垂直方向上滚动
- controller--控制器,与列表滚动相关,比如监听列表的滚动事件
- physics--列表滚动至边缘后继续拖动的物理效果,Android与iOS效果不同。Android会呈现出一个波纹状(对应ClampingScrollPhysics),而iOS上有一个回弹的弹性效果(对应BouncingScrollPhysics)。如果你想不同的平台上呈现各自的效果可以使用AlwaysScrollableScrollPhysics,它会根据不同平台自动选用各自的物理效果。如果你想禁用在边缘的拖动效果,那可以使用NeverScrollableScrollPhysics;
- shrinkWrap--该属性将决定列表的长度是否仅包裹其内容的长度。当ListView嵌在一个无限长的容器组件中时,shrinkWrap必须为true,否则Flutter会给出警告;
- padding--列表内边距
- itemExtent--子元素长度。当列表中的每一项长度是固定的情况下可以指定该值,有助于提高列表的性能(因为它可以帮助ListView在未实际渲染子元素之前就计算出每一项元素的位置);
- cacheExtent--预渲染区域长度,ListView会在其可视区域的两边留一个cacheExtent长度的区域作为预渲染区域(对于ListView.build或ListView.separated构造函数创建的列表,不在可视区域和预渲染区域内的子元素不会被创建或会被销毁);
- children:容纳子元素的组件数组
3.1 ListView.build()
ListView.builder({
...
int itemCount,
@required IndexedWidgetBuilder itemBuilder,
})
关键的两个参数分别是itemCount和ItemBuilder
3.2 ListView Item
我们通过代码实例来理解一下flutter的列表实现,item的拆分已经很清楚了,通过Row和Column的组装来实现结构的生成.
import 'package:flutter/material.dart';
class DynamicItem extends StatelessWidget {
final String title;
final String imageUrl;
final int viewCount;
static const double ITEM_HEIGHT = 100;
static const double TITLE_HEIGHT = 80;
static const double MARGIN_SIZE = 10;
const DynamicItem(this.title, this.imageUrl, this.viewCount, {Key? key})
: super(key: key);
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.all(MARGIN_SIZE),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_imageWrapper(this.imageUrl),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_titleWrapper(context, this.title),
_viewCountWrapper(this.viewCount.toString()),
],
))
],
),
);
}
Widget _viewCountWrapper(String text) {
return Container(
margin: EdgeInsets.fromLTRB(MARGIN_SIZE, 0, 0, 0),
height: ITEM_HEIGHT - TITLE_HEIGHT,
child: Row(
children: [
Icon(
Icons.remove_red_eye_outlined,
size: 14.0,
color: Colors.grey,
),
SizedBox(
width: 5,
),
Text(
this.viewCount.toString(),
style: TextStyle(color: Colors.grey, fontSize: 14.0),
)
],
),
);
}
Widget _titleWrapper(BuildContext context, String text) {
return Container(
height: TITLE_HEIGHT,
margin: EdgeInsets.fromLTRB(MARGIN_SIZE, 0, 0, 0),
child: Text(
this.title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.headline6,
),
);
}
Widget _imageWrapper(String imageUrl) {
return SizedBox(
width: 150,
height: ITEM_HEIGHT,
child: Image.network(imageUrl),
);
}
}
3.3 生成模拟数据item
我们可以通过输入页码和size来生成对应数据长度的list
class DynamicMockData {
static List<Map<String, Object>> list(int page, int size) {
return List<Map<String, Object>>.generate(size, (index) {
return {
'title': '标题${index + (page - 1) * size + 1}:这是一个列表标题,最多两行,多处部分会被截取',
'imageUrl':
'https://t7.baidu.com/it/u=963301259,1982396977&fm=193&f=GIF',
'viewCount': 20,
};
});
}
}
由于暂时不涉及下拉刷新和上拉加载,所以在动态页面直接声明一串固定长度的list
static const int PAGE_SIZE = 20;
List<Map<String, Object>> _listItems = DynamicMockData.list(1, PAGE_SIZE);
这样,数据源以及单个数据item的布局就已经构建完成了.
3.4 build ListView
在build方法里调用一下ListView.builder方法就可以
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: 20,
itemBuilder: (context, index) {
return DynamicItem(
_listItems[index]['title'] as String,
_listItems[index]['imageUrl'] as String,
_listItems[index]['viewCount'] as int);
});
}
以上,我们就简单实现了一个固定长度的列表展示,下一节我们将给这个列表的数据加上一些上拉加载和下拉刷新的功能.