在上篇文章中,我们已经介绍了盒子布局Container,Row(Column)。在这篇文章中,我们将继续探讨其他几个非常常用的布局组件:GridView, ListView和Stack。这些组件的运用可以大大简化Flutter应用的布局设计
还记得上篇中的例子吗?我们通过一个 GridView 简化了很多布局
dart
home: Scaffold(
body: Center(
child: GridView.count(
crossAxisCount: 3,
padding: EdgeInsets.zero,
crossAxisSpacing: 0,
mainAxisSpacing: 0,
children: List.generate(9, (index) {
int picName = index + 1;
return Center(
child: Image.asset('images/$picName.png',
width: 100, height: 100, fit: BoxFit.cover),
);
})))));
我们来修改一下它:
dart
Widget build(BuildContext context) {
return MaterialApp(
// home: Scaffold(body: Center(child:Column(children: buildRows()))) );
home: Scaffold(
body: GridView.extent(
maxCrossAxisExtent: 200,
padding: EdgeInsets.zero,
crossAxisSpacing: 4,
mainAxisSpacing: 4,
children: List.generate(9, (index) {
int picName = index + 1;
return Center(
child: Image.asset('images/$picName.png',
width: 200, height: 200, fit: BoxFit.cover),
);
}))));
}
代码中我们将 GridView.count
修改为 GridView.extent
,并且设置了 maxCrossAxisExtent
属性。GridView.extent
创建一个可以滚动的二维网格列表。与GridView.count
不同的是,GridView.extent
允许你指定每个网格的横向尺寸。这意味着它非常适用于在具有不同宽度的设备上创建一个均匀的网格布局,因为它将自动调整每行的网格数量以适应设备的宽度。
这么说其实有点抽象,用 maxCrossAxisExtent
这个值细说一下, 这个值我们目前设置的是 200, 这个200 不是物理像素,不同设备的物理像素不同,这个值是个逻辑像素,也就是在不同设备上他都 "200",那200是什么效果?
这就是 200 的效果, 一行3列,一个图片占 200 逻辑像素,那就意味着: 任何设备上,都是一行3列,所以它非常适合在不同设备上做自适应布局,那么我们将 200 改成 400,则在任何设备上,都一行展示2张图片
我们如果想在这些图片上写一些字该如何做呢??
Stack
需求:给每张图片上加一个拍摄时间
遇到这种需求我们需要使用 Stack 布局,Stack是一个用于叠加多个子组件的布局组件。在Stack组件中,第一个子组件是底部组件,接下来的子组件依次叠加在上面。子组件的位置可以通过Alignment参数进行精确控制,也可以通过Positioned组件进行相对定位。
dart
Widget build(BuildContext context) {
return MaterialApp(
// home: Scaffold(body: Center(child:Column(children: buildRows()))) );
home: Scaffold(
body: GridView.extent(
maxCrossAxisExtent: 400,
// crossAxisCount: 3,
padding: EdgeInsets.zero,
crossAxisSpacing: 4,
mainAxisSpacing: 4,
children: List.generate(9, (index) {
int picName = index + 1;
return Center(
child: Stack(
alignment: Alignment.bottomRight
,children: [
Image.asset('images/$picName.png',
width: 200, height: 200, fit: BoxFit.cover),
Text('2023/11/06', style: TextStyle(fontSize: 20, color: Colors.white, backgroundColor: Colors.lightBlue),)
],)
);
}))));
}
在 Stack
Widget 的 children
属性中,我们先放的元素会垫底,正如名字 stack 一样,我们使用 alignment
参数来将所有的文本组件都定位到图片的右下角。
此外我们也可以使用 Positioned
实现更精确的定位,比如我们想让文字距离底部10个逻辑像素, 不要贴着
dart
Widget build(BuildContext context) {
return MaterialApp(
// home: Scaffold(body: Center(child:Column(children: buildRows()))) );
home: Scaffold(
body: GridView.extent(
maxCrossAxisExtent: 400,
// crossAxisCount: 3,
padding: EdgeInsets.zero,
crossAxisSpacing: 4,
mainAxisSpacing: 4,
children: List.generate(9, (index) {
int picName = index + 1;
return Center(
child: Stack(
alignment: Alignment.bottomRight
,children: [
Image.asset('images/$picName.png',
width: 200, height: 200, fit: BoxFit.cover),
Positioned(bottom: 10,child: Text('2023/11/06', style: TextStyle(fontSize: 20, color: Colors.white, backgroundColor: Colors.lightBlue),))
],)
);
}))));
}
其实布局很灵活,我们可以嵌套任意的布局组件构建复杂界面,比如稍微美化一下
dart
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: GridView.extent(
maxCrossAxisExtent: 400,
padding: EdgeInsets.zero,
crossAxisSpacing: 4,
mainAxisSpacing: 4,
children: List.generate(9, (index) {
int picName = index + 1;
return Center(
child: Stack(
alignment: Alignment.bottomRight,
children: [
Image.asset('images/$picName.png',
width: 200, height: 200, fit: BoxFit.cover),
Positioned(
bottom: 0,
child: Container(
alignment: Alignment.centerRight,
color: Colors.black.withOpacity(0.6),
width: 200,
child: Padding(padding: EdgeInsets.all(8.0),child:Text(
'2023/11/06',
style: TextStyle(
fontSize: 20,
color: Colors.white,
))))
)],
));
}))));
}
ListView
ListView 在移动端 UI 设计中绝对是 C 位,它是一种可滚动的线性列表,我们通常看到的 App 列表都可以用 ListView 实现, 比如淘宝购物刷商品,美团刷外卖,我们都可以使用 ListView来构建各种不同类型的列表,包括文本、图像、图标、或者是这些元素的组合。此外,通过自定义每个列表项的行为,您可以创建出满足各种需求的用户界面。例如,您可以设置项目在被点击或者长按时执行特定的操作,或者添加滑动删除的交互。
下面来做个微信聊天列表, 先看灵魂原型图
先定义数据结构准备遍历
dart
final List<Map<String, String>> data = [
{
'imageUrl': 'images/1.png',
'title': '摄影爱好者',
'message': '这张图片怎么样',
},
{
'imageUrl': 'images/2.png',
'title': 'Max',
'message': '帮我取快递',
},
{
'imageUrl': 'images/3.png',
'title': '爱吃泡面',
'message': '。。。',
},
{
'imageUrl': 'images/4.png',
'title': 'Ferry',
'message': '你在哪里',
},
{
'imageUrl': 'images/5.png',
'title': '不瘦不换名字',
'message': '饿死了',
},
];
然后通过 ListView.builder 将这个数据渲染成列表
dart
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: ListView.builder(
itemCount: data.length,
itemBuilder: (context, index) {
return ListTile(
leading: Image.asset(data[index]['imageUrl']!, width: 50, height: 50, fit: BoxFit.cover,),
title: Text(data[index]['title']!),
subtitle: Text(data[index]['message']!),
);
}
),
),
);
}
这个例子展示了如何使用Flutter的 ListView.builder 方法创建一个具有自定义布局的列表, itemCount
是列表的长度,itemBuilder
是一个回调函数,用于构建列表的每一个项目。
在这个函数中,context 是构建上下文,index 是列表项目的索引。ListTile
是列表项目的布局,这个布局组件封装了大部分的通用展示场景,我们想要的 UI 效果基本都可以通过 ListTitle 实现。但是如果需求对自定义要求比较高,那我们也可以不使用 ListeTitle , 在 itemBuilder 中返回一个 Container 或者其他布局来完全自定义我们的列表样式
绕不开的大数据量问题
我们的列表随着数据量增加,性能会下降,这是不可接受的。比如在前端开发中我们会使用虚拟列表这样的组件,虚拟列表的原理是只渲染当前可视区域内的列表项,而非全部列表项。当用户滚动列表时,虚拟列表会动态地加载和卸载列表项,以确保任何时候渲染的列表项数量是固定的。这种方式大大减少了DOM节点的数量和频繁的布局计算,从而提高了滚动性能。这对于大数据量的列表尤其有效,因为它可以防止页面卡顿和内存溢出,提高用户体验。
其实 Flutter 已经非常贴心的帮我们考虑了这个问题,Flutter的 ListView.builder
构造函数可以高效地处理大量数据。它是一个 懒加载 列表,我们有 5 万条数据,它不会把 5 万条数据都渲染出来,只有当列表项滚动到视图中时,才会构建该列表项。这样,不论列表有多长,应用的性能都不会受到影响,因为只有在屏幕上显示的列表项才会被实际构造。在创建ListView.builder时,我们提供的itemBuilder函数在需要时才被调用。