圖片選擇功能:可選單張,或多張。
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}張');// 這里處理原始圖片列表}},);}
}