欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
一、image_picker 库简介
image_picker 是 Flutter 官方维护的插件库之一,用于从设备相册或相机获取图片和视频。无论是用户头像上传、图片分享、视频录制,还是多媒体内容管理,image_picker 都是移动应用开发中不可或缺的工具。
📋 image_picker 核心特点
| 特点 | 说明 |
|---|---|
| 图片选择 | 支持从相册选择单张或多张图片 |
| 视频选择 | 支持从相册选择视频或录制视频 |
| 相机拍摄 | 支持直接调用相机拍照或录像 |
| 图片压缩 | 支持设置最大宽高和图片质量 |
| 多选支持 | 支持一次性选择多张图片 |
| 媒体混合 | 支持同时选择图片和视频 |
| 跨平台兼容 | 支持 Android、iOS、Web、OpenHarmony |
平台功能支持对比
| 功能 | Android | iOS | Web | OpenHarmony |
|---|---|---|---|---|
| 从相册选图 | ✔️ | ✔️ | ✔️ | ✔️ |
| 从相机拍照 | ✔️ | ✔️ | ❌ | ✔️ |
| 从相册选视频 | ✔️ | ✔️ | ✔️ | ✔️ |
| 从相机录像 | ✔️ | ✔️ | ❌ | ✔️ |
| 多选图片 | ✔️ | ✔️ (iOS 14+) | ✔️ | ✔️ |
| 多选媒体 | ✔️ | ✔️ (iOS 14+) | ✔️ | ✔️ |
| 图片压缩 | ✔️ | ✔️ | ❌ | ✔️ |
| 获取元数据 | ✔️ | ✔️ | ❌ | ✔️ |
使用场景
- 用户头像上传
- 图片/视频分享应用
- 社交媒体内容发布
- 文档扫描与识别
- 多媒体文件管理
二、OpenHarmony 适配版本
2.1 环境说明
| 组件 | 版本 |
|---|---|
| Flutter | 3.27.5 |
| HarmonyOS | 6.0 |
| image_picker | 1.1.2 (OpenHarmony 适配版本) |
2.2 引入方式
在 pubspec.yaml 文件中添加以下依赖配置:
yaml
dependencies:
flutter:
sdk: flutter
# image_picker OpenHarmony 适配版本
image_picker:
git:
url: https://atomgit.com/openharmony-tpc/flutter_packages
path: packages/image_picker/image_picker
2.3 获取依赖
配置完成后,在项目根目录执行:
bash
flutter pub get
2.4 权限配置
image_picker 在 OpenHarmony 平台需要配置相机和媒体权限。
打开 ohos/entry/src/main/module.json5,在 requestPermissions 中添加:
json
"requestPermissions": [
{
"name": "ohos.permission.CAMERA",
"reason": "$string:camera_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
},
{
"name": "ohos.permission.READ_IMAGEVIDEO",
"reason": "$string:media_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
},
{
"name": "ohos.permission.WRITE_IMAGEVIDEO",
"reason": "$string:media_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
}
]
在 ohos/entry/src/main/resources/base/element/string.json 中添加权限说明:
json
{
"name": "camera_reason",
"value": "使用相机拍摄照片和视频"
},
{
"name": "media_reason",
"value": "访问相册中的图片和视频"
}
三、核心 API 讲解
3.1 ImagePicker 类
ImagePicker 是插件的核心类,提供了所有图片和视频选择的方法。
dart
import 'package:image_picker/image_picker.dart';
final ImagePicker picker = ImagePicker();
3.2 枚举类型
ImageSource(图片来源)
dart
enum ImageSource {
camera, // 从相机拍摄
gallery, // 从相册选择
}
CameraDevice(相机设备)
dart
enum CameraDevice {
rear, // 后置相机
front, // 前置相机
}
3.3 主要 API 详解
3.3.1 pickImage - 选择单张图片
dart
Future<XFile?> pickImage({
required ImageSource source, // 必填:图片来源(camera 或 gallery)
double? maxWidth, // 可选:图片最大宽度
double? maxHeight, // 可选:图片最大高度
int? imageQuality, // 可选:图片质量 (0-100)
CameraDevice preferredCameraDevice = CameraDevice.rear, // 可选:优先使用的相机
bool requestFullMetadata = true, // 可选:是否请求完整元数据
})
参数说明:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| source | ImageSource | 是 | - | 图片来源:camera(相机)或 gallery(相册) |
| maxWidth | double? | 否 | null | 图片最大宽度,超出会等比缩放 |
| maxHeight | double? | 否 | null | 图片最大高度,超出会等比缩放 |
| imageQuality | int? | 否 | null | 图片质量,范围 0-100,100 为原图质量 |
| preferredCameraDevice | CameraDevice | 否 | CameraDevice.rear | 优先使用的相机:rear(后置)或 front(前置) |
| requestFullMetadata | bool | 否 | true | 是否获取完整元数据(EXIF 信息等) |
返回值: Future<XFile?> - 返回选中的图片文件对象,用户取消时返回 null
使用示例:
dart
// 从相册选择图片
final XFile? image = await ImagePicker().pickImage(
source: ImageSource.gallery,
maxWidth: 1000,
maxHeight: 1000,
imageQuality: 80,
);
// 从相机拍照
final XFile? image = await ImagePicker().pickImage(
source: ImageSource.camera,
preferredCameraDevice: CameraDevice.front,
);
3.3.2 pickMultiImage - 选择多张图片
dart
Future<List<XFile>> pickMultiImage({
double? maxWidth, // 可选:图片最大宽度
double? maxHeight, // 可选:图片最大高度
int? imageQuality, // 可选:图片质量 (0-100)
bool requestFullMetadata = true, // 可选:是否请求完整元数据
})
参数说明:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| maxWidth | double? | 否 | null | 图片最大宽度 |
| maxHeight | double? | 否 | null | 图片最大高度 |
| imageQuality | int? | 否 | null | 图片质量 (0-100) |
| requestFullMetadata | bool | 否 | true | 是否获取完整元数据 |
返回值: Future<List<XFile>> - 返回选中的图片文件列表,用户取消时返回空列表
注意: 此方法在 iOS 14 以下版本不支持,OpenHarmony 平台完全支持。
3.3.3 pickVideo - 选择视频
dart
Future<XFile?> pickVideo({
required ImageSource source, // 必填:图片来源
CameraDevice preferredCameraDevice = CameraDevice.rear, // 可选:优先使用的相机
Duration? maxDuration, // 可选:最大录制时长
})
参数说明:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| source | ImageSource | 是 | - | 图片来源:camera 或 gallery |
| preferredCameraDevice | CameraDevice | 否 | CameraDevice.rear | 优先使用的相机 |
| maxDuration | Duration? | 否 | null | 最大录制时长,不限制则为 null |
返回值: Future<XFile?> - 返回选中的视频文件对象,用户取消时返回 null
使用示例:
dart
// 从相册选择视频
final XFile? video = await ImagePicker().pickVideo(
source: ImageSource.gallery,
);
// 录制视频(最长 30 秒)
final XFile? video = await ImagePicker().pickVideo(
source: ImageSource.camera,
maxDuration: const Duration(seconds: 30),
);
3.3.4 pickMedia - 选择单个媒体(图片或视频)
dart
Future<XFile?> pickMedia({
double? maxWidth, // 可选:图片最大宽度
double? maxHeight, // 可选:图片最大高度
int? imageQuality, // 可选:图片质量 (0-100)
bool requestFullMetadata = true, // 可选:是否请求完整元数据
})
说明: 此方法允许用户从相册选择图片或视频,返回单个媒体文件。
返回值: Future<XFile?> - 返回选中的媒体文件对象,用户取消时返回 null
3.3.5 pickMultipleMedia - 选择多个媒体(图片或视频)
dart
Future<List<XFile>> pickMultipleMedia({
double? maxWidth, // 可选:图片最大宽度
double? maxHeight, // 可选:图片最大高度
int? imageQuality, // 可选:图片质量 (0-100)
bool requestFullMetadata = true, // 可选:是否请求完整元数据
})
说明: 此方法允许用户从相册同时选择多张图片和多个视频。
返回值: Future<List<XFile>> - 返回选中的媒体文件列表,用户取消时返回空列表
3.3.6 supportsImageSource - 检查是否支持图片来源
dart
bool supportsImageSource(ImageSource source)
说明: 检查当前平台是否支持指定的图片来源。
返回值: bool - 支持返回 true,否则返回 false
3.4 XFile 文件对象
XFile 是跨平台文件抽象类,提供了文件操作的标准接口。
常用属性
| 属性 | 类型 | 说明 |
|---|---|---|
| name | String | 文件名(包含扩展名) |
| path | String? | 文件路径(部分平台可能为 null) |
| length | Future<int> |
文件大小(字节) |
| mimeType | String? | 文件的 MIME 类型 |
常用方法
| 方法 | 返回值 | 说明 |
|---|---|---|
| readAsBytes() | Future<Uint8List> |
读取文件内容为字节数组 |
| readAsString() | Future<String> |
读取文件内容为字符串 |
| openRead() | Stream<Uint8List> |
以流的方式读取文件 |
| saveTo(String path) | Future<void> |
将文件保存到指定路径 |
四、完整使用示例

以下是一个完整的图片视频采集应用,整合了所有核心功能:
dart
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
void main() {
runApp(const ImagePickerApp());
}
class ImagePickerApp extends StatelessWidget {
const ImagePickerApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'image_picker 示例',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF6366F1)),
useMaterial3: true,
),
home: const ImagePickerHomePage(),
);
}
}
class ImagePickerHomePage extends StatefulWidget {
const ImagePickerHomePage({super.key});
@override
State<ImagePickerHomePage> createState() => _ImagePickerHomePageState();
}
class _ImagePickerHomePageState extends State<ImagePickerHomePage> {
final ImagePicker _picker = ImagePicker();
XFile? _selectedImage;
List<XFile> _selectedImages = [];
XFile? _selectedVideo;
List<XFile> _selectedMedias = [];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('image_picker 图片视频采集'),
backgroundColor: const Color(0xFF6366F1),
foregroundColor: Colors.white,
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
_buildSectionTitle('单张图片选择'),
_buildButton(
icon: Icons.photo_library,
label: '从相册选择图片',
onTap: _pickImageFromGallery,
),
_buildButton(
icon: Icons.camera_alt,
label: '拍照',
onTap: _takePhoto,
),
if (_selectedImage != null) _buildImagePreview(_selectedImage!),
const SizedBox(height: 24),
_buildSectionTitle('多张图片选择'),
_buildButton(
icon: Icons.photo_library_outlined,
label: '选择多张图片',
onTap: _pickMultipleImages,
),
if (_selectedImages.isNotEmpty) _buildMultipleImagePreview(),
const SizedBox(height: 24),
_buildSectionTitle('视频选择'),
_buildButton(
icon: Icons.video_library,
label: '从相册选择视频',
onTap: _pickVideoFromGallery,
),
_buildButton(
icon: Icons.videocam,
label: '录制视频',
onTap: _recordVideo,
),
if (_selectedVideo != null) _buildVideoPreview(),
const SizedBox(height: 24),
_buildSectionTitle('混合媒体选择'),
_buildButton(
icon: Icons.library_add,
label: '选择单个媒体',
onTap: _pickMedia,
),
_buildButton(
icon: Icons.library_add_check,
label: '选择多个媒体',
onTap: _pickMultipleMedias,
),
if (_selectedMedias.isNotEmpty) _buildMultipleMediaPreview(),
const SizedBox(height: 32),
],
),
);
}
Widget _buildSectionTitle(String title) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Text(
title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFF6366F1),
),
),
);
}
Widget _buildButton({
required IconData icon,
required String label,
required VoidCallback onTap,
}) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: ElevatedButton.icon(
onPressed: onTap,
icon: Icon(icon, color: Colors.white),
label: Text(label),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF6366F1),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
);
}
Widget _buildImagePreview(XFile image) {
return Card(
margin: const EdgeInsets.only(top: 12),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'选中的图片: ${image.name}',
style: const TextStyle(fontWeight: FontWeight.w600),
),
const SizedBox(height: 8),
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.file(
File(image.path!),
height: 200,
width: double.infinity,
fit: BoxFit.cover,
),
),
const SizedBox(height: 8),
Text('路径: ${image.path}'),
FutureBuilder<int>(
future: image.length(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text('大小: ${_formatFileSize(snapshot.data!)}');
}
return const Text('大小: 计算中...');
},
),
],
),
),
);
}
Widget _buildMultipleImagePreview() {
return Card(
margin: const EdgeInsets.only(top: 12),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'选中的图片: ${_selectedImages.length} 张',
style: const TextStyle(fontWeight: FontWeight.w600),
),
const SizedBox(height: 8),
SizedBox(
height: 150,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: _selectedImages.length,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.only(right: 8),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.file(
File(_selectedImages[index].path!),
width: 150,
height: 150,
fit: BoxFit.cover,
),
),
);
},
),
),
],
),
),
);
}
Widget _buildVideoPreview() {
return Card(
margin: const EdgeInsets.only(top: 12),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'选中的视频',
style: TextStyle(fontWeight: FontWeight.w600),
),
const SizedBox(height: 8),
Text('文件名: ${_selectedVideo!.name}'),
Text('路径: ${_selectedVideo!.path}'),
FutureBuilder<int>(
future: _selectedVideo!.length(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text('大小: ${_formatFileSize(snapshot.data!)}');
}
return const Text('大小: 计算中...');
},
),
],
),
),
);
}
Widget _buildMultipleMediaPreview() {
return Card(
margin: const EdgeInsets.only(top: 12),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'选中的媒体: ${_selectedMedias.length} 个',
style: const TextStyle(fontWeight: FontWeight.w600),
),
const SizedBox(height: 8),
SizedBox(
height: 100,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: _selectedMedias.length,
itemBuilder: (context, index) {
final media = _selectedMedias[index];
final isVideo = media.name.endsWith('.mp4') ||
media.name.endsWith('.mov') ||
media.name.endsWith('.avi');
return Padding(
padding: const EdgeInsets.only(right: 8),
child: Stack(
alignment: Alignment.center,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.file(
File(media.path!),
width: 100,
height: 100,
fit: BoxFit.cover,
),
),
if (isVideo)
Container(
decoration: BoxDecoration(
color: Colors.black54,
shape: BoxShape.circle,
),
padding: const EdgeInsets.all(8),
child: const Icon(
Icons.play_arrow,
color: Colors.white,
size: 24,
),
),
],
),
);
},
),
),
],
),
),
);
}
String _formatFileSize(int bytes) {
if (bytes < 1024) return '$bytes B';
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(2)} KB';
return '${(bytes / (1024 * 1024)).toStringAsFixed(2)} MB';
}
Future<void> _pickImageFromGallery() async {
final XFile? image = await _picker.pickImage(
source: ImageSource.gallery,
maxWidth: 1920,
maxHeight: 1080,
imageQuality: 85,
);
if (image != null && mounted) {
setState(() {
_selectedImage = image;
});
}
}
Future<void> _takePhoto() async {
final XFile? image = await _picker.pickImage(
source: ImageSource.camera,
preferredCameraDevice: CameraDevice.rear,
imageQuality: 90,
);
if (image != null && mounted) {
setState(() {
_selectedImage = image;
});
}
}
Future<void> _pickMultipleImages() async {
final List<XFile> images = await _picker.pickMultiImage(
maxWidth: 1920,
maxHeight: 1080,
imageQuality: 85,
);
if (images.isNotEmpty && mounted) {
setState(() {
_selectedImages = images;
});
}
}
Future<void> _pickVideoFromGallery() async {
final XFile? video = await _picker.pickVideo(
source: ImageSource.gallery,
);
if (video != null && mounted) {
setState(() {
_selectedVideo = video;
});
}
}
Future<void> _recordVideo() async {
final XFile? video = await _picker.pickVideo(
source: ImageSource.camera,
maxDuration: const Duration(seconds: 60),
);
if (video != null && mounted) {
setState(() {
_selectedVideo = video;
});
}
}
Future<void> _pickMedia() async {
final XFile? media = await _picker.pickMedia();
if (media != null && mounted) {
setState(() {
_selectedMedias = [media];
});
}
}
Future<void> _pickMultipleMedias() async {
final List<XFile> medias = await _picker.pickMultipleMedia();
if (medias.isNotEmpty && mounted) {
setState(() {
_selectedMedias = medias;
});
}
}
}
五、适配要点
5.1 OpenHarmony 平台特性
-
权限处理
- OpenHarmony 平台需要显式声明相机和媒体权限
- 系统会在首次访问时自动弹出权限请求对话框
- 权限被拒绝后,需要在系统设置中手动开启
-
文件路径处理
- OpenHarmony 平台返回的文件路径为沙箱路径
- 路径仅在应用生命周期内有效,不应跨会话保存
- 如需持久化存储,应使用
saveTo方法复制文件
-
图片压缩
maxWidth和maxHeight参数会保持原始宽高比imageQuality参数仅对 JPEG 格式有效- HEIC 格式在 OpenHarmony 平台的处理与 Android 有所不同
-
相机设备选择
preferredCameraDevice参数在部分设备上可能被忽略- 建议在使用前检查设备是否支持指定相机
5.2 与 Android/iOS 的差异
| 差异点 | Android/iOS | OpenHarmony |
|---|---|---|
| 权限申请 | 运行时动态申请 | 声明式权限,首次使用时弹窗 |
| 文件路径 | 持久化路径 | 沙箱临时路径 |
| HEIC 支持 | iOS 支持,Android 8 不支持 | 支持,但需配合尺寸修改使用 |
| 元数据获取 | 完整 EXIF 信息 | 部分元数据可能需要额外权限 |
| 视频格式 | 多种格式支持 | 主要支持 MP4 格式 |
5.3 注意事项
-
不要保存文件路径
XFile的路径仅在应用会话内有效- 跨会话使用应复制文件到持久化目录
-
错误处理
- 所有方法都可能抛出
PlatformException - 建议使用 try-catch 包裹调用代码
- 所有方法都可能抛出
-
内存管理
- 大图片建议使用
maxWidth/maxHeight限制 - 多选时注意内存占用,必要时进行压缩
- 大图片建议使用
-
UI 线程
- 图片选择器会阻塞 UI,直到用户完成选择
- 建议在异步方法中调用
六、常见问题
Q1: 调用相机拍照时应用崩溃?
原因: 未配置相机权限或权限被拒绝。
解决方案:
- 检查
module.json5中是否添加了ohos.permission.CAMERA权限 - 确保在
string.json中添加了权限说明 - 在系统设置中检查应用权限是否已授予
Q2: 选择的图片无法显示?
原因: 文件路径为沙箱临时路径,可能已被清理。
解决方案:
dart
// 错误做法:直接保存路径
String path = image.path!; // 跨会话可能失效
// 正确做法:复制文件到持久化目录
import 'package:path_provider/path_provider.dart';
final directory = await getApplicationDocumentsDirectory();
final newPath = '${directory.path}/${image.name}';
await image.saveTo(newPath);
Q3: 图片压缩不生效?
原因: imageQuality 参数仅对 JPEG 格式有效,PNG 格式不支持压缩。
解决方案:
- 确认选择的图片格式是否为 JPEG
- 如果需要对 PNG 进行压缩,可以先转换为 JPEG:
dart
final XFile? image = await _picker.pickImage(
source: ImageSource.gallery,
imageQuality: 80, // 仅对 JPEG 有效
);
Q4: 多选图片时返回空列表?
原因: 用户取消了选择或相册中没有符合条件的图片。
解决方案:
dart
final List<XFile> images = await _picker.pickMultiImage();
if (images.isEmpty) {
// 用户取消或没有选择图片
return;
}
// 处理选中的图片
Q5: 视频录制时长限制不生效?
原因: maxDuration 参数在某些设备上可能被系统相机忽略。
解决方案:
maxDuration是建议值,系统相机可能不严格遵守- 建议在录制完成后检查视频时长并进行处理
Q6: 如何判断选中的文件是图片还是视频?
解决方案:
dart
bool isVideoFile(XFile file) {
final extension = file.name.split('.').last.toLowerCase();
return ['mp4', 'mov', 'avi', 'mkv', 'webm'].contains(extension);
}
// 使用示例
if (isVideoFile(media)) {
// 处理视频
} else {
// 处理图片
}
Q7: 应用重启后如何恢复之前选择的图片?
解决方案:
dart
// 保存时复制文件到持久化目录
Future<String> saveImagePermanently(XFile image) async {
final directory = await getApplicationDocumentsDirectory();
final newPath = '${directory.path}/${DateTime.now().millisecondsSinceEpoch}_${image.name}';
await image.saveTo(newPath);
return newPath;
}
// 使用时从持久化目录加载
Image.file(File(savedPath));
七、总结
image_picker 是 Flutter 生态中最常用的图片视频采集插件,在 OpenHarmony 平台的适配已经非常成熟。通过本文的介绍,你应该已经掌握了:
- image_picker 的核心 API 和使用方法
- OpenHarmony 平台的权限配置要求
- 完整的图片视频采集应用实现
- 平台适配的注意事项和常见问题解决方案
在实际开发中,建议根据具体需求选择合适的 API,并注意文件路径的临时性特点。对于需要持久化存储的场景,务必使用 saveTo 方法将文件复制到应用的持久化目录中。
💡 提示: 更多 OpenHarmony 适配的 Flutter 三方库信息,请访问 开源鸿蒙跨平台开发者社区 获取最新资源和技术支持。