这里介绍的是图片查看库photo_view的使用以及达到我们的效果我们使用了一下插件,效果还是很棒的
图片浏览的插件是不支持保存到本地的,所以我们这边结合image_gallery_saver保存到相册。写入相册是需要申请权限的所以用到 permission_handler。
- 图片浏览查看
photo_view 官方地址 https://pub.dev/flutter/packages?q=photo_view - 保存图片到相册
image_gallery_saver 官方地址 https://pub.dev/packages/image_gallery_saver#-installing-tab- - 权限申请
permission_handler 官方地址 https://pub.dev/packages/permission_handler - 弹框提醒
fluttertoast https://pub.dev/packages/fluttertoast - 网络请求,保存图片的是需要使用
dio https://pub.dev/packages/dio
1。配置pubspec.yaml
#图片浏览查看
photo_view: ^0.9.2
#图片保存到本地相册
image_gallery_saver: ^1.2.2
#权限申请
permission_handler: ^5.0.0+hotfix.3
#弹框提醒
fluttertoast: ^4.0.1
#dio 网络请求
dio: 3.0.9
这里是路由跳转的动画可以试下在一个页面打开没有push效果
//路由跳转
class FadeRoute extends PageRouteBuilder {
final Widget page;
FadeRoute({this.page}): super(
pageBuilder: (
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
) =>page,transitionsBuilder: (
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) =>FadeTransition(
opacity: animation,
child: child,
),
);
}
//图片浏览以及保存图片跟权限申请
import 'package:photo_view/photo_view.dart';
import 'package:photo_view/photo_view_gallery.dart';
import 'package:image_gallery_saver/image_gallery_saver.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:dio/dio.dart';
import 'dart:typed_data';
import 'package:fluttertoast/fluttertoast.dart';
import 'dart:io';
//自己写的弹框
import 'package:flutter_app_demo/app/CoustomWidget/alert_widget.dart';
class GalleryPhotoViewWrapper extends StatefulWidget {
GalleryPhotoViewWrapper({
this.loadingBuilder,
this.backgroundDecoration,
this.minScale,
this.maxScale,
this.initialIndex,
@required this.galleryItems,
this.scrollDirection = Axis.horizontal,
}) : pageController = PageController(initialPage: initialIndex);
final LoadingBuilder loadingBuilder;
final Decoration backgroundDecoration;
final dynamic minScale;
final dynamic maxScale;
final int initialIndex;
final PageController pageController;
final List galleryItems;
final Axis scrollDirection;
@override
State<StatefulWidget> createState() {
return _GalleryPhotoViewWrapperState();
}
}
class _GalleryPhotoViewWrapperState extends State<GalleryPhotoViewWrapper> {
int currentIndex;
@override
void initState() {
currentIndex = widget.initialIndex;
super.initState();
}
void onPageChanged(int index) {
setState(() {
currentIndex = index;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: widget.backgroundDecoration,
constraints: BoxConstraints.expand(
height: MediaQuery.of(context).size.height,
),
child: Stack(
alignment: Alignment.bottomRight,
children: <Widget>[
PhotoViewGallery.builder(
scrollPhysics: const BouncingScrollPhysics(),
builder: _buildItem,
itemCount: widget.galleryItems.length,
loadingBuilder: widget.loadingBuilder,
backgroundDecoration: widget.backgroundDecoration,
pageController: widget.pageController,
onPageChanged: onPageChanged,
scrollDirection: widget.scrollDirection,
),
Positioned(//图片index显示
top: MediaQuery.of(context).padding.top+15,
width: MediaQuery.of(context).size.width,
child: Center(
child: Text("${currentIndex+1}/${widget.galleryItems.length}",style: TextStyle(color: Colors.white,fontSize: 16)),
),
),
Positioned(//右上角关闭按钮
right: 10,
top: MediaQuery.of(context).padding.top,
child: IconButton(
icon: Icon(Icons.close,size: 30,color: Colors.white,),
onPressed: (){
Navigator.of(context).pop();
},
),
),
Positioned(
bottom: MediaQuery.of(context).padding.bottom +16,
child: FlatButton(
child: Text('保存图片',style: TextStyle(color: Colors.white,fontSize: 16),),
onPressed: (){
if (Platform.isIOS){
return _savenNetworkImage();
}
requestPermission().then((bool){
if (bool){
_savenNetworkImage();
}
});
},
),
)
],
),
),
);
}
//动态申请权限,ios 要在info.plist 上面添加
Future<bool> requestPermission() async {
var status = await Permission.photos.status;
if (status.isUndetermined) {
Map<Permission, PermissionStatus> statuses = await [
Permission.photos,
].request();
}
return status.isGranted;
}
//保存网络图片到本地
_savenNetworkImage() async {
var status = await Permission.photos.status;
if (status.isDenied) {
// We didn't ask for permission yet.
print('暂无相册权限');
showDialog(context: context,builder: (context){
return AlertWidget(title: '微信提示',message: '您当前没有开启相册权限',confirm: '去开启',
confirmCallback: (){
//打开ios的设置
openAppSettings();
},);
});
return;
}
var response = await Dio().get(widget.galleryItems[currentIndex].resource, options: Options(responseType: ResponseType.bytes));
final result = await ImageGallerySaver.saveImage(Uint8List.fromList(response.data));
print(result);
if (Platform.isIOS){
if(result){
Fluttertoast.showToast(msg: '保存成功',gravity:ToastGravity.CENTER);
}else{
Fluttertoast.showToast(msg: '保存失败',gravity:ToastGravity.CENTER);
}
}else{
if(result !=null){
Fluttertoast.showToast(msg: '保存成功',gravity:ToastGravity.CENTER);
}else{
Fluttertoast.showToast(msg: '保存失败',gravity:ToastGravity.CENTER);
}
}
}
PhotoViewGalleryPageOptions _buildItem(BuildContext context, int index) {
final GalleryExampleItem item = widget.galleryItems[index];
return PhotoViewGalleryPageOptions(
onTapUp: (BuildContext context, TapUpDetails details, PhotoViewControllerValue controllerValue){
Navigator.of(context).pop();
},
imageProvider:NetworkImage(item.resource),
// initialScale: PhotoViewComputedScale.contained,
// minScale: PhotoViewComputedScale.contained * (0.5 + index / 10),
// maxScale: PhotoViewComputedScale.covered * 1.1,
heroAttributes: PhotoViewHeroAttributes(tag: item.id),
);
}
}
//Hero 动画组件
class GalleryExampleItemThumbnail extends StatelessWidget {
const GalleryExampleItemThumbnail(
{Key key, this.galleryExampleItem, this.onTap})
: super(key: key);
final GalleryExampleItem galleryExampleItem;
final GestureTapCallback onTap;
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
),
child: GestureDetector(
onTap: onTap,
child: Hero(
tag: galleryExampleItem.id,
child:Image.network(galleryExampleItem.resource,fit: BoxFit.cover,),
),
),
);
}
}
//model
class GalleryExampleItem {
GalleryExampleItem({this.id, this.resource, this.isSvg = false});
//hero的id 不能重复
String id;
String resource;
bool isSvg;
}
使用
model.imageUrlArray 路面存放的是 服务器上拉去的 多个图片的地址 ,我们需要把url转换成 GalleryExampleItem model,里面,有个字段 id就是Hero的id 要唯一 不能重复
GridView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: model.imageUrlArray ==null?0 :model.imageUrlArray.length,
itemBuilder: (context,index){
return AspectRatio(
aspectRatio: 1,
child: GalleryExampleItemThumbnail(
galleryExampleItem: imageModel()[index],
onTap: (){
Navigator.of(context).push(
new FadeRoute(page: GalleryPhotoViewWrapper(
galleryItems: imageModel(),
backgroundDecoration: const BoxDecoration(
color: Colors.black,
),
initialIndex: index,
)));
},
),
);
},
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: model.imageUrlArray.length ==1?2: 3,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
),
)
我们这里使用到的是自定义的一个 id,通过所以跟服务器获取列表每一行的id来拼接
List imageModel(){
List imgList = List();
for (int x = 0;x<model.imageUrlArray.length;x ++)
{
GalleryExampleItem item = GalleryExampleItem();
item.id = 'tag${x}+${model.id}';
item.resource =model.imageUrlArray[x];
imgList.add(item);
}
print(imgList.first.id);
return imgList;
}