前言
现在市面上图片挑选器大致能够分为以下两类:
- 原生完结UI
- Flutter完结UI
两种类型的对比如下
类型 | 优点 | 缺点 |
---|---|---|
原生完结UI | 功能,流通度很好 | 双端别离需求完结UI及相册逻辑 |
Flutter完结UI | 双端UI统一 | 双端需求别离完结相册逻辑,流通度以及功能有瓶颈 |
简介
本文是根据第二种也便是Flutter完结UI完结的图片挑选器链接在此,使用到的库有
- photo_gallery用于获取相关相册内容,这儿做了一些改动,所以对其进行的源码依赖
- image_picker用于摄影
- permission_handler用于相册,相机等权限处理
现在完结的功能有
- 图片、视频共存与互斥显示
- 约束挑选item的数量
- 每行展现item的数量
- 视频时长约束
- 摄影
- 摄影之后默许选中
- 相册列表
- 切换相册
- 切换挑选器巨细窗
部分截图
详细完结
构造办法
由于要实时同享已选的资源给调用者,还要与外部同享切换巨细窗等数据,在这儿使用ValueNotifier,能够轻松处理同享数据,构造办法里提供了一些必选以及可选参数,能够根据详细场景做相应的处理
const MediaPickerPage(
{Key? key,
this.type = allType, //媒体类型
required this.maxSelectCount, //最大挑选数量
this.crossAxisCount = 4, //每行展现的数量
required this.lastSelectMedia, //与调用者同享的已选资源
this.defaultShowHeight = 260, //半弹窗默许高度
this.maxHeight = 600, //弹窗最大高度
this.maxVideoDuration, //挑选视频最大时长
this.showBigViewNotifier, //切换巨细窗监听
this.canSelectedVideoNotifier, //是否可选视频监听
this.actionCallBack, //一些自定义事件回调
this.mediaPickerItemClickCallBack, //点击item的回调
this.mediaPickerMediumInfoListCreatedCallBack}) //获取到数据的回调
: super(key: key);
数据获取
获取默许相册列表里的数据,在这儿根据permission_handler做了权限判别,详细的业务场景其实未必会在这儿判别,可能在之前就现已做了处理
void _obtainMediaInfo({bool isAdd = false}) async {
if (await Permission.photos.request().isGranted) {
MediumType? type = jsonToMediumType(widget.type);
if (type != null) {
List<MediumInfo> list = await FlutterPluginMediaPicker.listMedium(
_thumbnailSize, type,
maxVideoDuration: widget.maxVideoDuration ?? defaultMaxVideoDuration);
setState(() {
_mediumInfoList.clear();
_mediumInfoList.addAll(list);
///相册列表数据获取完结后回调
if (widget.mediaPickerMediumInfoListCreatedCallBack != null) {
widget.mediaPickerMediumInfoListCreatedCallBack!(_mediumInfoList);
}
if (isAdd == true) {
///拍完照后,重置key,确保列表改写
_uniqueKey = UniqueKey();
_handleTakePhotoResult();
}
});
}
}
}
经过FlutterPluginMediaPicker.listMedium获取到相应相册的内容,之后改写GridView,这儿其实便是调用的photo_gallery的listMedia办法,默许获取全部资源,就把albumId给到默许值"__ALL__"
class PhotoGalleryExtension extends PhotoGallery {
static const MethodChannel _channel = MethodChannel('photo_gallery');
static const String _allAlbumId = "__ALL__";
static const int _defaultTotal = 1 << 16; // 给一个极大值
static Future<List<Medium>> listMedium(
{MediumType mediumType = MediumType.all}) async {
final json = await _channel.invokeMethod('listMedia', {
'albumId': _allAlbumId,
'mediumType': mediumTypeToJson(mediumType),
'newest': true,
'total': _defaultTotal,
});
return json['items'].map<Medium>((x) => Medium.fromJson(x)).toList();
}
数据展现
其中GridView.builder是来展现列表数据的,key: _uniqueKey
是为了能够在摄影之后成功改写列表,让摄影取得的相片放在列表第一位。AlbumListWidget是用来展现相册列表数据的,挑选相应的相册之后会在onAlbumClickedCallBack回调中更新列表数据,展现相应的相册资源。
Widget _getGridView() {
return Expanded(
child: Container(
color: Colors.white,
child: Stack(
children: [
GridView.builder(
addAutomaticKeepAlives: false,
addRepaintBoundaries: false,
key: _uniqueKey,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: widget.crossAxisCount!,
crossAxisSpacing: 3,
mainAxisSpacing: 3,
childAspectRatio: 1,
),
cacheExtent: 1,
padding: const EdgeInsets.only(left: 3, right: 3),
itemCount: _mediumInfoList.length + 1 + _getPlaceHolderCount(),
scrollDirection: Axis.vertical,
itemBuilder: (context, index) {
return _buildItem(index);
}),
AlbumListWidget(
type: jsonToMediumType(widget.type) ?? MediumType.all,
maxHeight: widget.maxHeight!,
showValueNotifier: showAlbumListNotifier,
onAlbumClickedCallBack: (Album album) {
albumName = album.name ?? albumName;
_loadAlbumData(album);
},
),
],
),
),
);
}
假如咱们获取大量图片的原图bytes的话关于channel的压力会很大,所以每一个item获取图片数据的时候都会由原生压缩之后获取到相应的数据,一起使用photo_gallery中的ThumbnailProvider展现图片,这儿会由体系imageCache处理缓存,之后会说关于体系缓存的处理,因为缓存是有上限的,关于资源数量巨大的情况下,需求做一些特别处理,这儿贴一下ThumbnailProvider的源码
part of photogallery;
/// Fetches the given medium thumbnail from the gallery.
class ThumbnailProvider extends ImageProvider<ThumbnailProvider> {
const ThumbnailProvider({
required this.mediumId,
this.mediumType,
this.height,
this.width,
this.highQuality = false,
});
final String mediumId;
final MediumType? mediumType;
final int? height;
final int? width;
final bool? highQuality;
@override
ImageStreamCompleter load(key, decode) {
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key, decode),
scale: 1.0,
informationCollector: () sync* {
yield ErrorDescription('Id: $mediumId');
},
);
}
Future<ui.Codec> _loadAsync(
ThumbnailProvider key, DecoderCallback decode) async {
assert(key == this);
final bytes = await PhotoGallery.getThumbnail(
mediumId: mediumId,
mediumType: mediumType,
height: height,
width: width,
highQuality: highQuality,
);
return await decode(Uint8List.fromList(bytes));
}
@override
Future<ThumbnailProvider> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture<ThumbnailProvider>(this);
}
@override
bool operator ==(dynamic other) {
if (other.runtimeType != runtimeType) return false;
final ThumbnailProvider typedOther = other;
return mediumId == typedOther.mediumId;
}
@override
int get hashCode => mediumId.hashCode;
@override
String toString() => '$runtimeType("$mediumId")';
}
摄影
摄影时调用的image_picker的办法,其实image_picker摄影完结之后不会把图片存到体系相册中,而是存在了缓存中,所以咱们需求先把图片存入相册之后改写列表
调用摄影办法并存入相册
static Future<String> takePhoto(
{double? maxWidth, double? maxHeight, int? quality}) async {
final ImagePicker _picker = ImagePicker();
final PickedFile? pickedFile = await _picker.getImage(
source: ImageSource.camera,
maxWidth: maxWidth,
maxHeight: maxHeight,
imageQuality: quality,
);
if (pickedFile != null) {
var data = await pickedFile.readAsBytes();
//写入体系相册
final result = await ImageGallerySaver.saveImage(Uint8List.fromList(data),
quality: 60, name: "image${DateTime.now().microsecond.toString()}",path:pickedFile.path);
debugPrint('保存的文件途径为${pickedFile.path.toString()}');
return pickedFile.path;
} else {
return '';
}
}
摄影完结存入相册之后,从头获取相册数据并更新UI
void _takePhoto() async {
if (await Permission.camera.request().isGranted) {
var imgPath = await MediaPicker.takePhoto(quality: 100);
if (imgPath.isNotEmpty) {
///Android渠道存入数据库,读取相册数据,有必定的延迟
if (Platform.isAndroid) {
Future.delayed(const Duration(milliseconds: 600), () {
_obtainMediaInfo(isAdd: true);
});
} else {
_obtainMediaInfo(isAdd: true);
}
}
} else {
_actionCallBack(MediaPickerActionType.camera);
}
}
item的选中和取消选中
这儿无非便是对相应的数据类型做判别,决定是否添加或者从lastSelectMedia中删去相应的数据
void _onItemAddCallBack(MediumInfo mediumInfo) async {
var values = widget.lastSelectMedia.value;
bool isContains = false;
late MediumInfo tempMedium;
values.forEach((element) {
if (element.id == mediumInfo.id) {
isContains = true;
tempMedium = element;
}
});
if (isContains) {
widget.lastSelectMedia.remove(tempMedium);
} else {
if (!isAddingItem) {
isAddingItem = true;
final bytes = await PhotoGallery.getThumbnail(
mediumId: mediumInfo.id,
mediumType: mediumInfo.medium.mediumType,
width: _thumbnailSize!.width.toInt() * 2,
height: _thumbnailSize!.height.toInt() * 2,
highQuality: true);
if (mediumInfo.type == MediumType.video) {
//添加文件途径
final file = await PhotoGallery.getFile(mediumId: mediumInfo.id);
widget.lastSelectMedia.add(MediumInfo(mediumInfo.medium,
bytes: Uint8List.fromList(bytes),
file: file.file,
orientation: file.orientation,
metaWidth: file.metaWidth,
metaHeight: file.metaHeight));
isAddingItem = false;
} else {
widget.lastSelectMedia.add(
MediumInfo(mediumInfo.medium, bytes: Uint8List.fromList(bytes)));
isAddingItem = false;
}
}
}
}
关于缓存imageCache
在imageCache源码中能够看到,缓存默许上限是1000张图,100MB的内存
const int _kDefaultSize = 1000;
const int _kDefaultSizeBytes = 100 << 20; // 100 MiB
这个关于长列表来说肯定体会是不友好的,调整之后会好一些。 在initState时修正imageCache
PaintingBinding.instance?.imageCache?.maximumSize = 2000; //图片缓存数量上限改成4000张
PaintingBinding.instance?.imageCache?.maximumSizeBytes =
500 * 1024 * 1024; //图片缓存巨细上限改成1000M
在dispose时imageCache铲除
@override
void dispose() {
super.dispose();
PaintingBinding.instance?.imageCache?.clear();
PaintingBinding.instance?.imageCache?.maximumSize =
1000; //图片缓存数量上限改成体系默许1000张
PaintingBinding.instance?.imageCache?.maximumSizeBytes =
100 * 1024 * 1024; //图片缓存巨细上限改成体系默许100M
PhotoGallery.cleanCache();
}
可是这一操作其实治标不治本,仅仅改大了缓存的上限,可是关于缓存的优化还是远远不够的,假如手机里面相片特别多的话肯定会打破上限,那么根据imageCache的LRU算法,现已加载的图片还会需求从头烘托,这个是无法到达体系的UI作用的最大弊端,所以想要无限挨近原生,这儿仍然有很多需求改进的点。