3.3 图片及ICON
📚 章节概览
图片和图标是应用UI的重要组成部分。Flutter 提供了强大的图片加载和图标系统,本章节将学习:
- Image 组件 - 图片显示
- ImageProvider - 不同的图片数据源
- BoxFit - 图片适配模式
- 图片混合与重复 - 特殊效果
- Icon - Material Design 图标
- 自定义字体图标 - iconfont 使用
🎯 核心知识点
1. Image 组件
Image 是 Flutter 中用于显示图片的组件,支持多种数据源。
基本用法
dart
Image(
image: AssetImage("images/avatar.png"),
width: 100,
height: 100,
)
常用属性
| 属性 | 类型 | 说明 |
|---|---|---|
image |
ImageProvider | 图片数据源(必选) |
width |
double? | 宽度 |
height |
double? | 高度 |
fit |
BoxFit? | 适配模式 |
color |
Color? | 混合颜色 |
colorBlendMode |
BlendMode? | 混合模式 |
repeat |
ImageRepeat | 重复模式 |
alignment |
AlignmentGeometry | 对齐方式 |
📦 ImageProvider
ImageProvider 是一个抽象类,定义了图片数据获取的接口。
1. AssetImage - 加载 Asset 图片
从项目资源中加载图片。
步骤1:添加图片到项目
将图片放到项目目录(如 images/)。
步骤2:在 pubspec.yaml 中声明
yaml
flutter:
assets:
- images/avatar.png
# 或加载整个目录
- images/
步骤3:使用图片
dart
// 方法1:Image + AssetImage
Image(
image: AssetImage("images/avatar.png"),
width: 100,
)
// 方法2:Image.asset(推荐,更简洁)
Image.asset(
"images/avatar.png",
width: 100,
)
2. NetworkImage - 加载网络图片
从网络URL加载图片。
dart
// 方法1:Image + NetworkImage
Image(
image: NetworkImage(
"https://avatars2.githubusercontent.com/u/20411648?s=460&v=4",
),
width: 100,
)
// 方法2:Image.network(推荐)
Image.network(
"https://avatars2.githubusercontent.com/u/20411648?s=460&v=4",
width: 100,
)
带加载指示器
dart
Image.network(
"https://example.com/image.png",
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) {
return child; // 加载完成,显示图片
}
return Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
);
},
)
错误处理
dart
Image.network(
"https://invalid-url.com/image.png",
errorBuilder: (context, error, stackTrace) {
return Container(
width: 100,
height: 100,
color: Colors.grey[300],
child: Icon(Icons.broken_image),
);
},
)
3. FileImage - 加载文件图片
从设备文件系统加载图片。
dart
import 'dart:io';
Image(
image: FileImage(File('/path/to/image.png')),
width: 100,
)
// 或使用快捷方式
Image.file(
File('/path/to/image.png'),
width: 100,
)
4. MemoryImage - 从内存加载
从 Uint8List 字节数据加载图片。
dart
import 'dart:typed_data';
Uint8List bytes = ...; // 图片字节数据
Image(
image: MemoryImage(bytes),
width: 100,
)
// 或使用快捷方式
Image.memory(
bytes,
width: 100,
)
🎨 BoxFit - 图片适配模式
BoxFit 控制图片如何适配容器的尺寸。
| 模式 | 说明 | 效果 |
|---|---|---|
BoxFit.fill |
填充整个容器 | 可能变形 |
BoxFit.contain |
完整显示图片 | 可能有空白 |
BoxFit.cover |
覆盖容器 | 可能裁剪 |
BoxFit.fitWidth |
宽度适配 | 高度可能溢出 |
BoxFit.fitHeight |
高度适配 | 宽度可能溢出 |
BoxFit.none |
不缩放 | 原始大小 |
BoxFit.scaleDown |
缩小到适配 | 不放大 |
示例对比
dart
// 容器:100x80,图片:200x200
// fill:拉伸填充,图片变形
Image.network(url, width: 100, height: 80, fit: BoxFit.fill)
// contain:完整显示,上下有空白
Image.network(url, width: 100, height: 80, fit: BoxFit.contain)
// cover:覆盖容器,左右被裁剪
Image.network(url, width: 100, height: 80, fit: BoxFit.cover)
// fitWidth:宽度100,高度可能超出80
Image.network(url, width: 100, height: 80, fit: BoxFit.fitWidth)
// fitHeight:高度80,宽度可能小于100
Image.network(url, width: 100, height: 80, fit: BoxFit.fitHeight)
// none:200x200 原始大小(会溢出容器)
Image.network(url, width: 100, height: 80, fit: BoxFit.none)
// scaleDown:如果图片大于容器,缩小到适配
Image.network(url, width: 100, height: 80, fit: BoxFit.scaleDown)
🌈 图片混合模式
通过 color 和 colorBlendMode 实现图片颜色混合效果。
dart
Image.asset(
"images/avatar.png",
width: 100,
color: Colors.blue, // 混合颜色
colorBlendMode: BlendMode.multiply, // 混合模式
)
常用混合模式
| 模式 | 说明 |
|---|---|
BlendMode.multiply |
正片叠底 |
BlendMode.screen |
滤色 |
BlendMode.overlay |
叠加 |
BlendMode.difference |
差值 |
BlendMode.color |
颜色 |
BlendMode.modulate |
调制 |
🔁 图片重复模式
通过 repeat 属性控制图片的重复方式。
dart
// 不重复(默认)
Image.asset(
"images/avatar.png",
repeat: ImageRepeat.noRepeat,
)
// 水平重复
Image.asset(
"images/avatar.png",
repeat: ImageRepeat.repeatX,
)
// 垂直重复
Image.asset(
"images/avatar.png",
repeat: ImageRepeat.repeatY,
)
// 水平和垂直重复
Image.asset(
"images/avatar.png",
repeat: ImageRepeat.repeat,
)
🎯 Icon - Material Design 图标
Flutter 内置了完整的 Material Design 图标库。
启用图标
在 pubspec.yaml 中启用(默认已启用):
yaml
flutter:
uses-material-design: true
基本用法
dart
Icon(
Icons.favorite,
size: 32,
color: Colors.red,
)
Icon 属性
| 属性 | 类型 | 说明 |
|---|---|---|
icon |
IconData? | 图标数据 |
size |
double? | 大小 |
color |
Color? | 颜色 |
semanticLabel |
String? | 语义标签(无障碍) |
常用图标示例
dart
Row(
children: [
Icon(Icons.home, size: 32, color: Colors.blue),
Icon(Icons.favorite, size: 32, color: Colors.red),
Icon(Icons.shopping_cart, size: 32, color: Colors.green),
Icon(Icons.person, size: 32, color: Colors.purple),
Icon(Icons.settings, size: 32, color: Colors.grey),
],
)
查找图标
Material Design 图标库:fonts.google.com/icons
🔤 IconData - 字体图标原理
图标本质上是字体文件中的字符。
通过 Unicode 使用图标
dart
String icons = "";
icons += "\uE03e"; // accessible: 0xe03e
icons += " \uE237"; // error: 0xe237
icons += " \uE287"; // fingerprint: 0xe287
Text(
icons,
style: TextStyle(
fontFamily: "MaterialIcons", // Material 图标字体
fontSize: 24.0,
color: Colors.green,
),
)
使用 Icon 和 Icons(推荐)
dart
Row(
children: [
Icon(Icons.accessible, color: Colors.green),
Icon(Icons.error, color: Colors.green),
Icon(Icons.fingerprint, color: Colors.green),
],
)
IconData 结构
dart
const IconData(
0xe03e, // 码点
fontFamily: 'MaterialIcons', // 字体家族
matchTextDirection: true, // 是否匹配文本方向
)
🎨 自定义字体图标
使用 iconfont.cn 等平台的自定义图标。
步骤1:下载字体文件
从 iconfont.cn 选择图标,下载 .ttf 文件。
步骤2:添加到项目
将 .ttf 文件放到 fonts/ 目录。
步骤3:在 pubspec.yaml 中声明
yaml
flutter:
fonts:
- family: myIcon # 自定义字体名
fonts:
- asset: fonts/iconfont.ttf
步骤4:定义 IconData
创建一个类来管理自定义图标:
dart
class MyIcons {
// book 图标(码点从 iconfont.cn 获取)
static const IconData book = IconData(
0xe614,
fontFamily: 'myIcon',
matchTextDirection: true,
);
// 微信图标
static const IconData wechat = IconData(
0xec7d,
fontFamily: 'myIcon',
matchTextDirection: true,
);
}
步骤5:使用自定义图标
dart
Row(
children: [
Icon(MyIcons.book, color: Colors.purple, size: 32),
Icon(MyIcons.wechat, color: Colors.green, size: 32),
],
)
🚀 图片缓存
Flutter 框架会自动缓存加载过的图片(内存缓存)。
缓存机制
清除缓存
dart
// 清除图片缓存
imageCache.clear();
// 清除所有缓存(包括磁盘)
imageCache.clearLiveImages();
自定义缓存大小
dart
// 设置最大缓存图片数量(默认1000)
imageCache.maximumSize = 100;
// 设置最大缓存字节数(默认50MB)
imageCache.maximumSizeBytes = 10 * 1024 * 1024; // 10MB
💡 最佳实践
1. 图片资源组织
项目根目录/
├── images/
│ ├── avatar.png
│ ├── logo.png
│ └── icons/
│ ├── icon1.png
│ └── icon2.png
└── pubspec.yaml
在 pubspec.yaml 中:
yaml
flutter:
assets:
- images/
- images/icons/
2. 不同分辨率适配
Flutter 支持自动选择合适分辨率的图片:
bash
images/
├── avatar.png # 1x
├── 2.0x/
│ └── avatar.png # 2x
└── 3.0x/
└── avatar.png # 3x
在代码中只需引用基础路径:
dart
Image.asset("images/avatar.png") // Flutter 自动选择合适分辨率
3. 图片优化
- 格式选择:PNG(透明图)、JPEG(照片)、WebP(高压缩)
- 尺寸控制:避免加载过大的图片
- 压缩:使用工具压缩图片(如 TinyPNG)
dart
// ❌ 不好:加载大图但只显示小尺寸
Image.asset(
"images/large_image.png", // 2000x2000
width: 50, // 只显示 50x50
)
// ✅ 好:准备合适尺寸的图片
Image.asset(
"images/small_image.png", // 100x100
width: 50,
)
4. 网络图片优化
dart
// 添加占位符
FadeInImage.assetNetwork(
placeholder: 'images/placeholder.png', // 占位图
image: 'https://example.com/image.png',
width: 100,
height: 100,
fit: BoxFit.cover,
)
// 或使用 cached_network_image 包(推荐)
import 'package:cached_network_image/cached_network_image.dart';
CachedNetworkImage(
imageUrl: "https://example.com/image.png",
placeholder: (context, url) => CircularProgressIndicator(),
errorWidget: (context, url, error) => Icon(Icons.error),
)
5. 图标选择指南
| 场景 | 推荐 | 理由 |
|---|---|---|
| 通用图标 | Material Icons | 内置,免费,数量多 |
| 品牌图标 | 自定义 iconfont | 品牌一致性 |
| 复杂图形 | SVG/图片 | 更灵活 |
🤔 常见问题(FAQ)
Q1: 为什么我的图片不显示?
A: 检查以下几点:
- Asset 图片 :
- 是否在
pubspec.yaml中正确声明 - 路径是否正确(区分大小写)
- 是否执行了
flutter pub get
- 是否在
yaml
# ❌ 错误
flutter:
assets:
- image/avatar.png # 拼写错误
# ✅ 正确
flutter:
assets:
- images/avatar.png
- 网络图片 :
- URL 是否正确
- 是否有网络权限(Android需要在 AndroidManifest.xml 中声明)
- 是否使用了 HTTPS(iOS 默认要求)
Q2: 如何实现图片圆角?
A: 使用 ClipRRect 包裹:
dart
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.asset(
"images/avatar.png",
width: 100,
height: 100,
),
)
Q3: 如何实现圆形图片?
A: 方法1:使用 CircleAvatar
dart
CircleAvatar(
radius: 50,
backgroundImage: AssetImage("images/avatar.png"),
)
方法2:使用 ClipOval
dart
ClipOval(
child: Image.asset(
"images/avatar.png",
width: 100,
height: 100,
fit: BoxFit.cover,
),
)
Q4: 网络图片加载慢怎么办?
A: 使用 cached_network_image 包:
dart
dependencies:
cached_network_image: ^3.3.0
dart
CachedNetworkImage(
imageUrl: "https://example.com/image.png",
placeholder: (context, url) => CircularProgressIndicator(),
errorWidget: (context, url, error) => Icon(Icons.error),
fadeInDuration: Duration(milliseconds: 500),
)
Q5: 如何获取图片的实际尺寸?
A: 使用 Image.image.resolve() 或 ImageStream:
dart
void getImageSize(ImageProvider imageProvider) {
final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty);
stream.addListener(ImageStreamListener((ImageInfo info, bool _) {
print('图片尺寸: ${info.image.width} x ${info.image.height}');
}));
}
// 使用
getImageSize(AssetImage("images/avatar.png"));
Q6: Icon 和 Image 的区别?
A:
| 特性 | Icon | Image |
|---|---|---|
| 本质 | 字体文件 | 图片文件 |
| 缩放 | 矢量,无损 | 位图,可能失真 |
| 颜色 | 单色,可改变 | 原图颜色 |
| 大小 | 体积小 | 相对较大 |
| 适用场景 | 简单图标 | 复杂图形、照片 |
🎯 跟着做练习
练习1:实现一个图片画廊
目标: 创建一个3x3的图片网格,点击图片可以查看大图
步骤:
- 使用
GridView创建网格 - 使用
Image.asset显示图片 - 点击时使用
showDialog显示大图
💡 查看答案
dart
class ImageGallery extends StatelessWidget {
const ImageGallery({super.key});
final List<String> images = const [
"images/avatar.png",
"images/avatar.png",
"images/avatar.png",
"images/avatar.png",
"images/avatar.png",
"images/avatar.png",
"images/avatar.png",
"images/avatar.png",
"images/avatar.png",
];
void _showImage(BuildContext context, String imagePath) {
showDialog(
context: context,
builder: (context) => Dialog(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Image.asset(imagePath),
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('关闭'),
),
],
),
),
);
}
@override
Widget build(BuildContext context) {
return GridView.builder(
padding: const EdgeInsets.all(8),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
itemCount: images.length,
itemBuilder: (context, index) {
return GestureDetector(
onTap: () => _showImage(context, images[index]),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.asset(
images[index],
fit: BoxFit.cover,
),
),
);
},
);
}
}
练习2:实现一个带加载动画的网络图片
目标: 加载网络图片时显示进度,失败时显示错误提示
步骤:
- 使用
Image.network - 添加
loadingBuilder显示进度 - 添加
errorBuilder处理错误
💡 查看答案
dart
class NetworkImageWithLoading extends StatelessWidget {
final String imageUrl;
const NetworkImageWithLoading({
super.key,
required this.imageUrl,
});
@override
Widget build(BuildContext context) {
return Image.network(
imageUrl,
width: 200,
height: 200,
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) {
// 加载完成
return child;
}
// 加载中
return Container(
width: 200,
height: 200,
color: Colors.grey[200],
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
const SizedBox(height: 8),
Text(
loadingProgress.expectedTotalBytes != null
? '${(loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! * 100).toStringAsFixed(0)}%'
: '加载中...',
style: const TextStyle(fontSize: 12),
),
],
),
),
);
},
errorBuilder: (context, error, stackTrace) {
// 加载失败
return Container(
width: 200,
height: 200,
color: Colors.grey[300],
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.broken_image, size: 48, color: Colors.grey),
const SizedBox(height: 8),
const Text(
'加载失败',
style: TextStyle(color: Colors.grey),
),
TextButton(
onPressed: () {
// 可以触发重新加载
},
child: const Text('重试'),
),
],
),
);
},
);
}
}
// 使用
NetworkImageWithLoading(
imageUrl: "https://avatars2.githubusercontent.com/u/20411648?s=460&v=4",
)
练习3:实现一个图标选择器
目标: 显示多个图标,点击后高亮选中的图标
步骤:
- 创建一个图标列表
- 使用
StatefulWidget管理选中状态 - 点击时更新选中图标
💡 查看答案
dart
class IconSelector extends StatefulWidget {
const IconSelector({super.key});
@override
State<IconSelector> createState() => _IconSelectorState();
}
class _IconSelectorState extends State<IconSelector> {
final List<IconData> icons = const [
Icons.home,
Icons.favorite,
Icons.shopping_cart,
Icons.person,
Icons.settings,
Icons.notifications,
Icons.email,
Icons.search,
];
int _selectedIndex = 0;
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'选择一个图标',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
Wrap(
spacing: 16,
runSpacing: 16,
children: List.generate(icons.length, (index) {
final isSelected = index == _selectedIndex;
return GestureDetector(
onTap: () {
setState(() {
_selectedIndex = index;
});
},
child: Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: isSelected
? Colors.blue.withValues(alpha: 0.2)
: Colors.grey.withValues(alpha: 0.1),
border: Border.all(
color: isSelected ? Colors.blue : Colors.transparent,
width: 2,
),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
icons[index],
size: 32,
color: isSelected ? Colors.blue : Colors.grey,
),
),
);
}),
),
const SizedBox(height: 16),
Text(
'已选择:${icons[_selectedIndex].toString().split('.').last}',
style: const TextStyle(fontSize: 14, color: Colors.grey),
),
],
);
}
}
📋 小结
核心要点
| 组件/类 | 用途 | 关键方法 |
|---|---|---|
| Image | 显示图片 | Image.asset, Image.network |
| ImageProvider | 图片数据源 | AssetImage, NetworkImage |
| BoxFit | 图片适配 | fill, contain, cover |
| Icon | 显示图标 | Icon(Icons.xxx) |
| IconData | 图标数据 | 码点 + 字体家族 |
加载方式对比
| 方式 | 数据源 | 适用场景 |
|---|---|---|
Image.asset |
项目资源 | 应用图标、Logo |
Image.network |
网络URL | 用户头像、动态内容 |
Image.file |
本地文件 | 相册、下载的图片 |
Image.memory |
内存字节 | 生成的图片、截图 |
图标对比
| 特性 | Icon | Image |
|---|---|---|
| 类型 | 矢量(字体) | 位图 |
| 大小 | 体积小 | 相对大 |
| 缩放 | 无损 | 可能失真 |
| 颜色 | 可变 | 固定 |