HarmonyOS社交通讯应用开发16:如何实现本地图库选择

📅 2026/8/25 6:42:28
HarmonyOS社交通讯应用开发16:如何实现本地图库选择
本地图库选择引言发布内容的第一件事通常是配图。在 ContinuePublish 发布页里点击虚线加号 → 选择本地设备会弹出系统图库选择器选完图片后缩略图直接以九宫格形式出现在编辑区顶部。这条看似简单的链路背后其实串起了四个系统能力PhotoViewPicker系统相册选择器免权限、免申请photoAccessHelper媒体库访问助手用来按 URI 精确查回图片资源PhotoAsset.getThumbnail获取图片缩略图避免把原图整张加载进内存ImagePacker.packToData把 PixelMap 压缩编码成 JPEG 字节流为写入分布式文件做准备。本篇按选图 → 取缩略图 → 编码落盘的调用链逐一讲解。知识点讲解PhotoViewPicker系统级图片选择器HarmonyOS 提供了安全控件思路下的系统选择器PhotoViewPicker开发者只配置选项、调用select()系统会弹出官方相册界面用户选完返回一个只读的 URI 列表。因为选择过程发生在系统进程里应用拿到的只是用户明确授权的那几张图的 URI所以不需要申请READ_IMAGEVIDEO这类敏感权限——这是它最大的价值。同样的系统代办思路还体现在粘贴场景的PasteButton第 18 篇上两者都是把敏感操作交给系统应用只收结果。配置项PhotoSelectOptions里有两个常用字段MIMEType允许的文件类型如IMAGE_TYPE仅图片、VIDEO_TYPE仅视频、IMAGE_VIDEO_TYPE图片视频maxSelectNumber最多可选数量。一个值得记住的细节选择器返回的photoUris是URI 引用而不是文件内容——读取内容仍需后续步骤这种先拿引用、按需读取的设计避免了把用户图库整批拷进应用内存。photoAccessHelper getAssets按 URI 查回资源选择器返回的photoUris是形如file://media/Photo/xxx的 URI 字符串。要拿到这张图的元数据名字、缩略图需要用photoAccessHelper.getPhotoAccessHelper(context)拿到媒体库助手再通过getAssets(fetchOption)查询。查询条件用FetchOptions描述其中predicates是 DataSharePredicates数据共享谓词可以理解为数据库 where 条件predicates.equalTo(uri,uri);// 只查 uri 等于目标值的资源fetchColumns声明需要返回哪些元数据列——传空数组表示使用默认列。查询结果FetchResultPhotoAsset是一个结果集用getFirstObject()取第一条按 uri 精确匹配时只有一条。注意FetchResult用完后应调用close()释放游标资源本项目示例省略了这步属简化处理。PhotoAsset.getThumbnail缩略图接口PhotoAsset是单张媒体资源的抽象displayName是文件名。getThumbnail(callback)异步返回该资源的缩略图PixelMap——注意是缩略图而不是原图尺寸远小于原图直接用于九宫格预览非常合适内存开销也小。若需原图另有getImage类接口但预览场景用缩略图是公认的最佳实践。PixelMap是 ArkUI/ImageKit 中的像素图对象既可以直接喂给Image组件显示本项目Image(item.imagePixelMap)就是这么用的也可以交给编码器转成字节流。ImagePacker.packToDataPixelMap 编码image.createImagePacker()创建图片打包器packToData(pixelMap, options)把 PixelMap 编码成ArrayBuffer字节流。PackingOption可指定格式如image/jpeg与质量0~100。本项目用质量 100 的 JPEG 编码缩略图再交给writeDistributedFile写入分布式文件目录——这是为应用接续到另一台设备后能恢复图片做的准备接续机制在后续模块详解。编码接口是异步的返回 Promise失败会以BusinessError形式进入.catch。工程里所有异步图片操作都保留了.catch并打 hilog——**每个异步环节都要有错误出口** 是这段代码想教给你的第一课。结合本项目源码分析第一步封装选择器——FileUtil.fileSelect文件路径entry/src/main/ets/utils/FileUtil.ets。工程把选图封装成独立工具函数返回PromiseArraystringURI 数组exportasyncfunctionfileSelect():PromiseArraystring {letimgUri:Arraystring [];letphotoSelectOptions newphotoAccessHelper.PhotoSelectOptions(); photoSelectOptions.MIMEType photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;// 仅图片photoSelectOptions.maxSelectNumber5;// 单次最多选 5 张letphotoPicker newphotoAccessHelper.PhotoViewPicker();try{letphotoSelectResult awaitphotoPicker.select(photoSelectOptions);if(photoSelectResult photoSelectResult.photoUris photoSelectResult.photoUris.length0) { imgUri photoSelectResult.photoUris;// 用户选中的图片 URI 列表returnimgUri; }else{return[];// 用户取消或未选中} }catch(error) { hilog.error(DOMAIN,TAG,FORMAT,PhotoViewPicker failed with err:${error.code},${error.message});return[]; } }注意两个细节maxSelectNumber 5是单次选择的上限而发布页整体的媒体上限是 9MAX_ADD_MEDIA_NUM两者会在上层叠加校验select()是 Promise 风格用await等待用户完成选择异常路径用户取消、权限异常都返回空数组调用方无需感知具体错误。第二步数量校验 逐张取缩略图——AddMedia.selectImage文件路径entry/src/main/ets/view/contentEditor/AddMedia.ets。selectImage():void{fileSelect().then((uri:ArrayResourceStr) {// 已选 新选超过上限 9 时截断到剩余名额if((this.mediaUriArray.length uri.length) CommonConstants.MAX_ADD_MEDIA_NUM) { uri.lengthCommonConstants.MAX_ADD_MEDIA_NUM-this.mediaUriArray.length;try{this.getUIContext().getPromptAction().showToast({message: $r(app.string.add_picture_prompt) }); }catch(err) { hilog.error(DOMAIN,TAG,FORMAT,ShowToast failed. Cause code:${err.code}, message:${err.message}); } }// 逐张把 URI 转成缩略图并加入列表uri.forEach((item: ResourceStr) {this.getThumbnail(itemasstring); }); }) }这里有一段值得品味的代码uri.length 9 - this.mediaUriArray.length;。ArkTS 的数组 length 可以赋值赋值后数组会被截断——这是利用语言特性快速丢弃超量部分只保留放得下的图同时弹 Toast 提示用户数量已达上限资源add_picture_prompt的文案是 The number of added pictures/videos reaches the upper limit.。第三步getAssets 查资源 取缩略图——getThumbnailasyncgetThumbnail(uri:string) {letphAccessHelper photoAccessHelper.getPhotoAccessHelper(this.context);// 构造查询谓词只查 uri 匹配的那张图letpredicates: dataSharePredicates.DataSharePredicatesnewdataSharePredicates.DataSharePredicates(); predicates.equalTo(uri, uri);letfetchOption: photoAccessHelper.FetchOptions {fetchColumns: [],// 不额外取字段predicates: predicates };try{letfetchResult: photoAccessHelper.FetchResultphotoAccessHelper.PhotoAsset awaitphAccessHelper.getAssets(fetchOption);letasset: photoAccessHelper.PhotoAssetawaitfetchResult.getFirstObject();// 从资源取缩略图回调式asset.getThumbnail((err, pixelMap) {if(err undefined) { hilog.info(DOMAIN,TAG,FORMAT,getThumbnail successful pixelMap);// 去掉扩展名得到纯文件名作为展示名letimageName asset.displayName.substring(0, (asset.displayName).indexOf(.));// 编码并写入分布式文件this.PixelMapToBuffer(pixelMap, imageName);// 加入全局媒体列表UI 立刻刷新this.mediaUriArray.push({imagePixelMap: pixelMap,mediaName: imageName,mediaType:MediaType.MEDIA_IMAGE}); }else{ hilog.error(DOMAIN,TAG,FORMAT,getThumbnail fail with error:${err.code},${err.message}); } }); }catch(err) { hilog.error(DOMAIN,TAG,FORMAT,ShowToast failed. Cause code:${err.code}, message:${err.message}); } }this.context是组件里通过this.getUIContext().getHostContext()拿到的 UIAbilityContext媒体库助手必须用它初始化。取缩略图成功后mediaUriArray里 push 一条MediaInfo记录——mediaUriArray是StorageLink(mediaUriArray)所以这行 push 会立刻驱动 AddMedia 的 List 刷新出新的缩略图格子。第四步编码落盘——PixelMapToBufferPixelMapToBuffer(pixelMap: image.PixelMap,displayName:string):void{constimagePackerApi: image.ImagePacker image.createImagePacker();letpackOpts: image.PackingOption {format:image/jpeg,quality:100};// 把 PixelMap 编码为 JPEG 字节流imagePackerApi.packToData(pixelMap, packOpts).then((data:ArrayBuffer) {// 写入分布式文件目录供跨设备接续时恢复writeDistributedFile(this.context, displayName,MediaType.MEDIA_IMAGE, data); }).catch((err: BusinessError) { hilog.info(DOMAIN,TAG,FORMAT,PackToData failed. Cause code:${err.code}, message:${err.message}); }); }落点writeDistributedFile 写入分布式目录writeDistributedFile也在FileUtil.ets这里顺带看它如何落盘详细机制在模块四展开exportfunctionwriteDistributedFile(context: common.UIAbilityContext, displayName:string, mediaType: MediaType, buf?:ArrayBuffer, uri?:string):void{// 分布式文件目录路径letdistributedDir:string context.distributedFilesDir;letfileName:string/ displayName;letfilePath:string distributedDir fileName;letfile: fileIo.File|undefinedundefined;try{ file fileIo.openSync(filePath, fileIo.OpenMode.READ_WRITE| fileIo.OpenMode.CREATE);if(mediaType MediaType.MEDIA_IMAGE buf) {// 图片把编码后的字节流直接写入fileIo.writeSync(file.fd, buf); }// 视频分支openSync 源文件 copyFileSync后文涉及}catch(error) { ... }finally{if(file) { fileIo.closeSync(file.fd); } } }关键点写入的是context.distributedFilesDir分布式文件目录。这个目录由系统管理同账号的设备间可自动同步——这正是发布内容接续到另一台设备时图片能跟着过去的底层保障。也就是说本地选图看似只做了一件事实际已经为跨设备接续埋好了伏笔。整条链路串联把上面的片段连起来一次本地选图的完整数据流是PhotoViewPicker.select()→ photoUris: Arraystring// 1. 系统相册选图免权限→ getThumbnail(uri)× N → getPhotoAccessHelper(context)→ getAssets({predicates:uri})→ asset.getThumbnail()→ PixelMap// 2. 查回资源、取缩略图→PixelMapToBuffer(pixelMap,name)→ImagePacker.packToData()// 3. 编码为 JPEG 字节流→ writeDistributedFile()// 4. 写入分布式文件目录→ mediaUriArray.push(MediaInfo)// 5. 更新全局状态UI 刷新五个步骤职责单一、层层递进每步的输出恰好是下一步的输入。值得记录的工程细节1. context 从哪里来。AddMedia里this.context在组件声明处一次性获取// Get the UIAbilityContext information of device A.privatecontext this.getUIContext().getHostContext()ascommon.UIAbilityContext;getHostContext()返回宿主 UIAbility 的上下文媒体库助手、文件操作、权限请求都要用它。声明时获取、全组件复用比每个方法里重复调用更整洁。2. 数量上限的双保险。PhotoSelectOptions.maxSelectNumber 5只限制单次选择数量与发布页整体的 9 张上限MAX_ADD_MEDIA_NUM是两层独立约束用户第一次选 5 张、第二次再选 5 张时第二次的 5 张会被selectImage截断到剩余 4 个名额并 toast 提示。系统限制单次 5与业务限制总 9各管一段这个分层思路在做任何批量选择功能时都适用。3. 文件名清洗。asset.displayName.substring(0, displayName.indexOf(.))去掉扩展名只留纯文件名作为mediaName存入MediaInfo。这个名字后续会直接用作分布式文件目录里的文件名writeDistributedFile以displayName拼路径所以必须保证不含路径分隔符等危险字符——substring截取正好顺带完成了清洗。4. 图片与视频分支。writeDistributedFile的落盘逻辑按MediaType分流图片直接把编码后的ArrayBuffer写盘fileIo.writeSync视频则打开源 URI 的文件用copyFileSync复制——不同媒体类型的落盘姿势不同但对外暴露的接口签名统一调用方无需关心内部差异if(mediaTypeMediaType.MEDIA_IMAGEbuf) { fileIo.writeSync(file.fd,buf);// 图片字节流直写}elseif(mediaTypeMediaType.MEDIA_VIDEOuri) { srcFile fileIo.openSync(uri,fileIo.OpenMode.READ_ONLY); fileIo.copyFileSync(srcFile.fd,file.fd);// 视频文件复制}小结本地图库选择这条链路的工程价值主要体现在三点权限策略最优PhotoViewPicker是系统选择器选图全程由系统完成应用只拿用户明确授权的 URI无需申请相册读权限——比直接遍历媒体库的方案更安全、更省事。内存友好全程操作的是缩略图 PixelMap而不是把原图加载进内存即便要落盘也是缩略图编码而非原图。为接续铺路选完图立刻写入分布式文件目录让本机编辑、他机续写成为可能。初级开发者可以沿着AddMedia.selectImage → getThumbnail → PixelMapToBuffer这条路径复现一遍就会对系统选择器 媒体库助手 图片编码这套组合拳有直观认识。复现时建议刻意练习三件事给每个异步环节补上错误处理.catch 日志、注意FetchResult的资源释放、以及用断点观察mediaUriArray.push之后列表是否即时刷新——后者的答案就是StorageLink的魔法所在。下一篇我们把视线从本机移到跨设备——看看如何从另一台设备上拉取媒体。本文引用源码entry/src/main/ets/utils/FileUtil.ets、entry/src/main/ets/view/contentEditor/AddMedia.ets、entry/src/main/ets/model/ContentInfo.ets