当前位置: 首页 > news >正文

Flutter:上传图片,选择相机或相册:wechat_assets_picker

图片选择功能:可选单张,或多张。
1、showModalBottomSheet(选择相册/相机)
2、WechatImagePicker(选取图片)
3、CompressMediaFile(图片压缩)

1、ActionSheetUtil

import 'package:ducafe_ui_core/ducafe_ui_core.dart';
import 'package:flutter/material.dart';
import 'package:happy/common/index.dart';
import 'package:get/get.dart';/// 底部操作表
/* 使用示例
ActionSheetUtil.showActionSheet(context: context,title: '选择图片',items: [{'id': 1, 'title': '相机', 'type': 'camera'},],onConfirm: (item) {},
);
*/
class ActionSheetUtil {/// 底部操作表/// [context] 上下文/// [title] 标题/// [items] 选项列表 [{'id': 1, 'title': '相机', 'type': 'camera'}]/// [onConfirm] 确认回调 返回选中项static void showActionSheet({required BuildContext context,required String title,required List<Map<String, dynamic>> items,required Function(Map<String, dynamic>) onConfirm,}) {showModalBottomSheet(context: context,backgroundColor: Colors.transparent,builder: (context) => Container(decoration: BoxDecoration(color: AppTheme.pageBgColor,borderRadius: BorderRadius.only(topLeft: Radius.circular(30.w),topRight: Radius.circular(30.w),),),child: SafeArea(child: Column(mainAxisSize: MainAxisSize.min,children: [// 标题Container(height: 100.w,alignment: Alignment.center,child: TextWidget.body(title,size: 30.sp,weight: FontWeight.w600,color: AppTheme.color000,),),// 选项列表...items.map((item) => GestureDetector(onTap: () {Navigator.pop(context);onConfirm(item);},child: Container(height: 100.w,alignment: Alignment.center,decoration: BoxDecoration(border: Border(top: BorderSide(color: AppTheme.dividerColor,width: 1,),),),child: TextWidget.body(item['title'],size: 28.sp,color: AppTheme.color000,),),)),// 间隔Container(height: 16.w,color: AppTheme.dividerColor,),// 取消按钮GestureDetector(onTap: () => Navigator.pop(context),child: Container(height: 100.w,alignment: Alignment.center,color: Colors.transparent,child: TextWidget.body('取消'.tr,size: 28.sp,color: AppTheme.color000,),),),],),),),);}
}

在这里插入图片描述

2、WechatImagePicker

import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:happy/common/index.dart';
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
import 'package:wechat_camera_picker/wechat_camera_picker.dart';/// 微信风格图片选择器封装
class WechatImagePicker {/// 显示图片选择弹窗(相机 + 相册)/// 选择图片后自动压缩,返回压缩后的文件/// [maxAssets] 最大选择数量,1为单选,>1为多选/// [onSingleResult] 单张图片选择回调/// [onMultiResult] 多张图片选择回调static void showImagePicker({required BuildContext context,Function(File?)? onSingleResult,Function(List<File>)? onMultiResult,int maxAssets = 1,bool autoCompress = true,}) {// 验证回调参数if (maxAssets == 1 && onSingleResult == null) {throw ArgumentError('单张选择时必须提供 onSingleResult 回调');}if (maxAssets > 1 && onMultiResult == null) {throw ArgumentError('多张选择时必须提供 onMultiResult 回调');}List<Map<String, dynamic>> actions = [];// 单张选择时显示相机和相册选项if (maxAssets == 1) {actions = [{"id": 1, "title": "相机".tr, "type": "camera"},{"id": 2, "title": "相册".tr, "type": "gallery"},];} else {// 多张选择时只显示相册选项actions = [{"id": 1, "title": "相册选择($maxAssets张)".tr, "type": "gallery"},];}ActionSheetUtil.showActionSheet(context: context,title: '请选择'.tr,items: actions,onConfirm: (item) async {try {if (item['type'] == 'camera') {// 相机拍照(仅单张)final selectedFile = await _pickFromCamera(context);if (selectedFile != null && autoCompress) {final compressedFile = await _compressImage(selectedFile);onSingleResult!(compressedFile);} else {onSingleResult!(selectedFile);}} else if (item['type'] == 'gallery') {if (maxAssets == 1) {// 单张相册选择final selectedFile = await _pickFromGallery(context);if (selectedFile != null && autoCompress) {final compressedFile = await _compressImage(selectedFile);onSingleResult!(compressedFile);} else {onSingleResult!(selectedFile);}} else {// 多张相册选择final selectedFiles = await _pickMultipleFromGallery(context, maxAssets);if (autoCompress && selectedFiles.isNotEmpty) {final compressedFiles = await _compressMultipleImages(selectedFiles);onMultiResult!(compressedFiles);} else {onMultiResult!(selectedFiles);}}}} catch (e) {print('图片选择异常: $e');if (maxAssets == 1) {onSingleResult!(null);} else {onMultiResult!([]);}}},);}/// 直接从相机拍照static Future<File?> _pickFromCamera(BuildContext context) async {try {final AssetEntity? entity = await CameraPicker.pickFromCamera(context,pickerConfig: CameraPickerConfig(enableRecording: false,enableAudio: false,enableSetExposure: true,enableExposureControlOnPoint: true,enablePinchToZoom: true,shouldDeletePreviewFile: true,maximumRecordingDuration: const Duration(seconds: 15),),);if (entity != null) {final File? file = await entity.file;if (file != null && await file.exists()) {print('相机拍照成功: ${file.path}');return file;}}return null;} catch (e) {print('相机拍照失败: $e');Loading.toast('相机拍照失败'.tr);return null;}}/// 直接从相册选择static Future<File?> _pickFromGallery(BuildContext context) async {try {final List<AssetEntity>? assets = await AssetPicker.pickAssets(context,pickerConfig: AssetPickerConfig(maxAssets: 1,requestType: RequestType.image,themeColor: Theme.of(context).primaryColor,textDelegate: const AssetPickerTextDelegate(),),);if (assets != null && assets.isNotEmpty) {final File? file = await assets.first.file;if (file != null && await file.exists()) {print('相册选择成功: ${file.path}');return file;}}return null;} catch (e) {print('相册选择失败: $e');if (e.toString().contains('permission')) {Loading.toast('请允许访问相册权限'.tr);} else {Loading.toast('相册选择失败'.tr);}return null;}}/// 选择多张图片(仅相册)static Future<List<File>> pickMultipleImages(BuildContext context, {int maxAssets = 9,}) async {try {final List<AssetEntity>? assets = await AssetPicker.pickAssets(context,pickerConfig: AssetPickerConfig(maxAssets: maxAssets,requestType: RequestType.image,themeColor: Theme.of(context).primaryColor,textDelegate: const AssetPickerTextDelegate(),),);if (assets != null && assets.isNotEmpty) {final List<File> files = [];for (final asset in assets) {final File? file = await asset.file;if (file != null && await file.exists()) {files.add(file);}}return files;}return [];} catch (e) {print('多图片选择失败: $e');Loading.toast('图片选择失败'.tr);return [];}}/// 从相册选择多张图片static Future<List<File>> _pickMultipleFromGallery(BuildContext context, int maxAssets) async {try {final List<AssetEntity>? assets = await AssetPicker.pickAssets(context,pickerConfig: AssetPickerConfig(maxAssets: maxAssets,requestType: RequestType.image,themeColor: Theme.of(context).primaryColor,textDelegate: const AssetPickerTextDelegate(),),);if (assets != null && assets.isNotEmpty) {final List<File> files = [];for (final asset in assets) {final File? file = await asset.file;if (file != null && await file.exists()) {files.add(file);}}print('相册选择成功: ${files.length}张图片');return files;}return [];} catch (e) {print('多图片选择失败: $e');if (e.toString().contains('permission')) {Loading.toast('请允许访问相册权限'.tr);} else {Loading.toast('图片选择失败'.tr);}return [];}}/// 压缩多张图片static Future<List<File>> _compressMultipleImages(List<File> originalFiles) async {final List<File> compressedFiles = [];for (int i = 0; i < originalFiles.length; i++) {final originalFile = originalFiles[i];print('压缩第${i + 1}/${originalFiles.length}张图片');final compressedFile = await _compressImage(originalFile);if (compressedFile != null) {compressedFiles.add(compressedFile);}}print('批量压缩完成: ${compressedFiles.length}/${originalFiles.length}张');return compressedFiles;}/// 压缩图片static Future<File?> _compressImage(File originalFile) async {try {print('开始压缩图片: ${originalFile.path}');final compressedFile = await DuCompress.image(originalFile.path);if (compressedFile == null) {print('图片压缩失败,返回原文件');return originalFile;}final File compressedImageFile = File(compressedFile.path);if (await compressedImageFile.exists()) {final originalSize = await originalFile.length();final compressedSize = await compressedImageFile.length();print('图片压缩成功: ${originalSize}KB -> ${compressedSize}KB');return compressedImageFile;} else {print('压缩后的文件不存在,返回原文件');return originalFile;}} catch (e) {print('图片压缩异常: $e,返回原文件');return originalFile;}}
}

在这里插入图片描述

3、CompressMediaFile

import 'dart:io';
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:video_compress/video_compress.dart';/// 压缩工具类
/* 使用示例
DuCompress.image('图片路径');
DuCompress.video('视频路径');
final file = await ImagePicker().pickImage(source: ImageSource.gallery);
if (file == null) return;
// 创建文件对象
File originalFile = File(file.path);
// 压缩图片
var newFile = await DuCompress.image(originalFile.path);
if (newFile == null) return;
// 将 XFile 转换为 File
File compressedFile = File(newFile.path);
// 上传压缩后的图片
ChatApi.uploadFile(compressedFile);
*//// 压缩返回类型
class CompressMediaFile {final File? thumbnail;final MediaInfo? video;CompressMediaFile({this.thumbnail,this.video,});
}/// 媒体压缩
class DuCompress {// 压缩图片static Future<XFile?> image(String path, {int minWidth = 1920,int minHeight = 1080,}) async {return await FlutterImageCompress.compressAndGetFile(path,'${path}_temp.jpg',keepExif: true,quality: 70,format: CompressFormat.jpeg,minHeight: minHeight,minWidth: minWidth,);}/// 压缩视频static Future<CompressMediaFile> video(File file) async {var result = await Future.wait([// 1 视频压缩VideoCompress.compressVideo(file.path,quality: VideoQuality.Res640x480Quality,deleteOrigin: false, // 默认不要去删除原视频includeAudio: true,frameRate: 25,),// 2 视频缩略图VideoCompress.getFileThumbnail(file.path,quality: 80,position: -1000,),]);return CompressMediaFile(video: result.first as MediaInfo,thumbnail: result.last as File,);}/// 清理缓存static Future<bool?> clean() async {return await VideoCompress.deleteAllCache();}/// 取消static Future<void> cancel() async {await VideoCompress.cancelCompression();}
}

使用示例

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:happy/common/utils/wechat_image_picker.dart';/// WechatImagePicker 使用示例
class WechatImagePickerExample {/// 示例1:选择单张图片(自动压缩)static void pickSingleImage(BuildContext context) {WechatImagePicker.showImagePicker(context: context,maxAssets: 1, // 单张选择autoCompress: true, // 自动压缩onSingleResult: (File? compressedFile) {if (compressedFile != null) {print('单张图片选择成功: ${compressedFile.path}');// 这里处理选择的图片,已经是压缩后的} else {print('用户取消选择或选择失败');}},);}/// 示例2:选择多张图片(最多9张,自动压缩)static void pickMultipleImages(BuildContext context) {WechatImagePicker.showImagePicker(context: context,maxAssets: 9, // 最多选择9张autoCompress: true, // 自动压缩onMultiResult: (List<File> compressedFiles) {if (compressedFiles.isNotEmpty) {print('多张图片选择成功: ${compressedFiles.length}张');for (int i = 0; i < compressedFiles.length; i++) {print('图片${i + 1}: ${compressedFiles[i].path}');}// 这里处理选择的图片列表,都是压缩后的} else {print('用户取消选择或选择失败');}},);}/// 示例3:选择单张图片(不压缩)static void pickSingleImageWithoutCompress(BuildContext context) {WechatImagePicker.showImagePicker(context: context,maxAssets: 1,autoCompress: false, // 不压缩onSingleResult: (File? originalFile) {if (originalFile != null) {print('单张原图选择成功: ${originalFile.path}');// 这里处理原始图片}},);}/// 示例4:选择多张图片(最多3张,不压缩)static void pickMultipleImagesWithoutCompress(BuildContext context) {WechatImagePicker.showImagePicker(context: context,maxAssets: 3, // 最多选择3张autoCompress: false, // 不压缩onMultiResult: (List<File> originalFiles) {if (originalFiles.isNotEmpty) {print('多张原图选择成功: ${originalFiles.length}张');// 这里处理原始图片列表}},);}
}
http://www.xdnf.cn/news/1135783.html

相关文章:

  • 【软件开发】Copilot 编码插件
  • 【网易云-body1】
  • TRAE IDE** 下载、安装、开发、测试和部署 2048 小游戏的全流程指南
  • 界面控件Kendo UI for Angular 2025 Q2新版亮点 - 增强跨设备的无缝体验
  • 杨耀东老师在ICML2025上对齐教程:《语言模型的对齐方法:一种机器学习视角》
  • 《工程伦理》分析报告五 软件开发
  • Vue3入门-计算属性+监听器
  • Vmware虚拟机使用仅主机模式共享物理网卡访问互联网
  • 时序数据库选型指南 —— 为什么选择 Apache IoTDB?
  • Linux中的数据库操作基础
  • ros2 标定相机
  • Qwen3-8B Dify RAG环境搭建
  • 2D视觉系统标定流程与关键要求
  • 高光谱相机(Hyperspectral Camera)
  • 【后端】Linux系统发布.NetCore项目
  • 尺寸标注识别3 实例分割 roboflow
  • NumPy, SciPy 之间的区别
  • 大语言模型任务分解与汇总:从认知瓶颈到系统化解决方案
  • AutoMQ 与 Lightstreamer 达成战略合作:NASA也在用的 Kafka 数据实时分享新架构
  • 【C# in .NET】16. 探秘类成员-索引器:通过索引访问对象
  • 使用Pytorch进行数字手写体识别
  • OpenCV中常用特征提取算法(SURF、ORB、SIFT和AKAZE)用法示例(C++和Python)
  • 手撕Spring底层系列之:后置处理器“PostProcessor”
  • 学习OpenCV---显示图片
  • 代码随想录算法训练营十八天|二叉树part08
  • 算法竞赛备赛——【图论】求最短路径——Floyd算法
  • 深度学习之反向传播
  • Electron实现“仅首次运行时创建SQLite数据库”
  • 数据集相关类代码回顾理解 | utils.make_grid\list comprehension\np.transpose
  • HDFS基本操作训练(创建、上传、下载、删除)