一、需求来源
最近遇到群头像更新的需求,花点时间封装成组件,方便复用。默认支持压缩、裁剪(可选),使用极其简单。
效果如下:
二、使用示例
方式一:
kotlin
onTap: () async {
final val = await handleImageFromPhotoAlbum();
if (val == null) {
return;
}
debugPrint("updateAvatar: $val");
},
方式二:
kotlin
onTap: () async {
updateAvatar(
cb: (val){
if (val == null) {
return;
}
debugPrint("updateAvatar: $val");
}
);
},
三、源码
1、BottomSheetAvatarMixin 源码,
ini
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter_templet_project/basicWidget/n_overlay.dart';
import 'package:flutter_templet_project/basicWidget/upload/image_service.dart';
import 'package:flutter_templet_project/extension/widget_ext.dart';
import 'package:image_picker/image_picker.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
/// 头像更换(回调返回单张图片的路径)
/// 默认图片压缩,图片裁剪
mixin BottomSheetAvatarMixin<T extends StatefulWidget> on State<T> {
// 相册选择器
final ImagePicker _picker = ImagePicker();
// 更新头像
updateAvatar({
bool needCropp = true,
Function(String? path)? cb,
}) {
final titles = ['拍摄', '从相册选择'];
CupertinoActionSheet(
actions: titles.map((e) {
return CupertinoActionSheetAction(
onPressed: () {
Navigator.of(context).pop();
if (e == titles[0]) {
handleImageFromCamera(needCropp: needCropp, cb: cb);
} else {
handleImageFromPhotoAlbum(needCropp: needCropp, cb: cb);
}
},
child: Text(e),
);
}).toList(),
cancelButton: CupertinoActionSheetAction(
isDestructiveAction: true,
onPressed: () {
Navigator.pop(context);
},
child: const Text('取消'),
),
).toShowCupertinoModalPopup(context: context);
}
/// 拍照获取图像
Future<String?> handleImageFromCamera({
bool needCropp = true,
required Function(String? path)? cb,
}) async {
final file = await _takePhoto();
if (file == null) {
NOverlay.showToast(context, message: '请重新拍摄',);
return null;
}
NOverlay.showLoading(context, message: "图片处理中...");
final compressImageFile = await file.toCompressImage();
if (!needCropp) {
NOverlay.hide();
cb?.call(compressImageFile.path);
return compressImageFile.path;
}
final cropImageFile = await compressImageFile.toCropImage();
NOverlay.hide();
cb?.call(cropImageFile.path);
return cropImageFile.path;
}
/// 从相册获取图像
Future<String?> handleImageFromPhotoAlbum({
bool needCropp = true,
Function(String? path)? cb,
}) async {
final file = await _chooseAvatarByWechatPicker();
if (file == null) {
NOverlay.showToast(context, message: '请重新选择',);
return null;
}
NOverlay.showLoading(context, message: "图片处理中...");
final compressImageFile = await file.toCompressImage();
if (!needCropp) {
NOverlay.hide();
cb?.call(compressImageFile.path);
return compressImageFile.path;
}
final cropImageFile = await compressImageFile.toCropImage();
NOverlay.hide();
cb?.call(cropImageFile.path);
return cropImageFile.path;
}
/// 拍照
Future<File?> _takePhoto() async {
try {
final file = await _picker.pickImage(
source: ImageSource.camera,
imageQuality: 50,
);
if (file == null) {
return null;
}
var fileNew = File(file.path);
return fileNew;
} catch (err) {
openAppSettings();
}
return null;
}
/// 通过微信相册选择器选择头像
Future<File?> _chooseAvatarByWechatPicker() async {
var maxCount = 1;
final entitys = await AssetPicker.pickAssets(
context,
pickerConfig: AssetPickerConfig(
requestType: RequestType.image,
specialPickerType: SpecialPickerType.noPreview,
selectedAssets: [],
maxAssets: maxCount,
),
) ?? [];
if (entitys.isEmpty) {
return null;
}
final item = entitys[0];
final file = await item.file;
if (file == null) {
return null;
}
return file;
}
}
2、ImageService 源码,图片处理工具类
dart
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:flutter_templet_project/cache/cache_asset_service.dart';
import 'package:flutter_templet_project/extension/num_ext.dart';
/// 图片处理工具类
class ImageService{
/// 图片压缩
Future<File?> compressAndGetFile(File file, [String? targetPath]) async {
try {
var fileName = file.absolute.path.split('/').last;
// Directory tempDir = await getTemporaryDirectory();
// Directory assetDir = Directory('${tempDir.path}/asset');
// if (!assetDir.existsSync()) {
// assetDir.createSync();
// debugPrint('assetDir 文件保存路径为 ${assetDir.path}');
// }
Directory? assetDir = await CacheAssetService().getDir();
var tmpPath = '${assetDir.path}/$fileName';
targetPath ??= tmpPath;
// debugPrint('fileName_${fileName}');
// debugPrint('assetDir_${assetDir}');
// debugPrint('targetPath_${targetPath}');
final compressQuality = file.lengthSync().compressQuality;
var result = await FlutterImageCompress.compressAndGetFile(
file.absolute.path, targetPath,
quality: compressQuality,
rotate: 0,
);
final path = result?.path;
if (result == null || path == null || path.isEmpty) {
debugPrint("压缩文件路径获取失败");
return file;
}
final lenth = await result.length();
final infos = [
"图片名称: $fileName",
"压缩前: ${file.lengthSync().fileSize}",
"压缩质量: $compressQuality",
"压缩后: ${lenth.fileSize}",
"原路径: ${file.absolute.path}",
"压缩路径: $targetPath",
];
debugPrint("图片压缩: ${infos.join("\n")}");
return File(path);
} catch (e) {
debugPrint("compressAndGetFile:${e.toString()}");
}
return null;
}
/// 图片压缩
Future<String> compressAndGetFilePath(String imagePath, [String? targetPath,]) async {
try {
final file = File(imagePath);
final fileNew = await compressAndGetFile(file, targetPath);
final result = fileNew?.path ?? imagePath;
return result;
} catch (e) {
debugPrint("compressAndGetFilePath:${e.toString()}");
}
return imagePath;
}
}
/// 图片文件扩展方法
extension ImageFileExt on File {
/// 图片压缩
Future<File> toCompressImage() async {
var compressFile = await ImageService().compressAndGetFile(this);
compressFile ??= this;// 压缩失败则使用原图
return compressFile;
}
/// 图像裁剪
Future<File> toCropImage({
int? maxWidth,
int? maxHeight,
}) async {
try {
final sourcePath = path;
var croppedFile = await ImageCropper().cropImage(
sourcePath: sourcePath,
maxWidth: maxWidth,
maxHeight: maxHeight,
aspectRatioPresets: [
CropAspectRatioPreset.square,
],
uiSettings: [
AndroidUiSettings(
toolbarTitle: '',
toolbarColor: Colors.blue,
toolbarWidgetColor: Colors.white,
initAspectRatio: CropAspectRatioPreset.original,
lockAspectRatio: false,
),
IOSUiSettings(
title: 'Cropper',
),
],
);
if (croppedFile == null) {
return this;
}
return File(croppedFile.path);
} catch (e) {
debugPrint("toCropImage: $e");
return this;
}
}
}
总结
1、最终只涉及方法的调用(不涉及状态)所以用 mixin 进行了代码拆分,方便多项目复用。
2、压缩和裁剪比较耗时,需要在执行前加上指示器,不然会严重影响用户体验;
3、NOverlay 是个人弹窗封装类,大家可以替换成自己项目的即可;
4、执行结果返回图片文件路径,大家调用自己封装网络模块上传即可;
涉及第三方库
// 图片选择
wechat_assets_picker
// 图片压缩
flutter_image_compress
// 负责缓存文件清理
path_provider