图片选择功能:可选单张,或多张。
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}张');
// 这里处理原始图片列表
}
},
);
}
}