进阶实战 Flutter for OpenHarmony:image_picker 第三方库实战 - 图片选择

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net


🔍 一、第三方库概述与应用场景

📱 1.1 为什么需要图片选择功能?

在现代移动应用开发中,图片选择功能几乎已经成为标配。无论是社交应用的用户头像上传、电商应用的商品图片发布、还是内容创作平台的图文编辑,都需要用户从设备中选择或拍摄图片。一个优秀的图片选择体验,能够显著提升用户满意度和应用的专业程度。

想象一下这样的场景:用户想要更换自己的头像,打开你的应用后,点击头像区域,系统弹出一个优雅的选择面板,用户可以选择从相机拍照或从相册选择。选择完成后,图片自动压缩到合适的大小,裁剪成圆形头像,并上传到服务器。整个过程流畅自然,用户无需关心技术细节,只需要专注于自己的需求。

这就是 image_picker 库要解决的问题。它提供了一套统一的 API,让开发者可以轻松实现跨平台的图片选择功能,同时屏蔽了不同平台的底层差异。

📋 1.2 image_picker 是什么?

image_picker 是 Flutter 官方维护的一个核心插件,专门用于在应用内选择图片和视频。它支持从相机拍摄、相册选择等多种来源,提供了丰富的配置选项,如图片质量压缩、尺寸限制、多选模式等。

在 OpenHarmony 平台上,image_picker 同样提供了完整的支持,让开发者可以无缝地使用这套 API 来实现图片和视频选择功能。无论是单图选择、多图选择,还是视频录制,都可以通过简单的 API 调用来完成。

🎯 1.3 核心功能特性

功能特性 详细说明 OpenHarmony 支持
单图选择 从相机或相册选择单张图片 ✅ 完全支持
多图选择 一次选择多张图片 ✅ 完全支持
视频选择 从相机录制或相册选择视频 ✅ 完全支持
图片压缩 选择时自动压缩图片质量和尺寸 ✅ 完全支持
媒体混合 同时选择图片和视频 ✅ 完全支持
前后摄像头 支持指定前置或后置摄像头 ✅ 完全支持
录制时长限制 限制视频录制的最大时长 ✅ 完全支持

💡 1.4 典型应用场景

在实际的应用开发中,image_picker 有着广泛的应用场景:

社交应用:用户头像上传、动态图片发布、聊天图片发送、朋友圈图片分享等。用户可以随时记录生活中的精彩瞬间,与好友分享。

电商应用:商品图片上传、评价晒单、客服反馈图片、身份认证照片等。商家可以快速上传商品图片,买家可以晒出购买的商品。

内容创作:图文编辑、文章配图、笔记插图、作品展示等。创作者可以为自己的内容添加精美的配图,提升内容的吸引力。

企业应用:工单照片上传、巡检记录图片、报销凭证拍照、合同文档扫描等。员工可以方便地记录工作现场情况。


🏗️ 二、系统架构设计

📐 2.1 整体架构

为了构建一个可维护、可扩展的图片选择系统,我们采用分层架构设计:

复制代码
┌─────────────────────────────────────────────────────────┐
│                    UI 层 (展示层)                        │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐     │
│  │  图片预览   │  │  选择面板   │  │  操作按钮   │     │
│  └─────────────┘  └─────────────┘  └─────────────┘     │
├─────────────────────────────────────────────────────────┤
│                  服务层 (业务逻辑)                       │
│  ┌─────────────────────────────────────────────────┐   │
│  │              ImagePickerService                  │   │
│  │  • 统一的图片选择接口                            │   │
│  │  • 图片压缩与尺寸处理                            │   │
│  │  • 错误处理与重试机制                            │   │
│  │  • 选择结果缓存管理                              │   │
│  └─────────────────────────────────────────────────┘   │
├─────────────────────────────────────────────────────────┤
│                  基础设施层 (底层实现)                   │
│  ┌─────────────────────────────────────────────────┐   │
│  │              image_picker 插件                   │   │
│  │  • pickImage() - 选择图片                        │   │
│  │  • pickMultiImage() - 选择多张图片               │   │
│  │  • pickVideo() - 选择视频                        │   │
│  │  • pickMedia() - 选择媒体                        │   │
│  └─────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────┘

📊 2.2 数据模型设计

为了更好地管理图片选择的结果和配置,我们设计了一套数据模型:

dart 复制代码
/// 媒体类型枚举
enum MediaType {
  image,    // 图片
  video,    // 视频
}

/// 图片来源枚举
enum ImageSource {
  camera,   // 相机
  gallery,  // 相册
}

/// 选择配置模型
class PickerConfig {
  /// 图片来源
  final ImageSource source;
  
  /// 最大宽度
  final int? maxWidth;
  
  /// 最大高度
  final int? maxHeight;
  
  /// 图片质量 (0-100)
  final int? imageQuality;
  
  /// 最大录制时长(视频)
  final Duration? maxDuration;
  
  /// 是否支持多选
  final bool allowMultiple;
  
  const PickerConfig({
    required this.source,
    this.maxWidth,
    this.maxHeight,
    this.imageQuality,
    this.maxDuration,
    this.allowMultiple = false,
  });
}

/// 媒体文件模型
class MediaFile {
  /// 文件路径
  final String path;
  
  /// 文件名
  final String name;
  
  /// 媒体类型
  final MediaType type;
  
  /// 文件大小(字节)
  final int size;
  
  /// 创建时间
  final DateTime createdAt;
  
  const MediaFile({
    required this.path,
    required this.name,
    required this.type,
    required this.size,
    required this.createdAt,
  });
  
  /// 格式化的文件大小
  String get formattedSize {
    if (size < 1024) return '$size B';
    if (size < 1024 * 1024) return '${(size / 1024).toStringAsFixed(1)} KB';
    return '${(size / (1024 * 1024)).toStringAsFixed(1)} MB';
  }
}

📦 三、项目配置与依赖安装

📥 3.1 添加依赖

在 Flutter 项目中使用 image_picker,需要在 pubspec.yaml 文件中添加依赖。由于我们要支持 OpenHarmony 平台,需要使用适配版本的仓库。

打开项目根目录下的 pubspec.yaml 文件,找到 dependencies 部分,添加以下配置:

yaml 复制代码
dependencies:
  flutter:
    sdk: flutter
  
  # image_picker - 图片选择插件
  # 使用 OpenHarmony 适配版本
  image_picker:
    git:
      url: https://atomgit.com/openharmony-tpc/flutter_packages.git
      path: packages/image_picker/image_picker

配置说明

  • git 方式引用:因为 OpenHarmony 适配版本需要从指定的 Git 仓库获取
  • url:指向开源鸿蒙 TPC 维护的 flutter_packages 仓库
  • path:指定仓库中 image_picker 包的具体路径
  • 本项目基于 image_picker@1.1.2 开发,适配 Flutter 3.27.5-ohos-1.0.4

🔧 3.2 权限配置

在 OpenHarmony 平台上,访问相册或相机需要配置相应的权限。权限配置分为两个步骤:

步骤一:配置权限声明

ohos/entry/src/main/module.json5 文件中添加权限声明:

json5 复制代码
{
  "module": {
    "name": "entry",
    "type": "entry",
    "requestPermissions": [
      {
        "name": "ohos.permission.READ_MEDIA",
        "reason": "$string:read_media_reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      },
      {
        "name": "ohos.permission.CAMERA",
        "reason": "$string:camera_reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      }
    ]
  }
}
步骤二:添加字符串资源

ohos/entry/src/main/resources/base/element/string.json 文件中添加权限说明字符串:

json 复制代码
{
  "string": [
    {
      "name": "read_media_reason",
      "value": "用于选择和读取图片、视频等媒体文件"
    },
    {
      "name": "camera_reason",
      "value": "用于拍摄照片和录制视频"
    }
  ]
}

权限说明

权限 说明 用途
ohos.permission.READ_MEDIA 读取媒体文件 从相册选择图片和视频
ohos.permission.CAMERA 相机权限 拍摄照片和录制视频

⚠️ 重要提示reason 字段使用 $string:xxx 格式引用字符串资源,这是 OpenHarmony 的规范要求,必须在 string.json 中定义对应的字符串,否则可能导致编译失败或权限申请异常。


🛠️ 四、核心服务实现

📸 4.1 图片选择服务

首先,我们实现一个图片选择服务,封装 image_picker 的底层 API:

dart 复制代码
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';

/// 图片选择服务
/// 
/// 该服务封装了 image_picker 的底层 API,提供统一的图片选择接口。
/// 所有方法都是静态的,可以在应用的任何地方直接调用。
class ImagePickerService {
  /// ImagePicker 实例
  static final ImagePicker _picker = ImagePicker();
  
  /// 缓存的选择结果
  static final List<XFile> _selectedFiles = [];
  
  /// 获取已选择的文件
  static List<XFile> get selectedFiles => List.unmodifiable(_selectedFiles);

  /// 从相机拍照
  /// 
  /// [maxWidth] 最大宽度
  /// [maxHeight] 最大高度
  /// [imageQuality] 图片质量 (0-100)
  /// [preferredCameraDevice] 首选摄像头
  static Future<XFile?> takePhoto({
    int? maxWidth,
    int? maxHeight,
    int? imageQuality,
    CameraDevice preferredCameraDevice = CameraDevice.rear,
  }) async {
    try {
      final XFile? photo = await _picker.pickImage(
        source: ImageSource.camera,
        maxWidth: maxWidth?.toDouble(),
        maxHeight: maxHeight?.toDouble(),
        imageQuality: imageQuality,
        preferredCameraDevice: preferredCameraDevice,
      );
    
      if (photo != null) {
        _selectedFiles.clear();
        _selectedFiles.add(photo);
      }
    
      return photo;
    } catch (e) {
      debugPrint('拍照失败: $e');
      return null;
    }
  }

  /// 从相册选择单张图片
  /// 
  /// [maxWidth] 最大宽度
  /// [maxHeight] 最大高度
  /// [imageQuality] 图片质量 (0-100)
  static Future<XFile?> pickSingleImage({
    int? maxWidth,
    int? maxHeight,
    int? imageQuality,
  }) async {
    try {
      final XFile? image = await _picker.pickImage(
        source: ImageSource.gallery,
        maxWidth: maxWidth?.toDouble(),
        maxHeight: maxHeight?.toDouble(),
        imageQuality: imageQuality,
      );
    
      if (image != null) {
        _selectedFiles.clear();
        _selectedFiles.add(image);
      }
    
      return image;
    } catch (e) {
      debugPrint('选择图片失败: $e');
      return null;
    }
  }

  /// 从相册选择多张图片
  /// 
  /// [maxWidth] 最大宽度
  /// [maxHeight] 最大高度
  /// [imageQuality] 图片质量 (0-100)
  static Future<List<XFile>> pickMultipleImages({
    int? maxWidth,
    int? maxHeight,
    int? imageQuality,
  }) async {
    try {
      final List<XFile> images = await _picker.pickMultiImage(
        maxWidth: maxWidth?.toDouble(),
        maxHeight: maxHeight?.toDouble(),
        imageQuality: imageQuality,
      );
    
      if (images.isNotEmpty) {
        _selectedFiles.clear();
        _selectedFiles.addAll(images);
      }
    
      return images;
    } catch (e) {
      debugPrint('选择多张图片失败: $e');
      return [];
    }
  }

  /// 录制视频
  /// 
  /// [maxDuration] 最大录制时长
  /// [preferredCameraDevice] 首选摄像头
  static Future<XFile?> recordVideo({
    Duration? maxDuration,
    CameraDevice preferredCameraDevice = CameraDevice.rear,
  }) async {
    try {
      final XFile? video = await _picker.pickVideo(
        source: ImageSource.camera,
        maxDuration: maxDuration,
        preferredCameraDevice: preferredCameraDevice,
      );
    
      if (video != null) {
        _selectedFiles.clear();
        _selectedFiles.add(video);
      }
    
      return video;
    } catch (e) {
      debugPrint('录制视频失败: $e');
      return null;
    }
  }

  /// 从相册选择视频
  static Future<XFile?> pickVideo() async {
    try {
      final XFile? video = await _picker.pickVideo(
        source: ImageSource.gallery,
      );
    
      if (video != null) {
        _selectedFiles.clear();
        _selectedFiles.add(video);
      }
    
      return video;
    } catch (e) {
      debugPrint('选择视频失败: $e');
      return null;
    }
  }

  /// 选择图片或视频(混合模式)
  static Future<XFile?> pickMedia() async {
    try {
      final XFile? media = await _picker.pickMedia();
    
      if (media != null) {
        _selectedFiles.clear();
        _selectedFiles.add(media);
      }
    
      return media;
    } catch (e) {
      debugPrint('选择媒体失败: $e');
      return null;
    }
  }

  /// 选择多个图片或视频
  static Future<List<XFile>> pickMultipleMedia() async {
    try {
      final List<XFile> media = await _picker.pickMultipleMedia();
    
      if (media.isNotEmpty) {
        _selectedFiles.clear();
        _selectedFiles.addAll(media);
      }
    
      return media;
    } catch (e) {
      debugPrint('选择多个媒体失败: $e');
      return [];
    }
  }

  /// 获取文件信息
  static Future<MediaFile?> getFileInfo(XFile file) async {
    try {
      final stat = await File(file.path).stat();
      final extension = file.path.split('.').last.toLowerCase();
    
      return MediaFile(
        path: file.path,
        name: file.name,
        type: _getMediaType(extension),
        size: stat.size,
        createdAt: stat.changed,
      );
    } catch (e) {
      debugPrint('获取文件信息失败: $e');
      return null;
    }
  }

  /// 清除缓存
  static void clearCache() {
    _selectedFiles.clear();
  }

  /// 根据扩展名判断媒体类型
  static MediaType _getMediaType(String extension) {
    const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'heic'];
    const videoExts = ['mp4', 'mov', 'avi', 'mkv', 'webm', '3gp'];
  
    if (imageExts.contains(extension)) return MediaType.image;
    if (videoExts.contains(extension)) return MediaType.video;
    return MediaType.image;
  }
}

📋 4.2 选择配置管理

接下来,我们创建一个选择配置管理类,定义常用的配置预设:

dart 复制代码
/// 媒体类型枚举
enum MediaType {
  image,
  video,
}

/// 媒体文件模型
class MediaFile {
  final String path;
  final String name;
  final MediaType type;
  final int size;
  final DateTime createdAt;

  const MediaFile({
    required this.path,
    required this.name,
    required this.type,
    required this.size,
    required this.createdAt,
  });

  String get formattedSize {
    if (size < 1024) return '$size B';
    if (size < 1024 * 1024) return '${(size / 1024).toStringAsFixed(1)} KB';
    return '${(size / (1024 * 1024)).toStringAsFixed(1)} MB';
  }
}

/// 选择配置预设
class PickerPresets {
  /// 头像选择配置(正方形、压缩)
  static const avatar = PickerConfig(
    source: ImageSource.gallery,
    maxWidth: 512,
    maxHeight: 512,
    imageQuality: 85,
  );

  /// 高清图片配置
  static const hdImage = PickerConfig(
    source: ImageSource.gallery,
    maxWidth: 1920,
    maxHeight: 1080,
    imageQuality: 90,
  );

  /// 缩略图配置
  static const thumbnail = PickerConfig(
    source: ImageSource.gallery,
    maxWidth: 256,
    maxHeight: 256,
    imageQuality: 70,
  );

  /// 多图选择配置
  static const multiImage = PickerConfig(
    source: ImageSource.gallery,
    maxWidth: 1280,
    maxHeight: 720,
    imageQuality: 80,
    allowMultiple: true,
  );

  /// 短视频配置(30秒)
  static const shortVideo = PickerConfig(
    source: ImageSource.camera,
    maxDuration: Duration(seconds: 30),
  );
}

/// 选择配置模型
class PickerConfig {
  final ImageSource source;
  final int? maxWidth;
  final int? maxHeight;
  final int? imageQuality;
  final Duration? maxDuration;
  final bool allowMultiple;

  const PickerConfig({
    required this.source,
    this.maxWidth,
    this.maxHeight,
    this.imageQuality,
    this.maxDuration,
    this.allowMultiple = false,
  });
}

📝 五、完整示例代码

下面是一个完整的智能图片选择与管理系统示例:

dart 复制代码
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';

// ============ 枚举定义 ============

enum MediaType {
  image,
  video,
}

// ============ 数据模型 ============

class MediaFile {
  final String path;
  final String name;
  final MediaType type;
  final int size;
  final DateTime createdAt;

  const MediaFile({
    required this.path,
    required this.name,
    required this.type,
    required this.size,
    required this.createdAt,
  });

  String get formattedSize {
    if (size < 1024) return '$size B';
    if (size < 1024 * 1024) return '${(size / 1024).toStringAsFixed(1)} KB';
    return '${(size / (1024 * 1024)).toStringAsFixed(1)} MB';
  }
}

class PickerConfig {
  final ImageSource source;
  final int? maxWidth;
  final int? maxHeight;
  final int? imageQuality;
  final Duration? maxDuration;
  final bool allowMultiple;

  const PickerConfig({
    required this.source,
    this.maxWidth,
    this.maxHeight,
    this.imageQuality,
    this.maxDuration,
    this.allowMultiple = false,
  });
}

class PickerPresets {
  static const avatar = PickerConfig(
    source: ImageSource.gallery,
    maxWidth: 512,
    maxHeight: 512,
    imageQuality: 85,
  );

  static const hdImage = PickerConfig(
    source: ImageSource.gallery,
    maxWidth: 1920,
    maxHeight: 1080,
    imageQuality: 90,
  );

  static const thumbnail = PickerConfig(
    source: ImageSource.gallery,
    maxWidth: 256,
    maxHeight: 256,
    imageQuality: 70,
  );
}

// ============ 服务类 ============

class ImagePickerService {
  static final ImagePicker _picker = ImagePicker();
  static final List<XFile> _selectedFiles = [];
  static List<XFile> get selectedFiles => List.unmodifiable(_selectedFiles);

  static Future<XFile?> takePhoto({
    int? maxWidth,
    int? maxHeight,
    int? imageQuality,
    CameraDevice preferredCameraDevice = CameraDevice.rear,
  }) async {
    try {
      final XFile? photo = await _picker.pickImage(
        source: ImageSource.camera,
        maxWidth: maxWidth?.toDouble(),
        maxHeight: maxHeight?.toDouble(),
        imageQuality: imageQuality,
        preferredCameraDevice: preferredCameraDevice,
      );

      if (photo != null) {
        _selectedFiles.clear();
        _selectedFiles.add(photo);
      }

      return photo;
    } catch (e) {
      debugPrint('拍照失败: $e');
      return null;
    }
  }

  static Future<XFile?> pickSingleImage({
    int? maxWidth,
    int? maxHeight,
    int? imageQuality,
  }) async {
    try {
      final XFile? image = await _picker.pickImage(
        source: ImageSource.gallery,
        maxWidth: maxWidth?.toDouble(),
        maxHeight: maxHeight?.toDouble(),
        imageQuality: imageQuality,
      );

      if (image != null) {
        _selectedFiles.clear();
        _selectedFiles.add(image);
      }

      return image;
    } catch (e) {
      debugPrint('选择图片失败: $e');
      return null;
    }
  }

  static Future<List<XFile>> pickMultipleImages({
    int? maxWidth,
    int? maxHeight,
    int? imageQuality,
  }) async {
    try {
      final List<XFile> images = await _picker.pickMultiImage(
        maxWidth: maxWidth?.toDouble(),
        maxHeight: maxHeight?.toDouble(),
        imageQuality: imageQuality,
      );

      if (images.isNotEmpty) {
        _selectedFiles.clear();
        _selectedFiles.addAll(images);
      }

      return images;
    } catch (e) {
      debugPrint('选择多张图片失败: $e');
      return [];
    }
  }

  static Future<XFile?> recordVideo({
    Duration? maxDuration,
    CameraDevice preferredCameraDevice = CameraDevice.rear,
  }) async {
    try {
      final XFile? video = await _picker.pickVideo(
        source: ImageSource.camera,
        maxDuration: maxDuration,
        preferredCameraDevice: preferredCameraDevice,
      );

      if (video != null) {
        _selectedFiles.clear();
        _selectedFiles.add(video);
      }

      return video;
    } catch (e) {
      debugPrint('录制视频失败: $e');
      return null;
    }
  }

  static Future<XFile?> pickVideo() async {
    try {
      final XFile? video = await _picker.pickVideo(
        source: ImageSource.gallery,
      );

      if (video != null) {
        _selectedFiles.clear();
        _selectedFiles.add(video);
      }

      return video;
    } catch (e) {
      debugPrint('选择视频失败: $e');
      return null;
    }
  }

  static Future<XFile?> pickMedia() async {
    try {
      final XFile? media = await _picker.pickMedia();

      if (media != null) {
        _selectedFiles.clear();
        _selectedFiles.add(media);
      }

      return media;
    } catch (e) {
      debugPrint('选择媒体失败: $e');
      return null;
    }
  }

  static Future<List<XFile>> pickMultipleMedia() async {
    try {
      final List<XFile> media = await _picker.pickMultipleMedia();

      if (media.isNotEmpty) {
        _selectedFiles.clear();
        _selectedFiles.addAll(media);
      }

      return media;
    } catch (e) {
      debugPrint('选择多个媒体失败: $e');
      return [];
    }
  }

  static Future<MediaFile?> getFileInfo(XFile file) async {
    try {
      final stat = await File(file.path).stat();
      final extension = file.path.split('.').last.toLowerCase();

      return MediaFile(
        path: file.path,
        name: file.name,
        type: _getMediaType(extension),
        size: stat.size,
        createdAt: stat.changed,
      );
    } catch (e) {
      debugPrint('获取文件信息失败: $e');
      return null;
    }
  }

  static void clearCache() {
    _selectedFiles.clear();
  }

  static MediaType _getMediaType(String extension) {
    const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'];
    const videoExts = ['mp4', 'mov', 'avi', 'mkv', 'webm'];

    if (imageExts.contains(extension)) return MediaType.image;
    if (videoExts.contains(extension)) return MediaType.video;
    return MediaType.image;
  }
}

// ============ 应用入口 ============

void main() {
  runApp(const ImagePickerApp());
}

class ImagePickerApp extends StatelessWidget {
  const ImagePickerApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '图片选择系统',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: const ImagePickerPage(),
    );
  }
}

class ImagePickerPage extends StatefulWidget {
  const ImagePickerPage({super.key});

  @override
  State<ImagePickerPage> createState() => _ImagePickerPageState();
}

class _ImagePickerPageState extends State<ImagePickerPage> {
  List<XFile> _imageFiles = [];
  XFile? _videoFile;
  String? _error;
  bool _isLoading = false;

  Future<void> _executeAction(Future<dynamic> Function() action) async {
    setState(() => _isLoading = true);
    try {
      await action();
      setState(() => _error = null);
    } catch (e) {
      setState(() => _error = e.toString());
    } finally {
      if (mounted) {
        setState(() => _isLoading = false);
      }
    }
  }

  Future<void> _takePhoto() async {
    await _executeAction(() async {
      final photo = await ImagePickerService.takePhoto(
        maxWidth: 1280,
        maxHeight: 720,
        imageQuality: 85,
      );
      if (photo != null) {
        setState(() {
          _imageFiles = [photo];
          _videoFile = null;
        });
      }
    });
  }

  Future<void> _pickSingleImage() async {
    await _executeAction(() async {
      final image = await ImagePickerService.pickSingleImage(
        maxWidth: 1280,
        maxHeight: 720,
        imageQuality: 85,
      );
      if (image != null) {
        setState(() {
          _imageFiles = [image];
          _videoFile = null;
        });
      }
    });
  }

  Future<void> _pickMultipleImages() async {
    await _executeAction(() async {
      final images = await ImagePickerService.pickMultipleImages(
        maxWidth: 1280,
        maxHeight: 720,
        imageQuality: 80,
      );
      if (images.isNotEmpty) {
        setState(() {
          _imageFiles = images;
          _videoFile = null;
        });
      }
    });
  }

  Future<void> _recordVideo() async {
    await _executeAction(() async {
      final video = await ImagePickerService.recordVideo(
        maxDuration: const Duration(seconds: 30),
      );
      if (video != null) {
        setState(() {
          _videoFile = video;
          _imageFiles = [];
        });
      }
    });
  }

  Future<void> _pickVideo() async {
    await _executeAction(() async {
      final video = await ImagePickerService.pickVideo();
      if (video != null) {
        setState(() {
          _videoFile = video;
          _imageFiles = [];
        });
      }
    });
  }

  Future<void> _pickMedia() async {
    await _executeAction(() async {
      final media = await ImagePickerService.pickMedia();
      if (media != null) {
        final info = await ImagePickerService.getFileInfo(media);
        setState(() {
          if (info?.type == MediaType.video) {
            _videoFile = media;
            _imageFiles = [];
          } else {
            _imageFiles = [media];
            _videoFile = null;
          }
        });
      }
    });
  }

  void _clearAll() {
    setState(() {
      _imageFiles = [];
      _videoFile = null;
      _error = null;
    });
    ImagePickerService.clearCache();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('图片选择系统'),
        centerTitle: true,
        elevation: 0,
        actions: [
          if (_imageFiles.isNotEmpty || _videoFile != null)
            IconButton(
              icon: const Icon(Icons.clear),
              onPressed: _clearAll,
              tooltip: '清除所有',
            ),
        ],
      ),
      body: _isLoading
          ? const Center(child: CircularProgressIndicator())
          : Container(
              decoration: BoxDecoration(
                gradient: LinearGradient(
                  begin: Alignment.topCenter,
                  end: Alignment.bottomCenter,
                  colors: [
                    Colors.blue.shade50,
                    Colors.purple.shade50,
                  ],
                ),
              ),
              child: SingleChildScrollView(
                padding: const EdgeInsets.all(16),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    _buildImageSection(),
                    const SizedBox(height: 16),
                    _buildVideoSection(),
                    const SizedBox(height: 16),
                    _buildMediaSection(),
                    const SizedBox(height: 16),
                    if (_error != null) _buildErrorCard(),
                    const SizedBox(height: 16),
                    if (_imageFiles.isNotEmpty) _buildImagePreview(),
                    if (_videoFile != null) _buildVideoPreview(),
                    const SizedBox(height: 32),
                  ],
                ),
              ),
            ),
    );
  }

  Widget _buildImageSection() {
    return _buildSectionCard(
      title: '图片选择',
      icon: Icons.photo_library,
      color: Colors.blue,
      child: Column(
        children: [
          Row(
            children: [
              Expanded(
                child: ElevatedButton.icon(
                  onPressed: _takePhoto,
                  icon: const Icon(Icons.camera_alt),
                  label: const Text('拍照'),
                ),
              ),
              const SizedBox(width: 8),
              Expanded(
                child: OutlinedButton.icon(
                  onPressed: _pickSingleImage,
                  icon: const Icon(Icons.photo),
                  label: const Text('相册'),
                ),
              ),
            ],
          ),
          const SizedBox(height: 8),
          SizedBox(
            width: double.infinity,
            child: ElevatedButton.icon(
              onPressed: _pickMultipleImages,
              icon: const Icon(Icons.photo_library),
              label: const Text('选择多张图片'),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildVideoSection() {
    return _buildSectionCard(
      title: '视频选择',
      icon: Icons.videocam,
      color: Colors.purple,
      child: Row(
        children: [
          Expanded(
            child: ElevatedButton.icon(
              onPressed: _recordVideo,
              icon: const Icon(Icons.videocam),
              label: const Text('录制视频'),
            ),
          ),
          const SizedBox(width: 8),
          Expanded(
            child: OutlinedButton.icon(
              onPressed: _pickVideo,
              icon: const Icon(Icons.movie),
              label: const Text('选择视频'),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildMediaSection() {
    return _buildSectionCard(
      title: '混合选择',
      icon: Icons.perm_media,
      color: Colors.orange,
      child: SizedBox(
        width: double.infinity,
        child: ElevatedButton.icon(
          onPressed: _pickMedia,
          icon: const Icon(Icons.select_all),
          label: const Text('选择图片或视频'),
        ),
      ),
    );
  }

  Widget _buildErrorCard() {
    return Container(
      padding: const EdgeInsets.all(12),
      decoration: BoxDecoration(
        color: Colors.red.shade50,
        borderRadius: BorderRadius.circular(8),
        border: Border.all(color: Colors.red.shade200),
      ),
      child: Row(
        children: [
          const Icon(Icons.error, color: Colors.red),
          const SizedBox(width: 8),
          Expanded(
            child: Text(
              _error!,
              style: const TextStyle(color: Colors.red),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildImagePreview() {
    return _buildSectionCard(
      title: '选中的图片 (${_imageFiles.length})',
      icon: Icons.image,
      color: Colors.green,
      child: GridView.builder(
        shrinkWrap: true,
        physics: const NeverScrollableScrollPhysics(),
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 3,
          crossAxisSpacing: 8,
          mainAxisSpacing: 8,
        ),
        itemCount: _imageFiles.length,
        itemBuilder: (context, index) {
          return Stack(
            children: [
              Positioned.fill(
                child: ClipRRect(
                  borderRadius: BorderRadius.circular(8),
                  child: Image.file(
                    File(_imageFiles[index].path),
                    fit: BoxFit.cover,
                  ),
                ),
              ),
              Positioned(
                top: 4,
                right: 4,
                child: Container(
                  padding: const EdgeInsets.all(4),
                  decoration: BoxDecoration(
                    color: Colors.black54,
                    borderRadius: BorderRadius.circular(12),
                  ),
                  child: Text(
                    '${index + 1}',
                    style: const TextStyle(
                      color: Colors.white,
                      fontSize: 12,
                    ),
                  ),
                ),
              ),
            ],
          );
        },
      ),
    );
  }

  Widget _buildVideoPreview() {
    return _buildSectionCard(
      title: '选中的视频',
      icon: Icons.video_library,
      color: Colors.red,
      child: Container(
        padding: const EdgeInsets.all(16),
        decoration: BoxDecoration(
          color: Colors.grey.shade100,
          borderRadius: BorderRadius.circular(8),
        ),
        child: Column(
          children: [
            const Icon(Icons.play_circle_outline, size: 48),
            const SizedBox(height: 8),
            Text(
              _videoFile!.name,
              style: const TextStyle(fontWeight: FontWeight.w500),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildSectionCard({
    required String title,
    required IconData icon,
    required Color color,
    required Widget child,
  }) {
    return Card(
      elevation: 2,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(16),
      ),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                Container(
                  padding: const EdgeInsets.all(8),
                  decoration: BoxDecoration(
                    color: color.withOpacity(0.1),
                    borderRadius: BorderRadius.circular(8),
                  ),
                  child: Icon(
                    icon,
                    color: color,
                    size: 24,
                  ),
                ),
                const SizedBox(width: 12),
                Text(
                  title,
                  style: const TextStyle(
                    fontSize: 18,
                    fontWeight: FontWeight.bold,
                  ),
                ),
              ],
            ),
            const Divider(height: 24),
            child,
          ],
        ),
      ),
    );
  }
}

🏆 六、最佳实践与注意事项

⚠️ 6.1 图片压缩策略

在实际应用中,直接使用原图往往不是最佳选择。高分辨率的照片可能达到几 MB 甚至几十 MB,这会导致:

上传耗时过长:用户需要等待很长时间才能完成上传,体验很差。

占用存储空间:服务器需要存储大量的大文件,成本增加。

加载速度慢:其他用户查看图片时需要下载大文件,加载缓慢。

因此,在选择图片时进行压缩是非常必要的:

dart 复制代码
// 推荐的压缩配置
final XFile? image = await _picker.pickImage(
  source: ImageSource.gallery,
  maxWidth: 1280,      // 限制最大宽度
  maxHeight: 720,      // 限制最大高度
  imageQuality: 80,    // 压缩质量 80%
);

🔐 6.2 权限处理最佳实践

虽然 OpenHarmony 平台会自动处理权限请求,但作为开发者,了解权限处理流程仍然很重要:

提前告知用户:在请求权限之前,向用户解释为什么需要这个权限。

优雅处理拒绝:如果用户拒绝授权,提供替代方案或引导用户手动开启权限。

避免频繁请求:不要在短时间内频繁请求权限,这会让用户感到烦躁。

📱 6.3 OpenHarmony 平台特殊说明

相机调用:OpenHarmony 平台支持调用系统相机应用进行拍照和录像。

相册访问:可以正常访问系统相册,选择图片和视频。

权限管理:OpenHarmony 平台会自动处理运行时权限请求。


📌 七、总结

本文通过一个完整的智能图片选择与管理系统案例,深入讲解了 image_picker 第三方库的使用方法与最佳实践:

架构设计:采用分层架构(UI层 → 服务层 → 基础设施层),让代码更清晰,便于维护和测试。

服务封装:统一封装图片选择逻辑,提供语义化的方法名,让调用代码更易读。

配置预设:定义常用的选择配置预设,方便在不同场景下快速使用。

错误处理:完善的错误处理机制,确保应用在各种情况下都能稳定运行。

掌握这些技巧,你就能构建出专业级的图片选择功能,为用户提供流畅、可靠的媒体选择体验。


参考资料

相关推荐
键盘鼓手苏苏5 小时前
Flutter for OpenHarmony:markdown 纯 Dart 解析引擎(将文本转化为结构化 HTML/UI) 深度解析与鸿蒙适配指南
前端·网络·算法·flutter·ui·html·harmonyos
恋猫de小郭11 小时前
丰田正在使用 Flutter 开发游戏引擎 Fluorite
android·前端·flutter
阿林来了11 小时前
Flutter三方库适配OpenHarmony【flutter_speech】— 原始插件源码分析
flutter·harmonyos·鸿蒙
不爱吃糖的程序媛11 小时前
Flutter 应用退出插件 HarmonyOS 适配技术详解
flutter·华为·harmonyos
哈__15 小时前
基础入门 Flutter for OpenHarmony:video_thumbnail 视频缩略图详解
flutter·音视频
lqj_本人15 小时前
Flutter三方库适配OpenHarmony【apple_product_name】华为Pura系列设备映射表
flutter·华为
空白诗17 小时前
基础入门 Flutter for OpenHarmony:Divider 分割线组件详解
flutter
阿林来了19 小时前
Flutter三方库适配OpenHarmony【flutter_speech】— 语音识别停止与取消
flutter·语音识别·harmonyos
哈__19 小时前
基础入门 Flutter for OpenHarmony:flutter_slidable 列表滑动操作详解
flutter