鸿蒙(HarmonyOS)开发之无须申请权限写入图片到相册

简介: 如何不申请权限写入相册

写入图片到相册介绍

在上一篇中,猫林老师给大家分享了如何在不申请权限的情况下读取相册内容。这一篇教大家如何写入图片到相册。

这在应用开发中,也是一个很常见的场景,比如我们要做一个文件扫描的功能。那是不是得把扫描的结果保存到图库呢?再比如我们做一个美颜的功能,那把相册里的原始图片读取出来后,经过我们的美容处理,是不是还得把美容后的结果写入回相册呢?

所以这也是我们必须要学习和掌握的内容。其实写入相册也是HarmonyOS管控的比较严的一个权限,但好在HarmonyOS依然考虑到写入相册对于开发者而言也是一个非常常用的一个功能,因而提供了两种方案来实现图片保存到相册,分别是:

  1. 安全控件保存
  2. 弹窗授权保存

安全控件保存 - 基本使用

这其实就是HarmonyOS提供的一个类似按钮一样的组件,他叫SaveButton,在界面上放一个它即可轻松实现保存,我们先看看它长啥样。我们在界面上写一个SaveButton,如下图

image-20241215205345414

可以看到,从外形上看,就是一个带图标的按钮。当然,如果你要是觉得这个按钮的图标、文字不是你想要的,你还可以在使用时传递参数来修改。

对应的参数有

  • icon:设置图标,

    • 如果写SaveButton时写了{},并给了其他参数,唯独没给icon,则没有图标。
    • 如果要设置,仅能设置两个值,分别代表线条图标,填充图标。分别为:1
      1. SaveIconStyle.FULL_FILLED
      2. SaveIconStyle.LINES
    • 这两个值没太大变化可以理解为前一个线条粗一条,后一个线条细一点
  • text: 设置文字,但是仅提供固定的几个文字让你选择,无法自定义。可选择的文字见下表

    | 名称 | 值 | 说明 |
    | :----------------------- | :--- | :----------------------------------- |
    | DOWNLOAD | 0 | 保存按钮的文字描述为“下载”。 |
    | DOWNLOAD_FILE | 1 | 保存按钮的文字描述为“下载文件”。 |
    | SAVE | 2 | 保存按钮的文字描述为“保存”。 |
    | SAVE_IMAGE | 3 | 保存按钮的文字描述为“保存图片”。 |
    | SAVE_FILE | 4 | 保存按钮的文字描述为“保存文件”。 |
    | DOWNLOAD_AND_SHARE | 5 | 保存按钮的文字描述为“下载分享”。 |
    | RECEIVE | 6 | 保存按钮的文字描述为“接收”。 |
    | CONTINUE_TO_RECEIVE | 7 | 保存按钮的文字描述为“继续接收”。 |
    | SAVE_TO_GALLERY12+ | 8 | 保存按钮的文字描述为“保存至图库”。 |
    | EXPORT_TO_GALLERY12+ | 9 | 保存按钮的文字描述为“导出”。 |
    | QUICK_SAVE_TO_GALLERY12+ | 10 | 保存按钮的文字描述为“快速保存图片”。 |
    | RESAVE_TO_GALLERY12+ | 11 | 保存按钮的文字描述为“重新保存”。 |

​ buttonType:设置按钮样式、胶囊、圆形、普通(跟按钮的三大样式一样)

  • 举个🌰,如下图

    image-20241215210743262

    • 对应代码为
    
    
    struct Index {
         
      build() {
         
        Column({
          space: 20 }) {
         
          // 什么都不传时,有图标有文字,文字默认为下载
          SaveButton()
    
          // 仅传图标只有图标
          SaveButton({
          icon: SaveIconStyle.FULL_FILLED })
    
          // 仅传文字只有文字,因为选择的是Save枚举,所以显示保存
          SaveButton({
          text: SaveDescription.SAVE })
    
          // 有图标,且按钮样式为原型
          SaveButton({
          icon: SaveIconStyle.FULL_FILLED, buttonType: ButtonType.Circle })
        }
        .width('100%')
        .height('100%')
      }
    }
    

回到正题,如何使用它保存图片到相册呢?我们需要给它加点击事件

SaveButton()
.onClick((event: ClickEvent, result: SaveButtonOnClickResult) => {
   

})

参数1:事件对象

参数2:用户授权结果,因为这个按钮自带弹框,问用户是否允许保存到图库,如下图。所以我们需要拿到用户的点击结果,只有点击了允许,我们才往下执行代码。那如何判断用户点了允许呢?就是根据SaveButtonOnClickResult,它有两个值:如果返回的结果是SUCCESS即为用户授权,如为:TEMPORARY_AUTHORIZATION_FAILED即为授权失败

image-20241215211512537

安全控件保存 - 实现写入图片到图库(相册)

根据官方说明,需要两大步

  1. 调用MediaAssetChangeRequest类的的createImageAssetRequest方法创建一个资产变更请求
  2. 调用PhotoAccessHelper实例对象的 applyChanges方法,传入上述请求。用来提交本次媒体变更请求。

这里先解释下MediaAssetChangeRequest这个类,这个类是专门用来向系统做资产变更请求的

那什么是手机资产呢?可以简单粗暴的理解为手机里的任意文件数据都是资产。比如图片是资产、视频也是资产。对资产做变更说人话就相当于是对这些文件做改变。所以,我们要写入一个图片到相册,就相当于是资产变更。就需要用到MediaAssetChangeRequest这个类。可是,根据我们刚刚说的,任意文件都叫资产,而我们这次要做的是给相册创建一张新图片,因此使用这个类的createImageAssetRequest方法,代表创建一个图片资产变更请求对象。

正如上面所说MediaAssetChangeRequest.createImageAssetRequest仅仅只是得到一个图片资产变更请求的对象,还得同意这个请求才行。怎么同意呢?即用PhotoAccessHelper实例对象的 applyChanges方法来向系统提交本次请求。

以上是对概念的解释,我们聊聊代码使用

  1. MediaAssetChangeRequest类的的createImageAssetRequest方法需要传入两个参数。1. 当前上下文。 2. 图片的uri。

  2. PhotoAccessHelper实例对象的 applyChanges方法,首先需要创建PhotoAccessHelper类的实例(这是一个专门用来管理相册的实例),再调用applyChanges。而PhotoAccessHelper的实例是通过photoAccessHelper.getPhotoAccessHelper方法,传入当前上下文对象得到的

对应代码如下

// 获取当前上再问
let context = getContext();
// 使用createImageAssetRequest得到一个创建图片资产的请求
let assetChangeRequest = photoAccessHelper.MediaAssetChangeRequest.createImageAssetRequest(context, 图片的URI);

// 得到PhotoAccessHelper的实例对象
let phAccessHelper = photoAccessHelper.getPhotoAccessHelper(context);
// 利用上面得到的实例对象,调用applyChanges方法并传入创建图片资产的请求。
// 注意:这是一个异步操作,最好用.then捕捉,或者用await等待也可
await phAccessHelper.applyChanges(assetChangeRequest);

好了,知道这些以后,我们结合上篇文章的读取相册图片,来实现一个读取相册图片,并根据它创建一张一模一样的新图片写入到相册功能,代码如下

import {
    photoAccessHelper } from '@kit.MediaLibraryKit';



struct Index {
   
   imgUri: string = ''

  build() {
   
    Column({
    space: 20 }) {
   

      // 选择图片
      Button('选择图片')
        .width('80%')
        .onClick(() => {
   
          // 实例化选择器
          let photoPicker = new photoAccessHelper.PhotoViewPicker()
          // 开始选择图片,设置只允许选择图片,且最大选择1张
          photoPicker.select({
   
            MIMEType: photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE,
            maxSelectNumber: 1
          })
            .then((res: photoAccessHelper.PhotoSelectResult) => {
   

              // 读取成功则赋值给变量去展示到界面
              this.imgUri = res.photoUris[0]
            })
            .catch((err: Error) => {
   
              // 出错回调
              console.log(err.message)
            })
        })

      // 安全控件
      SaveButton()
        .onClick(async (event: ClickEvent, result: SaveButtonOnClickResult) => {
   
          // 判断用户允许授权
          if (result == SaveButtonOnClickResult.SUCCESS) {
   
                        // 得到当前上下文
            let context = getContext();
            // 创建图片资产请求对象
            let assetChangeRequest =
              photoAccessHelper.MediaAssetChangeRequest.createImageAssetRequest(context, this.imgUri);
            // 得到photoAccessHelper实例,也即得到图片管理实例对象
            let phAccessHelper = photoAccessHelper.getPhotoAccessHelper(context);
            // 提交本次请求
            await phAccessHelper.applyChanges(assetChangeRequest);

          } else {
   
            console.error('SaveButtonOnClickResult create asset failed');
          }
        })
    }
    .width('100%')
    .height('100%')
  }
}

这一段代码不多,倒是概念需要理解。总而言之核心就是:准备一个创建图片的请求(通过URI指定创建什么图片),然后向系统提交本次请求

安全控件保存 - 优缺点总结

优点:代码简单,固定的两大步:创建请求、提交请求

缺点:必须再在界面上提供一个额外按钮,且此按钮无法深度自定义

弹窗授权保存 - 基本使用

这是一套不用在界面上额外添加按钮,或者也可以深度进行按钮定制的一套方式。

其开发步骤虽对比安全控件步骤略多,但依然属于比较简单实现的一种方式(我们后续如果做文档扫描功能,会用这种方式更恰当)。

我们来看看大概的实现步骤:

  1. 指定待保存到图库(相册)的图片URI
  2. 指定待保存照片的创建选项,包括文件后缀和照片类型,文件标题等
  3. 调用showAssetsCreationDialog,基于弹窗授权的方式获取媒体库的目标uri
  4. 将来源照片内容写入到媒体库的目标uri(需要用到fs文件流读取原图,写入目标图)

根据步骤,我们来看看大致实现代码,如下

// 导入相册访问帮助类
import {
    photoAccessHelper } from '@kit.MediaLibraryKit';
// 导入文件流
import {
    fileIo } from '@kit.CoreFileKit';

async function example() {
   
  try {
   
    // 指定待保存到媒体库的位于应用沙箱的图片uri,注意:这个URI仅仅只是虚构的uri,实际替换成本身图片Uri即可
    let srcFileUri = 'file://com.example.temptest/data/storage/el2/base/haps/entry/files/test.jpg';

    // 因为接下来的方法需要传入的是来源URI数组,所以包装成数组
    let srcFileUris: Array<string> = [
      srcFileUri
    ];

    // 指定待保存照片的创建选项,包括文件后缀和照片类型,标题和照片子类型可选
    let photoCreationConfigs: Array<photoAccessHelper.PhotoCreationConfig> = [
      {
   
        title: 'test', // 可选,文件名
        fileNameExtension: 'jpg', // 文件后缀
        photoType: photoAccessHelper.PhotoType.IMAGE, // 文件类型
      }
    ];


        // 获得当前上下文
        let context = getContext(this);
        let phAccessHelper = photoAccessHelper.getPhotoAccessHelper(context);

    // 基于弹窗授权的方式获取媒体库的目标uri
    // 这句代码会弹出一个窗提示框,并显示出待写入相册的图片,问用户是否允许保存
    let desFileUris: Array<string> = await phAccessHelper.showAssetsCreationDialog(srcFileUris, photoCreationConfigs);

    // 设置一个写入文件流,文件位置为上面设置的相册位置
    let desFile: fileIo.File = await fileIo.open(desFileUris[0], fileIo.OpenMode.WRITE_ONLY);
    // 设置一个读取文件流,文件位置为之前的来源图片URI
    let srcFile: fileIo.File = await fileIo.open(srcFileUri, fileIo.OpenMode.READ_ONLY);
    // 开始将读出来的文件流复制给写文件流(即往相册写内容)
    await fileIo.copyFile(srcFile.fd, desFile.fd);
    // 关闭文件流
    fileIo.closeSync(srcFile);
    fileIo.closeSync(desFile);

  } catch (err) {
   
    console.error(`failed to create asset by dialog successfully errCode is: ${
   err.code}, ${
   err.message}`);
  }
}

这里因为篇幅关系,就不解释文件流了。跟Node、Java等语言中的文件流概念完全一致。无非就是读取、写入两套。这里略过不表。

这里需要解释的代码是:photoCreationConfigs用来设置写入到相册里的文件之文件名、文件类型、后缀等

showAssetsCreationDialog会出来一个弹窗让用户确认(如下图)。如果用户确认后,会将设置的写入路径、读取路径变的具有操作权限方便后续文件流操作

image-20241215235833073

当然,如果你实在看不懂代码,猫林老师给你个绝招:这段代码你会复制即可。然后需要改的部分仅仅只有两处

  1. 来源图片URI也即srcFileUri这个变量,把这个变量改成你要写入到相册的原始图片路径。
  2. photoCreationConfigs这个变量里,把要写入的新图片文件名改了

弹窗授权保存 - 实现写入图片到图库

我们继续实现一个读取相册图片,并根据它创建一张一模一样的新图片写入到相册功能,代码如下

import {
    photoAccessHelper } from '@kit.MediaLibraryKit';
import {
    fileIo } from '@kit.CoreFileKit';



struct Index {
   
   imgUri: string = ''

  build() {
   
    Column({
    space: 20 }) {
   

      // 选择图片
      Button('选择图片')
        .width('80%')
        .onClick(() => {
   
          // 实例化选择器
          let photoPicker = new photoAccessHelper.PhotoViewPicker()
          // 开始选择图片,设置只允许选择图片,且最大选择1张
          photoPicker.select({
   
            MIMEType: photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE,
            maxSelectNumber: 1
          })
            .then((res: photoAccessHelper.PhotoSelectResult) => {
   

              // 读取成功则赋值给变量去展示到界面
              this.imgUri = res.photoUris[0]
            })
            .catch((err: Error) => {
   
              // 出错回调
              console.log(err.message)
            })
        })

      Button('保存图片')
        .width('80%')
        .onClick(async () => {
   
          try {
   
            // 仅仅替换成我们想要写入的原本图片URI
            let srcFileUri = this.imgUri;

            // 因为接下来的方法需要传入的是来源URI数组,所以包装成数组
            let srcFileUris: Array<string> = [
              srcFileUri
            ];

            // 指定待保存照片的创建选项,包括文件后缀和照片类型,标题和照片子类型可选
            let photoCreationConfigs: Array<photoAccessHelper.PhotoCreationConfig> = [
              {
   
                title: 'test', // 可选,文件名
                fileNameExtension: 'jpg', // 文件后缀
                photoType: photoAccessHelper.PhotoType.IMAGE, // 文件类型
              }
            ];


            // 获得当前上下文
            let context = getContext(this);
            let phAccessHelper = photoAccessHelper.getPhotoAccessHelper(context);

            // 基于弹窗授权的方式获取媒体库的目标uri
            // 这句代码会弹出一个窗提示框,并显示出待写入相册的图片,问用户是否允许保存
            let desFileUris: Array<string> =
              await phAccessHelper.showAssetsCreationDialog(srcFileUris, photoCreationConfigs);

            // 设置一个写入文件流,文件位置为上面设置的相册位置
            let desFile: fileIo.File = await fileIo.open(desFileUris[0], fileIo.OpenMode.WRITE_ONLY);
            // 设置一个读取文件流,文件位置为之前的来源图片URI
            let srcFile: fileIo.File = await fileIo.open(srcFileUri, fileIo.OpenMode.READ_ONLY);
            // 开始将读出来的文件流复制给写文件流(即往相册写内容)
            await fileIo.copyFile(srcFile.fd, desFile.fd);
            // 关闭文件流
            fileIo.closeSync(srcFile);
            fileIo.closeSync(desFile);

          } catch (err) {
   
            console.error(`failed to create asset by dialog successfully errCode is: ${
   err.code}, ${
   err.message}`);
          }
        })
    }
    .width('100%')
    .height('100%')
  }
}

细心的读者已经发现了,这个代码仅仅就是复制基本使用里的代码,只是替换了srcFileUri这个变量的值。这也能实现图片写入。

总结

  • 由于HarmonyOS对用户的隐私绝对保护,导致相册读写的权限难以申请。好在HarmonyOS提供了这种无须申请权限即可读取与写入相册的方法。
  • 虽看代码感觉繁琐略多,但实际上代码都是固定的,例如上述的弹窗授权。我们仅需改改来源图片URI即可
  • 本篇内容请一定要有印象。因为后续我们要是制作文档扫描功能会用到
相关文章
|
10天前
|
存储 人工智能 JavaScript
Harmony OS开发-ArkTS语言速成二
本文介绍了ArkTS基础语法,包括三种基本数据类型(string、number、boolean)和变量的使用。重点讲解了let、const和var的区别,涵盖作用域、变量提升、重新赋值及初始化等方面。期待与你共同进步!
72 47
Harmony OS开发-ArkTS语言速成二
|
5天前
|
人工智能 安全 JavaScript
《鸿蒙HarmonyOS应用开发从入门到精通(第2版)》学习笔记——HarmonyOS纯血鸿蒙新特性
HarmonyOS 3.1引入了Stage模型,增强ArkTS语言、应用程序框架、Web、ArkUI等子系统能力。新增功能包括Ability框架的Stage开发模型、ArkUI组件能力提升、应用包管理接口、公共基础类库支持Buffer读写、Web服务文档预览及编辑、图形图像编解码支持等。从API 9开始,Stage模型成为主要开发模型,支持更灵活的应用生命周期管理和窗口调度,提供更好的组件与窗口弱耦合体验。此外,HarmonyOS NEXT开发者预览版实现了全面自研,被称为“纯血鸿蒙”,具备自主可控、高度弹性、更强的安全性和隐私保护特性。
53 21
|
11天前
|
前端开发 API 数据库
鸿蒙开发:异步并发操作
在结合async/await进行使用的时候,有一点需要注意,await关键字必须结合async,这两个是搭配使用的,缺一不可,同步风格在使用的时候,如何获取到错误呢,毕竟没有catch方法,其实,我们可以自己创建try/catch来捕获异常。
鸿蒙开发:异步并发操作
|
2天前
|
存储 JSON 区块链
【HarmonyOS NEXT开发——ArkTS语言】购物商城的实现【合集】
HarmonyOS应用开发使用@Component装饰器将Home结构体标记为一个组件,意味着它可以在界面构建中被当作一个独立的UI单元来使用,并且按照其内部定义的build方法来渲染具体的界面内容。txt:string定义了一个名为Data的接口,用于规范表示产品数据的结构。src:类型为,推测是用于引用资源(可能是图片资源等)的一种特定类型,用于指定产品对应的图片资源。txt:字符串类型,用于存放产品的文字描述,比如产品名称等相关信息。price:数值类型,用于表示产品的价格信息。
22 5
|
2天前
|
开发工具 开发者 容器
【HarmonyOS NEXT开发——ArkTS语言】欢迎界面(启动加载页)的实现【合集】
从ArkTS代码架构层面而言,@Entry指明入口、@Component助力复用、@Preview便于预览,只是初窥门径,为开发流程带来些许便利。尤其动画回调与Blank组件,细节粗糙,后续定当潜心钻研,力求精进。”,字体颜色为白色,字体大小等设置与之前类似,不过动画配置有所不同,时长为。,不过这里没有看到额外的动画效果添加到这个特定的图片元素上(与前面带动画的元素对比而言)。这是一个显示文本的视图,文本内容为“奇怪的知识”,设置了字体颜色为灰色(的结构体,它代表了整个界面组件的逻辑和视图结构。
15 1
|
12天前
|
API 索引
鸿蒙开发:实现一个超简单的网格拖拽
实现拖拽,最重要的三个方法就是,打开编辑状态editMode,实现onItemDragStart和onItemDrop,设置拖拽移动动画和交换数据,如果想到开启补位动画,还需要实现supportAnimation方法。
70 13
鸿蒙开发:实现一个超简单的网格拖拽
|
11天前
|
索引 API
鸿蒙开发:自定义一个股票代码选择键盘
金融类的软件,特别是股票基金类的应用,在查找股票的时候,都会有一个区别于正常键盘的键盘,也就是股票代码键盘,和普通键盘的区别就是,除了常见的数字之外,也有一些常见的股票代码前缀按钮,方便在查找股票的时候,更加方便的进行检索。
鸿蒙开发:自定义一个股票代码选择键盘
|
11天前
|
API
鸿蒙开发:自定义一个英文键盘
实现方式呢,有很多种,目前采用了比较简单的一种,如果大家采用网格Grid组件实现方式,也是可以的,但是需要考虑每行的边距以及数据,还有最后两行的格子占位问题。
鸿蒙开发:自定义一个英文键盘
|
12天前
|
存储 JSON 数据库
鸿蒙元服务项目实战:备忘录内容编辑开发
富文本内容编辑我们直接使用RichEditor组件即可,最重要的就是参数,value: RichEditorOptions,通过它,我们可以用来设置样式,和获取最后的富文本内容,这一点是很重要的。
鸿蒙元服务项目实战:备忘录内容编辑开发
|
12天前
|
开发框架 JavaScript 前端开发
Harmony OS开发-ArkT语言速成一
本文介绍ArkTS语言,它是鸿蒙生态的应用开发语言,基于TypeScript,具有静态类型检查、声明式UI、组件化架构、响应式编程等特性,支持跨平台开发和高效性能优化。ArkTS通过强化静态检查和分析,提升代码健壮性和运行性能,适用于Web、移动端和桌面端应用开发。关注我,带你轻松掌握HarmonyOS开发。
41 5
Harmony OS开发-ArkT语言速成一