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);
@override
Widget 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,
},
];
}
@override
Widget 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);
}
}
}