在业务中大图预览并保存图片至相册的需求是非常常见的:
本文将结合dio
image_gallery_saver
permission_handler
photo_view_gallery
几个库来实现功能。
安装三方库
- dio: ^5.3.3
- image_gallery_saver: '^2.0.3'
- photo_view: ^0.14.0
- permission_handler: ^11.0.1
原生权限配置
- android目录下
AndroidManifest.xml
添加storage权限
<!-- 读取缓存数据权限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!-- 读取缓存数据权限 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
- ios Runner目录下
Info.plist
添加NSPhotoLibraryAddUsageDescription
<key>NSPhotoLibraryAddUsageDescription</key>
<string>允许APP保存图片到相册</string>
ios目录下Podfile
文件添加以下内容,用于permission_handler
动态获取权限:
主要是需要将
PERMISSION_PHOTOS=1
的注释去掉,查看详情
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
# Start of the permission_handler configuration
target.build_configurations.each do |config|
# You can enable the permissions needed here. For example to enable camera
# permission, just remove the `#` character in front so it looks like this:
#
# ## dart: PermissionGroup.camera
# 'PERMISSION_CAMERA=1'
#
# Preprocessor definitions can be found in: https://github.com/Baseflow/flutter-permission-handler/blob/master/permission_handler_apple/ios/Classes/PermissionHandlerEnums.h
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
'$(inherited)',
## dart: PermissionGroup.photos
'PERMISSION_PHOTOS=1',
]
end
# End of the permission_handler configuration
end
end
封装动态获取权限功能
class CommonUtil {
// 授予权限返回true, 否则返回false
static Future<bool> requestScopePermission(Permission scope) async {
// 获取当前的权限
PermissionStatus status = await scope.status;
if (status == PermissionStatus.granted) {
// 已经授权
return true;
} else {
// 未授权则发起一次申请
status = await scope.request();
if (status == PermissionStatus.granted) {
return true;
} else {
return false;
}
}
}
获取相册权限
bool storageStatus = await requestScopePermission(Permission.storage);
抽离保存单张图片功能
NetUtil是使用dio封装的request自定义类,可自行使用自己项目的方法来调用,保证responseType: ResponseType.bytes
类型即可。
将结果传给ImageGallerySaver
即可完成图片的保存。
class CommonUtil {
static Future<dynamic> _saveImage(String imageUrl) async {
var response = await NetUtil().request(imageUrl, options: Options(responseType: ResponseType.bytes));
final result = await ImageGallerySaver.saveImage(Uint8List.fromList(response), quality: 60);
return result;
}
}
多张图保存
我们希望能支持多图保存的场景,其实也很简单,将单图功能做一次遍历就可以实现。
这里直接使用
curRes['index'] == imageUrls.length - 1
其实不够严谨,需要考虑图片下载失败等因素,可以将saveImage失败的状态都存到curRes中然后再去判断即可。
class CommonUtil {
static Future<void> saveToAlbum(List<String> imageUrls) async {
bool storageStatus = await requestScopePermission(Permission.storage);
if (storageStatus) {
Map<String, dynamic> curRes = {'index': 0, 'isSuccess': false};
showLoading(status: '正在保存');
for (int i = 0; i < imageUrls.length; i++) {
var result = await _saveImage(imageUrls[i]);
curRes['index'] = i;
curRes['isSuccess'] = result['isSuccess'];
}
if (curRes['index'] == imageUrls.length - 1) {
showSuccess('保存成功');
}
} else {
showError('暂无相册授权');
}
}
}
图片预览
- 新建一个页面,当点击图片时使用路由打开。
这个页面的功能就非常的单一,核心在于使用PhotoViewGallery.builder
去做了图片的预览便于后期需要使用该库的其他功能,也可以自己使用PageView
自定义。
预览页面主要接受以下2个参数
final List imgList; // 图片列表
final int index; // 当前预览的图片索引
预览页面代码:
import 'package:flutter/material.dart';
import 'package:photo_view/photo_view_gallery.dart';
import 'package:xiangkucun/util/common_util.dart';
class PhotoScreen extends StatefulWidget {
final List imgList;
final int index;
final GestureTapCallback? onLongPress;
const PhotoScreen({
super.key,
required this.imgList,
required this.index,
this.onLongPress,
});
@override
State<PhotoScreen> createState() => _PhotoScreenState();
}
class _PhotoScreenState extends State<PhotoScreen> {
int _currentIndex = 0;
PageController? _controller;
@override
void initState() {
super.initState();
_controller = PageController(initialPage: widget.index);
setState(() {
_currentIndex = widget.index;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
elevation: 0,
title: Text('${_currentIndex + 1}/${widget.imgList.length}'),
backgroundColor: Colors.black,
leading: const SizedBox(),
actions: [
IconButton(
icon: const Icon(Icons.close, size: 30, color: Colors.white),
onPressed: () => Navigator.of(context).pop(),
),
],
),
bottomNavigationBar: Container(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),
color: Colors.black,
child: SizedBox(
height: 50,
child: IconButton(
icon: const Icon(Icons.download_rounded, size: 30, color: Colors.white),
onPressed: () {
CommonUtil.saveToAlbum([widget.imgList[_currentIndex]]);
},
),
),
),
body: Center(
child: GestureDetector(
onTap: () => Navigator.of(context).pop(),
onLongPress: widget.onLongPress,
child: Container(
color: Colors.black,
child: PhotoViewGallery.builder(
scrollPhysics: const BouncingScrollPhysics(),
builder: (BuildContext context, int index) {
return PhotoViewGalleryPageOptions(
imageProvider: NetworkImage(widget.imgList[index]),
);
},
itemCount: widget.imgList.length,
backgroundDecoration: null,
pageController: _controller,
enableRotation: true,
onPageChanged: (index) {
setState(() {
_currentIndex = index;
});
},
),
),
),
),
);
}
}
点击图片打开预览
使用FadeTransition
对路由设置渐显的效果,让预览组件更有沉浸感.
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) {
return FadeTransition(
opacity: animation,
child: PhotoScreen(
imgList: _sharePoster!.shareImgs!,
index: index,
),
);
},
),
);
至此一个简单图片预览并可以批量下载至相册的业务功能就实现了。