

一键开通华为云码道 CodeArts 代码智能体:进入活动体验页面
仓库地址:lwcwam/guoqing-avatar-harmonyos
头像框看起来是个很小的应用,但真正做起来,每一处都绕不开取舍:照片怎么选、边框怎么叠、导出为什么会变黑、权限什么时候申请,以及一张透明 PNG 怎样才能真的贴合头像四边。
这个项目一开始并不复杂:从系统相册选一张照片,叠加节日边框,保存为新的头像。但第一版跑起来以后,我很快发现“功能能用”和“用户愿意用”之间差得很远。默认边框像简单图形拼接,红旗和五角星的比例不自然,页面也缺少完整的作品感。
所以后面的方向发生了变化。我不再用零散的矢量元素临时组框,而是为应用准备四套高精度透明 PNG 成品边框:山河、灯影、锦绣和星旗。头像是可变内容,节日装饰是固定美术资源,两者在界面中分层叠加。这样既能保证视觉质量,也让新增样式变成“增加资源与元数据”,不用重写合成逻辑。

四套设计的差别不只是名字。山河以红绸、长城、华表、飞鹤和金色山水构成较庄重的画面;灯影强调宫灯、烟花和古建灯火;锦绣用牡丹、珐琅青绿如意云、珍珠和金丝花枝;星旗则把五角星组、立体红绸与金色流光放在视觉中心。



边框素材本身留有透明中心,应用只需要负责把照片裁成正方形并铺满底层,再把边框放到上层。为了消除透明素材边缘的留白,我最后把边框放大到 108%,同时让外层 Stack 开启裁切。多出来的部分被裁掉,彩色装饰就能一直延伸到作品边缘,不再像悬浮在头像上。
核心组件 AvatarCanvas 使用双层 Stack。底层显示用户从系统 PhotoPicker 选择的照片,采用 Cover 模式铺满正方形;顶层显示当前边框资源,采用 Fill 模式并外扩。Stack 设置固定的 avatarCanvas 标识,后续导出就以它为快照边界。
Stack() {
Image(this.avatarUri)
.width('100%')
.aspectRatio(1)
.objectFit(ImageFit.Cover);
Image(this.frameRes())
.width('100%')
.aspectRatio(1)
.objectFit(ImageFit.Fill)
.scale({ x: this.frameScale, y: this.frameScale });
}
.id('avatarCanvas')
.width('100%')
.aspectRatio(1)
.clip(true);把它想成两张透明胶片:下面一张是用户照片,用 Cover 铺满正方形;上面一张是中间透明的节日边框,再轻微放大,让彩色装饰越过作品四边。最后用 clip(true) 把超出的部分裁齐,看到的就是一张边缘贴合的完整头像,而不是两张松散地摆在一起的图片。
这套结构带来的好处很直接:切换边框不会重新处理照片;新增边框不需要复制一套绘制代码;导出时只捕获头像和边框,不会把标题、按钮、样式选择器一起截进去。首屏使用明确标注的示例头像,示例状态下保存按钮不可用,分享入口也不会出现,避免用户把演示素材误当成自己的成品。
选图使用系统 PhotoPicker。用户主动选中的照片会获得临时访问授权,应用不需要在启动时索取整个相册的读取权限。照片从载入、裁切、叠框到合成都在本机完成,没有上传服务。
只有用户点击保存或分享时,应用才申请写入系统图库所需权限。权限被拒绝后,界面会恢复到可操作状态,而不是一直停留在“保存中”。这并不是多高级的技术,但它决定了一个小工具是否尊重用户:做头像框,不等于获得浏览全部照片的理由。
项目实际运行时遇到过一个典型问题:照片已经成功导入,页面上也能看到边框,但导出后得到的却是黑白图,甚至没有完整叠加效果。问题不在边框图片,而在渲染与快照链路。
最终实现中,照片 URI 直接交给原生 Image 图层显示,避免模拟器中 PixelMap 渲染路径的不稳定;导出则调用 getComponentSnapshot().get('avatarCanvas', { scale: 1, waitUntilRenderFinished: true }),等待整个 Stack 渲染完成,再取得包含“照片 + 边框”的 PixelMap。随后编码为 JPEG、创建图库资产、写入文件并及时释放资源。
const pixelMap: image.PixelMap = await this.snapshotCanvas();
try {
await SaveHelper.saveToAlbum(context, pixelMap);
promptAction.showToast({ message: '头像已保存到相册' });
} finally {
await pixelMap.release();
}
private snapshotCanvas(): Promise<image.PixelMap> {
return this.getUIContext().getComponentSnapshot().get(
'avatarCanvas',
{ scale: 1, waitUntilRenderFinished: true }
);
}预览能看到,为什么还要“等”?界面渲染像一桌菜陆续上齐:照片可能先出现,边框随后才加载。如果过早拍照,快照里就可能缺一层。这里明确等待 avatarCanvas 完成渲染,再把同一个 PixelMap 写入相册;无论成功还是失败,finally 都会释放它,避免一次次导出后留下不再使用的图像内存。
这次排查让我确认了一件事:界面截图不是导出功能的证明。只有真的选图、合成、保存,再去系统图库查看结果,才算跑通了完整闭环。
功能完成后,我把 AtomGit 仓库关联到 AtomCode 工作台,新建了“参赛作品|国庆头像框 HarmonyOS 实战分析”会话,让华为云码道 CodeArts Agent 完整阅读源码。我给它的重点不是“帮我夸一下项目”,而是要求它还原五条真实调用链:系统相册选图、照片与透明边框叠加、边框外扩、组件快照导出、本地隐私保护。

CodeArts Agent 从仓库中定位了 FrameStyle.ets、AvatarCanvas.ets、Index.ets、AvatarImage.ets 和 SaveHelper.ets,把原本分散在页面、组件与工具类中的逻辑串成了可以逐项核对的流程。它也明确指出,快照边界是带有 avatarCanvas 标识的 Stack,而不是整张页面。

这种使用方式比单纯让智能体生成代码更适合项目收尾。它像一次针对真实仓库的代码走查:我可以顺着分析结果回到源码,确认每一步是否真的存在,也能把开发过程中最难解释的部分整理成参赛文章里的技术主线。
assembleHap,均构建成功;unsigned 调试包大小 7,272,818 字节。构建仍会提示部分 HarmonyOS API 已废弃,以及若干可能抛出异常的接口需要进一步收紧处理。这些警告没有被隐藏,它们会作为下一轮迁移和兼容性优化的清单。
国庆头像框不是一个复杂系统,但它把很多容易被忽略的问题集中在了一起:透明素材有没有真正贴边,照片选取是否克制权限,示例素材能不能误导用户,组件快照是否包含正确图层,PixelMap 和文件描述符有没有及时释放。
做完这一轮以后,我更愿意把它称为“头像工坊”,而不是一张页面加四张图。用户看到的是选择照片、切换风格、保存头像三个动作;背后则是一条从系统选图到本地合成、从组件快照到图库写入的完整链路。小工具也值得把这些事情认真做完。
项目已在 AtomGit 开源:https://atomgit.com/lwcwam/guoqing-avatar-harmonyos
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。