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}张');// 这里处理原始图片列表}},);}
}