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,
        ),
      );
    },
  ),
);

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

相关推荐
一只大侠的侠4 小时前
Flutter开源鸿蒙跨平台训练营 Day 10特惠推荐数据的获取与渲染
flutter·开源·harmonyos
崔庆才丨静觅6 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60617 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了7 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅7 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅7 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
renke33648 小时前
Flutter for OpenHarmony:色彩捕手——基于HSL色轮与感知色差的交互式色觉训练系统
flutter
崔庆才丨静觅8 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment8 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅8 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端