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权限
xml 复制代码
<!-- 读取缓存数据权限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!-- 读取缓存数据权限 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
  • ios Runner目录下Info.plist添加NSPhotoLibraryAddUsageDescription
plist 复制代码
<key>NSPhotoLibraryAddUsageDescription</key>
<string>允许APP保存图片到相册</string>

ios目录下Podfile文件添加以下内容,用于permission_handler动态获取权限:

主要是需要将 PERMISSION_PHOTOS=1 的注释去掉,查看详情

Podfile 复制代码
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

封装动态获取权限功能

dart 复制代码
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;
      }
    }
}

获取相册权限

dart 复制代码
bool storageStatus = await requestScopePermission(Permission.storage);

抽离保存单张图片功能

NetUtil是使用dio封装的request自定义类,可自行使用自己项目的方法来调用,保证responseType: ResponseType.bytes类型即可。

将结果传给ImageGallerySaver即可完成图片的保存。

dart 复制代码
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 其实不够严谨,需要考虑图片下载失败等因素,可以将request与saveImage失败的状态都存到curRes中然后再去判断即可。

dart 复制代码
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个参数

dart 复制代码
final List imgList; // 图片列表
final int index;  // 当前预览的图片索引

预览页面代码:

dart 复制代码
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对路由设置渐显的效果,让预览组件更有沉浸感.

dart 复制代码
Navigator.push(
  context,
  PageRouteBuilder(
    pageBuilder: (context, animation, secondaryAnimation) {
      return FadeTransition(
        opacity: animation,
        child: PhotoScreen(
          imgList: _sharePoster!.shareImgs!,
          index: index,
        ),
      );
    },
  ),
);

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

相关推荐
UXbot2 小时前
UI设计工具推荐合集
前端·人工智能·ui
猛扇赵四那边好嘴.2 小时前
Flutter 框架跨平台鸿蒙开发 - 非遗文化查询:传承中华文化瑰宝
flutter·华为·harmonyos
●VON2 小时前
Flutter for OpenHarmony 21天训练营 Day01 总结:从零搭建开发环境
flutter·环境配置·openharmony·训练营·跨平台开发·von
●VON2 小时前
0基础也能行!「Flutter 跨平台开发训练营」1月19日正式启动!
学习·flutter·von·openjiuwen
敲敲了个代码3 小时前
如何优化批量图片上传?队列机制+分片处理+断点续传三连击!(附源码)
前端·javascript·学习·职场和发展·node.js
@AfeiyuO3 小时前
Vue 引入全局样式scss
前端·vue·scss
光影少年3 小时前
flex布局和grid布局区别,实现两边固定布局中间自适应
前端·css3·web·ai编程
全栈测试笔记3 小时前
异步函数与异步生成器
linux·服务器·前端·数据库·python
EndingCoder3 小时前
配置 tsconfig.json:高级选项
linux·前端·ubuntu·typescript·json
木风小助理4 小时前
JavaStreamAPI的性能审视,优雅语法背后的隐形成本与优化实践
java·前端·数据库