Flutter基础(前端教程②-卡片列表)
ListTile
是 Flutter 中专门为列表项设计的标准行组件,它提供了一种简洁的方式来创建带有标题、副标题、图标和按钮的列表项。就像乐高积木一样,ListTile
帮你快速搭建出常见的列表布局。
为什么需要 ListTile?
想象你要创建一个联系人列表,每个联系人需要显示:
- 左侧头像
- 中间名字(主标题)
- 下方电话号码(副标题)
- 右侧操作按钮
手动用 Column
、Row
和 Padding
组合实现会很繁琐,而 ListTile
把这些功能集成在一起,一行代码搞定!
ListTile
的核心属性:
ListTile(leading: Icon(Icons.person), // 左侧图标/图片title: Text('联系人姓名'), // 主标题(粗体)subtitle: Text('电话号码'), // 副标题(灰色小字)trailing: Icon(Icons.arrow_forward), // 右侧图标/按钮onTap: () {}, // 点击事件
);
好的!Card
是 Flutter 中专门用于创建卡片式布局的组件,它提供了默认的圆角、阴影和内边距,让你轻松实现现代应用中常见的卡片设计。
Card
的最简形式:
Card(child: Text('这是一个简单的卡片'),
);
效果:一个带有圆角和轻微阴影的白色矩形区域。
常用属性
Card(elevation: 8, // 阴影深度(默认2)color: Colors.blue, // 卡片背景色shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12), // 圆角半径),margin: EdgeInsets.all(16), // 卡片外边距child: Text('自定义样式的卡片'),
);
卡片
Card(child: Column(crossAxisAlignment: CrossAxisAlignment.start,children: [Image.network('https://example.com/news.jpg'), // 卡片顶部图片Padding(padding: EdgeInsets.all(12),child: Column(crossAxisAlignment: CrossAxisAlignment.start,children: [Text('这是新闻标题',style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),),SizedBox(height: 8),Text('这是新闻内容摘要...'),],),),],),
);
下面是这个代码对应的卡片视觉示意图(简化版):
+----------------------------------------+
| |
| [新闻图片] | ← Image.network(顶部占满宽度)
| (https://example.com/news.jpg) |
| |
+----------------------------------------+
| 这是新闻标题 | ← Text(加粗、18号字体)
| |
| 这是新闻内容摘要... | ← Text(普通文本)
| | (上下左右内边距12,与标题间距8)
+----------------------------------------+
Flutter 中创建列表的 3 种方式
方式 1:直接用 ListView
适合少量固定数量的列表项:
ListView(children: [Text('第一项'),Text('第二项'),Text('第三项'),],
)
方式 2:用 ListView.builder
动态生成
适合大量数据(自动复用组件,性能更好):
ListView.builder(itemCount: 100, // 列表项数量itemBuilder: (context, index) {return Text('第 $index 项');},
)
方式 3:用 ListTile
快速创建标准列表项
适合联系人、设置项等标准行布局:
ListView(children: [ListTile(leading: Icon(Icons.person),title: Text('联系人1'),subtitle: Text('电话:13800000000'),),ListTile(leading: Icon(Icons.person),title: Text('联系人2'),subtitle: Text('电话:13900000000'),),],
)
下面是一个类似微信聊天列表界面的简化实现,包含头像、名称、消息预览和时间:
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';void main() {runApp(const MyApp());
}class MyApp extends StatelessWidget {const MyApp({Key? key}) : super(key: key);@overrideWidget build(BuildContext context) {return MaterialApp(theme: ThemeData(primarySwatch: Colors.green),home: const WeChatHomePage(),);}
}class WeChatHomePage extends StatelessWidget {const WeChatHomePage({Key? key}) : super(key: key);// 模拟聊天数据List<Map<String, dynamic>> getChatData() {return [{'name': '张三','avatar': 'https://picsum.photos/100/100?random=1','message': '下午一起吃饭吗?','time': DateTime.now().subtract(const Duration(minutes: 5)),'unread': 2,},{'name': '李四','avatar': 'https://picsum.photos/100/100?random=2','message': '好的,我稍后回复你','time': DateTime.now().subtract(const Duration(hours: 1)),'unread': 0,},{'name': '工作群','avatar': 'https://picsum.photos/100/100?random=3','message': '王五:明天上午10点开会','time': DateTime.now().subtract(const Duration(hours: 3)),'unread': 5,},{'name': '妈妈','avatar': 'https://picsum.photos/100/100?random=4','message': '记得带伞,今天下雨','time': DateTime.now().subtract(const Duration(days: 1)),'unread': 0,},{'name': '同事A','avatar': 'https://picsum.photos/100/100?random=5','message': '项目文档已发送,请查收','time': DateTime.now().subtract(const Duration(days: 1)),'unread': 1,},];}@overrideWidget build(BuildContext context) {final chatData = getChatData();return Scaffold(appBar: AppBar(title: const Text('微信'),actions: [IconButton(icon: const Icon(Icons.search), onPressed: () {}),IconButton(icon: const Icon(Icons.add), onPressed: () {}),],),body: ListView.builder(itemCount: chatData.length,itemBuilder: (context, index) {final chat = chatData[index];final formattedTime = _formatTime(chat['time']);return Column(children: [// 分隔线(除了第一个)if (index > 0) const Divider(height: 0),// 聊天项ListTile(leading: CircleAvatar(backgroundImage: NetworkImage(chat['avatar']),radius: 28,),title: Text(chat['name'],style: TextStyle(fontWeight: chat['unread'] > 0 ? FontWeight.bold : FontWeight.normal,),),subtitle: Text(chat['message'],maxLines: 1,overflow: TextOverflow.ellipsis,style: TextStyle(color: Colors.grey[600],),),trailing: Column(mainAxisAlignment: MainAxisAlignment.center,crossAxisAlignment: CrossAxisAlignment.end,children: [// 时间Text(formattedTime,style: TextStyle(fontSize: 12,color: Colors.grey[500],),),// 未读数(如果有)if (chat['unread'] > 0)Container(margin: const EdgeInsets.only(top: 4),padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1),decoration: BoxDecoration(color: Colors.red,borderRadius: BorderRadius.circular(10),),child: Text(chat['unread'].toString(),style: const TextStyle(color: Colors.white, fontSize: 12),),),],),onTap: () {ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('打开与 ${chat['name']} 的聊天')));},),],);},),);}// 格式化时间(根据是否今天显示不同格式)String _formatTime(DateTime time) {final now = DateTime.now();final today = DateTime(now.year, now.month, now.day);final yesterday = today.subtract(const Duration(days: 1));if (DateTime(time.year, time.month, time.day).isAtSameMomentAs(today)) {// 今天:显示时:分return DateFormat('HH:mm').format(time);} else if (DateTime(time.year, time.month, time.day).isAtSameMomentAs(yesterday)) {// 昨天:显示"昨天"return '昨天';} else {// 其他:显示月-日return DateFormat('MM-dd').format(time);}}
}