likes
comments
collection
share

Flutter大图预览并保存图片至相册

作者站长头像
站长
· 阅读数 3

在业务中大图预览并保存图片至相册的需求是非常常见的:

Flutter大图预览并保存图片至相册

本文将结合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,
        ),
      );
    },
  ),
);

至此一个简单图片预览并可以批量下载至相册的业务功能就实现了。