HarmonyOS掌上记账APP开发实践第25篇:Image 组件高级用法 — 资源引用、网络图片与首字符占位方案

Image 组件高级用法 --- 资源引用、网络图片与首字符占位方案

一、引言

在移动应用中,图片是最常见的视觉元素之一。鸿蒙系统的 Image 组件提供了丰富的图片加载能力,包括本地资源引用、网络图片加载和占位图展示,同时还支持跨设备适配和缓存策略。在记账应用中,头像展示、空状态占位、图标渲染等场景都需要合理运用 Image 组件。MoneyTrack 结合条件渲染实现了用户头像的灵活展示------有头像时显示图片,无头像时展示首字符占位,同时还为空状态提供了友好的视觉提示。

二、核心知识点

2.1 $r 资源引用与限定符机制

$r 是鸿蒙系统中引用资源文件的标准语法,支持引用 media、图片、字符串等资源类型。与硬编码路径相比,$r 引用方式支持资源限定符机制,系统会根据设备的屏幕密度(DPI)、语言、横竖屏等条件自动匹配最合适的资源版本。

typescript 复制代码
Image($r('app.media.ic_empty_asset_bill'))
  .width(120).height(120)

资源目录组织示例:

  • drawable-mdpi/ic_launcher.png --- 160 DPI 基准密度
  • drawable-ldpi/ic_launcher.png --- 120 DPI 低密度
  • drawable-hdpi/ic_launcher.png --- 240 DPI 高密度
  • drawable-xhdpi/ic_launcher.png --- 320 DPI 超高密度

系统会自动根据设备屏幕密度选择对应资源,无需在代码中手动判断。

2.2 Image 核心属性详解

除基础的 width / height 外,Image 组件还提供了以下重要属性:

  • objectFit :控制图片在容器内的缩放方式。ImageFit.Cover 裁剪填充,ImageFit.Contain 完整显示,ImageFit.Fill 拉伸填充,ImageFit.None 保持原尺寸。
  • matchTextDirection:布尔值,是否跟随系统文字方向进行镜像翻转,在 RTL(从右到左)语言环境下很有用。
  • autoResize:布尔值,是否自动调整图片尺寸以节省内存,建议在列表场景开启。
typescript 复制代码
Image(this.imageUrl)
  .width('100%').height(200)
  .objectFit(ImageFit.Cover)     // 裁剪适配,保证填满容器
  .matchTextDirection(true)      // 跟随文字方向
  .autoResize(true)              // 自动调整尺寸优化内存

2.3 Image 网络图源与缓存策略

Image 组件通过字符串 URL 加载网络图片,支持 HTTP/HTTPS 协议。加载网络图片时需要配置 ohos.permission.INTERNET 权限。Image 组件内置了图片缓存 能力,默认对已加载的图片进行内存和磁盘缓存,避免重复网络请求。对于关键页面中的图片,可以使用 Image 的预加载机制提前加载到缓存中:

typescript 复制代码
// 在页面初始化时预加载关键图片
Image.preload([
  'https://example.com/hero_banner.jpg',
  'https://example.com/avatar_default.png'
])

缓存策略要点:

  1. 内存缓存:默认开启,LRU 策略淘汰,减少 GC 压力
  2. 磁盘缓存:持久化缓存,应用重启后仍可命中
  3. 预加载:适合首页、引导页等关键资源的提前加载
  4. 无缓存模式:通过设置 HTTP 响应头 Cache-Control: no-cache 控制

2.4 图片加载失败的兜底处理

Image 组件的 onError 回调可以捕获加载失败事件,配合条件渲染实现兜底方案:

typescript 复制代码
Image(this.memberAvatar)
  .width(40).height(40)
  .borderRadius(20)
  .objectFit(ImageFit.Cover)
  .onError(() => {
    this.imageLoadFailed = true   // 加载失败时切换到占位方案
  })

更完整的方案是将 Image 与占位 Text 封装为复合组件,加载失败自动回退到首字符占位。

2.5 Image 加载生命周期

#mermaid-svg-8526vHTlGzatYEjk{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-8526vHTlGzatYEjk .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-8526vHTlGzatYEjk .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-8526vHTlGzatYEjk .error-icon{fill:#552222;}#mermaid-svg-8526vHTlGzatYEjk .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-8526vHTlGzatYEjk .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-8526vHTlGzatYEjk .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-8526vHTlGzatYEjk .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-8526vHTlGzatYEjk .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-8526vHTlGzatYEjk .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-8526vHTlGzatYEjk .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-8526vHTlGzatYEjk .marker{fill:#333333;stroke:#333333;}#mermaid-svg-8526vHTlGzatYEjk .marker.cross{stroke:#333333;}#mermaid-svg-8526vHTlGzatYEjk svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-8526vHTlGzatYEjk p{margin:0;}#mermaid-svg-8526vHTlGzatYEjk .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-8526vHTlGzatYEjk .cluster-label text{fill:#333;}#mermaid-svg-8526vHTlGzatYEjk .cluster-label span{color:#333;}#mermaid-svg-8526vHTlGzatYEjk .cluster-label span p{background-color:transparent;}#mermaid-svg-8526vHTlGzatYEjk .label text,#mermaid-svg-8526vHTlGzatYEjk span{fill:#333;color:#333;}#mermaid-svg-8526vHTlGzatYEjk .node rect,#mermaid-svg-8526vHTlGzatYEjk .node circle,#mermaid-svg-8526vHTlGzatYEjk .node ellipse,#mermaid-svg-8526vHTlGzatYEjk .node polygon,#mermaid-svg-8526vHTlGzatYEjk .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-8526vHTlGzatYEjk .rough-node .label text,#mermaid-svg-8526vHTlGzatYEjk .node .label text,#mermaid-svg-8526vHTlGzatYEjk .image-shape .label,#mermaid-svg-8526vHTlGzatYEjk .icon-shape .label{text-anchor:middle;}#mermaid-svg-8526vHTlGzatYEjk .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-8526vHTlGzatYEjk .rough-node .label,#mermaid-svg-8526vHTlGzatYEjk .node .label,#mermaid-svg-8526vHTlGzatYEjk .image-shape .label,#mermaid-svg-8526vHTlGzatYEjk .icon-shape .label{text-align:center;}#mermaid-svg-8526vHTlGzatYEjk .node.clickable{cursor:pointer;}#mermaid-svg-8526vHTlGzatYEjk .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-8526vHTlGzatYEjk .arrowheadPath{fill:#333333;}#mermaid-svg-8526vHTlGzatYEjk .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-8526vHTlGzatYEjk .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-8526vHTlGzatYEjk .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-8526vHTlGzatYEjk .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-8526vHTlGzatYEjk .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-8526vHTlGzatYEjk .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-8526vHTlGzatYEjk .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-8526vHTlGzatYEjk .cluster text{fill:#333;}#mermaid-svg-8526vHTlGzatYEjk .cluster span{color:#333;}#mermaid-svg-8526vHTlGzatYEjk div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-8526vHTlGzatYEjk .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-8526vHTlGzatYEjk rect.text{fill:none;stroke-width:0;}#mermaid-svg-8526vHTlGzatYEjk .icon-shape,#mermaid-svg-8526vHTlGzatYEjk .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-8526vHTlGzatYEjk .icon-shape p,#mermaid-svg-8526vHTlGzatYEjk .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-8526vHTlGzatYEjk .icon-shape .label rect,#mermaid-svg-8526vHTlGzatYEjk .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-8526vHTlGzatYEjk .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-8526vHTlGzatYEjk .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-8526vHTlGzatYEjk :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 本地资源 $r
网络 URL




开始加载图片
图片来源
读取资源包
检查缓存
缓存命中?
直接展示
发起网络请求
请求成功?
写入缓存 & 展示
触发 onError 回调
条件渲染兜底方案
解析资源
根据限定符选配
显示占位图或首字符

三、项目代码案例

3.1 BillDetailPage 头像条件渲染

products/entry/src/main/ets/views/bill/BillDetailPage.ets 中,用户头像展示采用条件渲染策略:

typescript 复制代码
if (this.memberAvatar && !this.imageLoadFailed) {
  Image(this.memberAvatar)
    .width(40).height(40)
    .borderRadius(20)
    .objectFit(ImageFit.Cover)
    .onError(() => { this.imageLoadFailed = true })
} else {
  Circle() {
    Text(this.memberName.charAt(0))
      .fontSize(16)
      .fontColor(Color.White)
  }
  .width(40).height(40)
  .fill('#1890FF')
}
  • memberAvatar 有值时,加载用户头像图片,并监听 onError 回调
  • 加载失败时切换到 Circle + Text 首字符占位
  • 圆形背景采用品牌蓝色 #1890FF,文字白色居中显示,字体 16fp
  • 占位组件宽度高度均设为 40vp,与图片尺寸一致,确保布局稳定

3.2 空状态占位

在账单和资产列表为空时,通过 Image 展示友好的空状态提示图:

typescript 复制代码
Column() {
  Image($r('app.media.ic_empty_asset_bill'))
    .width(120).height(120)
    .objectFit(ImageFit.Contain)  // 完整显示图片内容
  Text('暂无账单记录')
    .fontSize(14)
    .fontColor('#999')
    .margin({ top: 12 })
}
.justifyContent(FlexAlign.Center)
.height('100%')

3.3 @Extend 图片样式封装

对于高频使用的图片样式,可以通过 @Extend 封装为自定义样式函数,实现代码复用:

typescript 复制代码
@Extend(Image) function avatarStyle() {
  .width(40).height(40)
  .borderRadius(20)
  .objectFit(ImageFit.Cover)
}

四、最佳实践

实践要点 说明
资源引用规范 优先使用 $r 引用而非硬编码路径,利用限定符机制适配多设备
图片大小控制 列表中的图片尽量控制在 40-120vp,大图使用 objectFit 适配
缓存策略 关键图片预加载,非关键图片按需加载;列表页开启 autoResize
加载失败处理 始终监听 onError 回调,提供占位兜底 UI,避免裂图
首字符占位 圆形 + 首字符是最轻量的占位方案,适合头像场景

五、总结

Image 组件是鸿蒙 ArkUI 中最基础也最灵活的组件之一。本文从 $r 资源引用出发,介绍了限定符机制如何实现多设备自适应,深入讲解了 objectFitmatchTextDirectionautoResize 等属性的使用场景。针对网络图片场景,重点阐述了内置缓存策略和 preload 预加载机制。同时,通过 onError 回调实现了图片加载失败的优雅降级,结合首字符占位方案确保了用户在任何情况下都能获得一致的视觉体验。在 MoneyTrack 的实际项目中,这些技术共同构成了一个健壮、高性能的图片加载体系。

六、参考文档

相关推荐
mounter6253 小时前
从内存隔离到运行时防线:透视 Linux 内核安全强化的博弈与演进
linux·网络·安全·linux kernel·kernel
2501_919749033 小时前
华为鸿蒙免费记账与统计APP—小羊统计
华为·harmonyos
AD02273 小时前
32-Token明文存储-把凭据边界收到安全仓储里
harmonyos·arkts·鸿蒙开发
特立独行的猫a4 小时前
Node.js三方库鸿蒙 PC 移植:鸿蒙PC开发者面对面 · 线下课件分享
华为·harmonyos·三方库移植·鸿蒙pc
Bug退散师5 小时前
多路IO复用[select版TCP服务器与poll版TCP服务器]与常用网络编程函数详解
linux·服务器·c语言·网络·驱动开发·tcp/ip
春卷同学5 小时前
HarmonyOS掌上记账APP开发实践第23篇:弹性动效与手势 — EdgeEffect.Spring 与手势系统实战
spring·华为·harmonyos
l134062082356 小时前
鸿蒙应用开发实战【88】— DevEco Studio高效开发技巧
华为·harmonyos·鸿蒙系统
Acrellea6 小时前
固态变压器(SST)热管理破局:安科瑞交直流专属测温方案护航无人值守运维
运维·网络
振浩微433射频芯片6 小时前
5个IO控制20个LED?查理复用实战拆解
网络·单片机·物联网·学习·智能家居