第3章:基础组件 —— 3.3 图片及ICON

12 阅读8分钟

3.3 图片及ICON

📚 章节概览

图片和图标是应用UI的重要组成部分。Flutter 提供了强大的图片加载和图标系统,本章节将学习:

  • Image 组件 - 图片显示
  • ImageProvider - 不同的图片数据源
  • BoxFit - 图片适配模式
  • 图片混合与重复 - 特殊效果
  • Icon - Material Design 图标
  • 自定义字体图标 - iconfont 使用

🎯 核心知识点

1. Image 组件

Image 是 Flutter 中用于显示图片的组件,支持多种数据源。

基本用法
Image(
  image: AssetImage("images/avatar.png"),
  width: 100,
  height: 100,
)
常用属性
属性类型说明
imageImageProvider图片数据源(必选)
widthdouble?宽度
heightdouble?高度
fitBoxFit?适配模式
colorColor?混合颜色
colorBlendModeBlendMode?混合模式
repeatImageRepeat重复模式
alignmentAlignmentGeometry对齐方式

📦 ImageProvider

ImageProvider 是一个抽象类,定义了图片数据获取的接口。

1. AssetImage - 加载 Asset 图片

从项目资源中加载图片。

步骤1:添加图片到项目

将图片放到项目目录(如 images/)。

步骤2:在 pubspec.yaml 中声明
flutter:
  assets:
    - images/avatar.png
    # 或加载整个目录
    - images/
步骤3:使用图片
// 方法1:Image + AssetImage
Image(
  image: AssetImage("images/avatar.png"),
  width: 100,
)

// 方法2:Image.asset(推荐,更简洁)
Image.asset(
  "images/avatar.png",
  width: 100,
)

2. NetworkImage - 加载网络图片

从网络URL加载图片。

// 方法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,
)
带加载指示器
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,
      ),
    );
  },
)
错误处理
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 - 加载文件图片

从设备文件系统加载图片。

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 字节数据加载图片。

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缩小到适配不放大

示例对比

// 容器: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)

🌈 图片混合模式

通过 colorcolorBlendMode 实现图片颜色混合效果。

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 属性控制图片的重复方式。

// 不重复(默认)
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 中启用(默认已启用):

flutter:
  uses-material-design: true

基本用法

Icon(
  Icons.favorite,
  size: 32,
  color: Colors.red,
)

Icon 属性

属性类型说明
iconIconData?图标数据
sizedouble?大小
colorColor?颜色
semanticLabelString?语义标签(无障碍)

常用图标示例

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 使用图标

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(推荐)

Row(
  children: [
    Icon(Icons.accessible, color: Colors.green),
    Icon(Icons.error, color: Colors.green),
    Icon(Icons.fingerprint, color: Colors.green),
  ],
)

IconData 结构

const IconData(
  0xe03e,                     // 码点
  fontFamily: 'MaterialIcons', // 字体家族
  matchTextDirection: true,    // 是否匹配文本方向
)

🎨 自定义字体图标

使用 iconfont.cn 等平台的自定义图标。

步骤1:下载字体文件

iconfont.cn 选择图标,下载 .ttf 文件。

步骤2:添加到项目

.ttf 文件放到 fonts/ 目录。

步骤3:在 pubspec.yaml 中声明

flutter:
  fonts:
    - family: myIcon         # 自定义字体名
      fonts:
        - asset: fonts/iconfont.ttf

步骤4:定义 IconData

创建一个类来管理自定义图标:

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:使用自定义图标

Row(
  children: [
    Icon(MyIcons.book, color: Colors.purple, size: 32),
    Icon(MyIcons.wechat, color: Colors.green, size: 32),
  ],
)

🚀 图片缓存

Flutter 框架会自动缓存加载过的图片(内存缓存)。

缓存机制

flowchart LR
    A["请求图片"] --> B{"缓存中存在?"}
    B -->|是| C["从缓存获取"]
    B -->|否| D["加载图片"]
    D --> E["存入缓存"]
    E --> F["显示图片"]
    C --> F
    
    style A fill:#e1f5ff
    style B fill:#fff9e1
    style C fill:#e1ffe1
    style D fill:#ffe1f5
    style E fill:#ffe1f5
    style F fill:#e1f5ff

清除缓存

// 清除图片缓存
imageCache.clear();

// 清除所有缓存(包括磁盘)
imageCache.clearLiveImages();

自定义缓存大小

// 设置最大缓存图片数量(默认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 中:

flutter:
  assets:
    - images/
    - images/icons/

2. 不同分辨率适配

Flutter 支持自动选择合适分辨率的图片:

images/
├── avatar.png       # 1x
├── 2.0x/
│   └── avatar.png  # 2x
└── 3.0x/
    └── avatar.png  # 3x

在代码中只需引用基础路径:

Image.asset("images/avatar.png")  // Flutter 自动选择合适分辨率

3. 图片优化

  • 格式选择:PNG(透明图)、JPEG(照片)、WebP(高压缩)
  • 尺寸控制:避免加载过大的图片
  • 压缩:使用工具压缩图片(如 TinyPNG)
// ❌ 不好:加载大图但只显示小尺寸
Image.asset(
  "images/large_image.png",  // 2000x2000
  width: 50,  // 只显示 50x50
)

// ✅ 好:准备合适尺寸的图片
Image.asset(
  "images/small_image.png",  // 100x100
  width: 50,
)

4. 网络图片优化

// 添加占位符
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: 检查以下几点:

  1. Asset 图片
    • 是否在 pubspec.yaml 中正确声明
    • 路径是否正确(区分大小写)
    • 是否执行了 flutter pub get
# ❌ 错误
flutter:
  assets:
  - image/avatar.png  # 拼写错误

# ✅ 正确
flutter:
  assets:
    - images/avatar.png
  1. 网络图片
    • URL 是否正确
    • 是否有网络权限(Android需要在 AndroidManifest.xml 中声明)
    • 是否使用了 HTTPS(iOS 默认要求)

Q2: 如何实现图片圆角?

A: 使用 ClipRRect 包裹:

ClipRRect(
  borderRadius: BorderRadius.circular(8),
  child: Image.asset(
    "images/avatar.png",
    width: 100,
    height: 100,
  ),
)

Q3: 如何实现圆形图片?

A: 方法1:使用 CircleAvatar

CircleAvatar(
  radius: 50,
  backgroundImage: AssetImage("images/avatar.png"),
)

方法2:使用 ClipOval

ClipOval(
  child: Image.asset(
    "images/avatar.png",
    width: 100,
    height: 100,
    fit: BoxFit.cover,
  ),
)

Q4: 网络图片加载慢怎么办?

A: 使用 cached_network_image 包:

dependencies:
  cached_network_image: ^3.3.0
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

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:

特性IconImage
本质字体文件图片文件
缩放矢量,无损位图,可能失真
颜色单色,可改变原图颜色
大小体积小相对较大
适用场景简单图标复杂图形、照片

🎯 跟着做练习

练习1:实现一个图片画廊

目标: 创建一个3x3的图片网格,点击图片可以查看大图

步骤:

  1. 使用 GridView 创建网格
  2. 使用 Image.asset 显示图片
  3. 点击时使用 showDialog 显示大图
💡 查看答案
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:实现一个带加载动画的网络图片

目标: 加载网络图片时显示进度,失败时显示错误提示

步骤:

  1. 使用 Image.network
  2. 添加 loadingBuilder 显示进度
  3. 添加 errorBuilder 处理错误
💡 查看答案
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:实现一个图标选择器

目标: 显示多个图标,点击后高亮选中的图标

步骤:

  1. 创建一个图标列表
  2. 使用 StatefulWidget 管理选中状态
  3. 点击时更新选中图标
💡 查看答案
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内存字节生成的图片、截图

图标对比

特性IconImage
类型矢量(字体)位图
大小体积小相对大
缩放无损可能失真
颜色可变固定

🔗 相关资源