各类格式的场景实战经验

发布时间:2026/7/21 7:38:15
各类格式的场景实战经验 一、前言GIF合成有多实用不知道你有没有遇到过这样的场景- 拍了一连串动作照片想合成动图分享- 想做表情包需要把多张图片合成 GIF- 想记录一个连续动作比如健身动作、舞蹈动作- 想做教程用 GIF 展示操作步骤这种 GIF 合成功能说实话真的挺实用的。用户参与感强分享欲一下子就上来了。我们做开发者工具箱 APP 的时候想着加个好玩又实用的功能就做了这个 GIF 合成示例。一开始我们还觉得这玩意儿挺复杂得用第三方库吧得自己编码吧后来研究了一下发现用鸿蒙原生的 ImagePacker 就能搞定而且效果还不错。![](https://i-blog.csdnimg.cn/img_convert/30d6da87f28e7af47706a4a46f7030b4.gif)今天我就把这个实现过程分享出来把我们踩过的坑、总结的经验都告诉你。对了这个功能在 API 18 及以上版本才支持大家注意一下版本要求。这话虽然说了好几遍了但真的很重要。二、核心 APIImagePacker在实现 GIF 合成之前我们需要了解一个核心 APIImagePacker。这个组件是鸿蒙提供的图片打包工具可以将多张图片编码成 GIF 动图。### 2.1 主要方法typescriptimport image from ohos.multimedia.image;// 创建 ImagePacker 实例let packer image.createImagePacker();// 将多个 PixelMap 编码成 GIF 文件await packer.packToFileFromPixelmapSequence(pixelMapList, // PixelMap 数组fileFd, // 文件描述符ops // 编码参数);说实话这个方法名挺长的刚开始用的时候老是记不住多用几次就熟了。### 2.2 编码参数说明typescriptlet ops: image.PackingOptionsForSequence {frameCount: 10, // 帧数GIF 中有多少帧delayTimeList: [50], // 每帧延迟时间单位10ms50500msdisposalTypes: [3], // 帧过渡模式3恢复到之前的状态loopCount: 0 // 循环次数0无限循环};这几个参数很重要决定了 GIF 的播放效果- **frameCount**就是你选了多少张图片一张图片就是一帧- **delayTimeList**控制播放速度数值越小播放越快。50 就是 500 毫秒也就是每秒 2 帧- **disposalTypes**帧与帧之间怎么过渡3 是恢复到之前的状态一般用这个就行- **loopCount**0 就是一直循环1 是播放一次2 是播放两次以此类推说实话刚开始看官方文档的时候这些参数真的是云里雾里的后来一点点试才慢慢搞懂。特别是这个 delayTimeList单位是 10ms不是 ms一开始我们以为是 ms结果生成的 GIF 慢得要命后来才发现是单位搞错了。这个坑希望大家别踩真的。三、完整实现从选图到生成 GIF### 3.1 整体流程整个 GIF 合成的流程其实挺清晰的1. 让用户从图库选择多张图片2. 把每张图片解码成 PixelMap3. 用 ImagePacker 把 PixelMap 数组编码成 GIF4. 保存 GIF 到本地5. 展示或分享看着挺简单吧其实我们一开始连文件放哪儿都不知道后来一点点试才找到这个方案。第一次写的时候连文件应该存哪儿都不知道后来才知道用 cacheDir。### 3.2 第一步从图库选择图片typescriptimport { photoAccessHelper } from kit.MediaLibraryKit;async selectPhotos() {let uris: Arraystring [];let pixelMapList: image.PixelMap[] [];// 创建选择器配置let photoSelectOptions new photoAccessHelper.PhotoSelectOptions();photoSelectOptions.MIMEType photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;photoSelectOptions.maxSelectNumber 9; // 最多选 9 张// 创建选择器并让用户选择let photoPicker new photoAccessHelper.PhotoViewPicker();let result await photoPicker.select(photoSelectOptions);uris result.photoUris; // 获取选中图片的 URIreturn uris;}![](https://i-blog.csdnimg.cn/img_convert/0fb260859849982af2886c32a77aa062.png)**注意**这里最多选 9 张图片你可以根据需要调整。一开始我们设的是 20 张结果发现生成的 GIF 太大加载很慢后来改成了 9 张。这个大家可以试试找到合适的数量。### 3.3 第二步将图片解码为 PixelMaptypescriptimport image from ohos.multimedia.image;import { fileIo } from kit.CoreFileKit;async decodeToPixelMaps(uris: string[]) {let pixelMapList: image.PixelMap[] [];for (let i 0; i uris.length; i) {// 打开文件let fileImg fileIo.openSync(uris[i], fileIo.OpenMode.READ_ONLY);// 读取文件数据let buffer new ArrayBuffer(4096);let readLen fileIo.readSync(fileImg.fd, buffer);// 创建 ImageSourceconst imageSource: image.ImageSource image.createImageSource(fileImg.fd);// 解码为 PixelMaplet decodingOptions: image.DecodingOptions {editable: true,desiredPixelFormat: 3, // RGBA_8888};pixelMapList[i] await imageSource.createPixelMap(decodingOptions);// 关闭文件fileIo.closeSync(fileImg);}return pixelMapList;}**注意**这里有几个关键点一定要记住1. editable: true必须设为 true不然编码时会报错2. desiredPixelFormat: 3RGBA_8888 格式这个是通用的3. 记得关闭文件不然会内存泄漏这个很重要我们一开始就是忘了关闭文件结果生成的 GIF 总是报错查了半天才发现是文件句柄没释放。这个坑希望大家别踩真的这个地方一定要记住。### 3.4 第三步合成 GIFtypescriptimport { common } from kit.AbilityKit;import fs from ohos.file.fs;import { fileUri } from kit.CoreFileKit;async generateGIF(pixelMapList: image.PixelMap[]) {// 获取上下文let context this.getUIContext().getHostContext() as common.UIAbilityContext;// 生成文件路径使用缓存目录let path: string context.cacheDir /result_ new Date().getTime() .gif;// 打开文件let file fs.openSync(path, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE);try {// 配置编码参数let ops: image.PackingOptionsForSequence {frameCount: pixelMapList.length, // 帧数delayTimeList: [50], // 每帧延迟 500msdisposalTypes: [3], // 帧过渡模式loopCount: 0 // 无限循环};// 创建 ImagePackerlet packer image.createImagePacker();// 合成 GIFawait packer.packToFileFromPixelmapSequence(pixelMapList,file.fd,ops);console.info(GIF 合成成功);// 获取 URI 用于展示let uri fileUri.getUriFromPath(path);return uri;} catch (error) {console.error(GIF 合成失败:, error);throw error;} finally {// 关闭文件fs.close(file);}}![](https://i-blog.csdnimg.cn/img_convert/a194e2e2f83fe296161404afdb06bee0.png)**注意**这里有几个关键点1. 使用 cacheDir 缓存目录临时文件会被系统自动清理2. 文件名加上时间戳避免重名3. 记得在 finally 里关闭文件这个很重要4. 用 try-catch 包裹处理可能的错误我们一开始就是没加 try-catch结果出错了都不知道咋回事。后来加了错误处理就能知道具体是哪里出问题了。这个大家一定要加记住啊。### 3.5 完整流程整合typescriptEntryComponentstruct GIFGeneratorPage {State uri: string ;State loading: boolean false;async createGIF() {this.loading true;try {// 1. 选择图片let uris await this.selectPhotos();if (uris.length 0) {this.loading false;return;}// 2. 解码为 PixelMaplet pixelMapList await this.decodeToPixelMaps(uris);// 3. 合成 GIFlet gifUri await this.generateGIF(pixelMapList);// 4. 更新 UIthis.uri gifUri;} catch (error) {console.error(生成 GIF 失败:, error);promptAction.showToast({ message: 生成失败 });} finally {this.loading false;}}build() {Column() {Button(选择图片生成 GIF).onClick(() {this.createGIF();})if (this.loading) {LoadingProgress().width(50).height(50)}if (this.uri) {Image(this.uri).width(300).height(200).objectFit(ImageFit.Contain)}}}}## 四、常见问题与解决方案### 4.1 GIF 太大怎么办**问题**生成的 GIF 文件太大分享很慢**解决方案**1. 减少图片数量9 张就够了不要太多2. 压缩图片在解码前缩小图片尺寸3. 增加延迟时间让每帧显示时间更长减少总帧数typescript// 压缩图片示例let decodingOptions: image.DecodingOptions {editable: true,desiredPixelFormat: 3,size: { width: 400, height: 400 } // 缩小尺寸};### 4.2 GIF 播放太快/太慢怎么办**问题**生成的 GIF 播放速度不合适**解决方案**调整 delayTimeList 参数typescript// 播放快一点每帧 300msdelayTimeList: [30]// 播放慢一点每帧 800msdelayTimeList: [80]// 不同帧不同速度delayTimeList: [50, 50, 100, 50, 50] // 第 3 帧慢一点我们一开始用的是 50后来发现有点快改成了 60就比较合适了。这个大家可以试试找到合适的速度。### 4.3 内存溢出怎么办**问题**图片太多或太大导致内存溢出**解决方案**1. 限制选择数量最多 9 张2. 压缩图片减小尺寸3. 及时释放用完 PixelMap 后调用 release()typescript// 释放 PixelMappixelMapList.forEach(pixelMap {pixelMap.release();});### 4.4 版本兼容性问题**问题**低版本 API 不支持 packToFileFromPixelmapSequence**解决方案**检查 API 版本低版本用其他方案typescriptimport { deviceInfo } from kit.BasicServicesKit;let apiVersion deviceInfo.sdkApiVersion;if (apiVersion 18) {// 使用新方法await packer.packToFileFromPixelmapSequence(...);} else {// 使用老方法或其他方案promptAction.showToast({ message: 您的设备不支持此功能 });}我们一开始就是没检查版本结果在老设备上直接崩溃当时就把我们吓出一身冷汗。后来加了版本检查就安全多了。这个大家一定要加记住啊。## 五、进阶技巧### 5.1 保存到相册typescriptimport { photoAccessHelper } from kit.MediaLibraryKit;async saveToAlbum(uri: string) {let context this.getUIContext().getHostContext() as common.UIAbilityContext;let phAccessHelper photoAccessHelper.getPhotoAccessHelper(context);try {let assetChangeRequest photoAccessHelper.MediaAssetChangeRequest.createImageAssetRequest(context, uri);await phAccessHelper.applyChanges(assetChangeRequest);console.info(保存到相册成功);promptAction.showToast({ message: 已保存至相册 });} catch (error) {console.error(保存失败:, error);promptAction.showToast({ message: 保存失败 });}}### 5.2 分享 GIFtypescriptimport { shareUtil } from kit.ShareKit;async shareGIF(uri: string) {try {await shareUtil.share({uriList: [uri],mimeType: image/gif});console.info(分享成功);} catch (error) {console.error(分享失败:, error);}}### 5.3 预览 GIFtypescriptEntryComponentstruct GIFPreviewPage {State uri: string ;build() {Column() {// 标题栏Row() {Button(返回).onClick(() {router.back();});Text(GIF 预览).fontSize(20).fontWeight(700);}.width(100%).padding(16);// GIF 图片Column() {Image(this.uri).width(100%).objectFit(ImageFit.Contain);}.width(100%).height(85%);// 操作按钮Row() {Button(保存到相册).onClick(() {this.saveToAlbum(this.uri);});Button(分享).onClick(() {this.shareGIF(this.uri);});}.width(100%).padding(16);}}}## 六、性能优化建议### 6.1 图片预处理在合成前压缩图片减少内存占用typescriptlet decodingOptions: image.DecodingOptions {editable: true,desiredPixelFormat: 3,size: {width: Math.min(originalWidth, 800), // 最大 800pxheight: Math.min(originalHeight, 800)}};### 6.2 异步处理不要在主线程处理大量图片使用异步typescript// 使用 Promise.all 并行处理let pixelMapList await Promise.all(uris.map(async (uri) {return await this.decodeToPixelMap(uri);}));### 6.3 缓存管理定期清理缓存目录typescriptaboutToAppear() {let context this.getUIContext().getHostContext() as common.UIAbilityContext;let path: string context.cacheDir;// 清理旧的 GIF 文件fs.rmdir(path);fs.mkdir(path);}## 七、写在最后一些心里话通过这个项目我们确实学到了不少东西说实话收获挺大的- ImagePacker 的用法特别是 GIF 编码参数的配置这个真的很强大- 如何用 PixelMap 处理图片数据这个是核心- 文件操作的技巧打开、读取、关闭这个很实用- 性能优化的方法压缩、异步、缓存这些细节很重要- 用户体验的重要性加载提示、错误处理这些都不能少说实话做这个项目之前我们对 ImagePacker 也就是一知半解官方文档看了好几遍还是云里雾里的。做完之后才算真正搞懂了。所以啊有时候接到需求别怕硬着头皮上做着做着就会了。这个是真的。其实一开始我们对 GIF 合成也不太熟悉官方文档看了好几遍还是云里雾里真的是啥都不懂。后来干脆自己动手试踩了无数坑才慢慢摸索出这套方案。给大家提个醒官方文档虽然详细但有时候不够直观遇到问题还是要自己动手试多踩坑才能多积累经验。比如我们一开始忘了关闭文件结果生成的 GIF 总是报错查了半天才发现急死人了后来又忘了加版本检查结果在老设备上崩溃当时就把我们吓出一身冷汗还有图片太大导致内存溢出后来加了压缩才解决。最要命的是错误处理一开始没加 try-catch出错了都不知道咋回事后来加了才知道具体是哪里的问题。说实话这些问题一个个冒出来的时候真的有点崩溃。但解决完之后就特别有成就感。这些功能我们也在考虑但想着先把基础的做好再慢慢加。毕竟一口吃不成胖子对吧再说了基础打好了加这些功能也简单。如果你也在做鸿蒙应用刚好也需要 GIF 合成功能希望这个示例代码能帮到你。如果有什么问题或者你有更好的实现方式欢迎在评论区交流我们也在不断学习和改进希望能做出更好的示例给大家参考。说真的看到大家用我们的代码做出好玩的功能我们比谁都开心。对了差点忘了说GIF 合成这个功能在 API 18 及以上版本才支持模拟器上可能会有点卡。我们一开始就是在模拟器上测的结果发现性能问题当时还以为是代码写得不好后来上真机才发现其实还好模拟器的问题。所以大家尽量用真机测试能少走不少弯路。这话说了好几遍了但真的很重要。