ArkUI 视觉资源管理:ShowcaseCard、CardImages 与样式页图片映射
HarmonyOS 应用做卡片类界面时,视觉资源很容易失控:有些页面用纯色块,有些页面用本地图片,有些页面直接写资源名,最后新增模板或样式时到处改。这个项目把视觉资源拆成两层:组件层ShowcaseCard负责渲染,资源层CardImages.ets负责 key 到 media 的映射。
为什么不用页面直接引用图片
页面直接写:
Image($r('app.media.card_template_birthday'))短期最快,但问题很快出现:
- 同一模板在首页、分类、详情页都要重复引用。
- 新增模板要改多个页面。
- 有些场景需要按分类 fallback。
- 样式页顶部 banner 和网格 tile 比例不同。
项目改成让视图模型携带imageKey:
export interface ShowcaseCardModel { id: string; title: string; subtitle: string; tone: ToneName; imageKey?: string; }页面只传模型,组件自己解析图片。
CardImages:集中管理资源映射
CardImages.ets里定义 key:
export class CardImageKeys { static readonly heroDark: string = 'hero-dark'; static readonly marketLight: string = 'market-light'; static readonly categoryCountdown: string = 'category-countdown'; static readonly templateBirthday: string = 'template-birthday'; static readonly styleNightBanner: string = 'style-night-banner'; static readonly styleNightTile: string = 'style-night-tile'; }再通过cardImageResource()转成真正的 media 资源:
export function cardImageResource(imageKey?: string): Resource { switch (imageKey) { case CardImageKeys.categoryCountdown: return $r('app.media.card_category_countdown'); case 'template-birthday': return $r('app.media.card_template_birthday'); case CardImageKeys.styleNightBanner: return $r('app.media.card_style_night_banner'); default: return $r('app.media.market_banner_light'); } }页面和服务层都不直接依赖$r('app.media.xxx')。
模板图片:templateId 到 imageKey
模板目录只保存templateId和categoryId。图片 key 由 helper 生成:
export function imageKeyForTemplate(templateId: string, categoryId: CardCategoryId): string { switch (templateId) { case 'birthday': return 'template-birthday'; case 'exam-countdown': return 'template-exam-countdown'; default: return imageKeyForCategory(categoryId); } }如果模板图片缺失,就回退到分类图。这个 fallback 能兜底,但新增模板时仍应该补齐专属图。
样式页:同一个 styleId 要区分 banner 和 tile
样式页比较特殊:顶部预览卡需要横向 banner,下方样式库需要 tile。项目用preview参数区分:
export function imageKeyForStyle(styleId: string, preview: boolean = false): string { switch (styleId) { case 'style-night': return preview ? CardImageKeys.styleNightBanner : CardImageKeys.styleNightTile; case 'style-rose': return preview ? CardImageKeys.styleRoseBanner : CardImageKeys.styleRoseTile; default: return preview ? CardImageKeys.styleRoseBanner : CardImageKeys.styleRoseTile; } }这个设计避免“顶部换了图,下方还是旧图”的视觉割裂。
ShowcaseCard:一套组件支持多种展示
ShowcaseCard.ets支持普通卡、hero 卡、banner 图片卡和自定义图片高度:
@Component export struct ShowcaseCard { @Prop item: ShowcaseCardModel; compactBadge?: boolean; hero?: boolean; bannerImage?: boolean; imageHeight?: number; onCardClick?: (id: string) => void; }图片是否展示由 helper 决定:
private showImage(): boolean { return this.hero === true || this.bannerImage === true || this.imageHeight !== undefined; }图片高度也根据模式变化:
private mediaHeight(): number { if (this.hero === true) { return 86; } if (this.bannerImage === true) { return 128; } return this.imageHeight ? this.imageHeight : 0; }这样详情页、样式页、分类页不用各写一套卡片组件。
紧凑徽标布局解决窄卡片文本被挤压
首页和分类页有两列窄卡片。如果沿用左右对称 badge 占位,标题容易被挤压。项目给ShowcaseCard增加compactBadge:
private useCompactBadgeLayout(): boolean { return this.compactBadge === true && this.item.badge ? true : false; }页面使用时:
ShowcaseCard({ item: item, compactBadge: true, onCardClick: () => { this.openShowcaseCard(item); } })窄卡片优先保证标题和副标题可读,badge 收到右上角。
资源更新时的验证重点
视觉资源变化不一定影响编译,但很容易影响运行效果。项目里形成了几个检查点:
- 新增模板时,检查
TEMPLATE_CATALOG和CardImages.ets是否同步。 - 样式页同一
styleId同时检查 preview/banner 和 tile。 - 图片中的主要文字不要烧录在边缘,避免
ImageFit.Cover裁切。 - 小卡片使用
imageHeight或固定比例,避免布局跳动。 - 分类图和模板图都要在真机或模拟器截图中检查。
常见坑
- 只补 media 文件,不补
cardImageResource()映射。 - 只给样式页顶部预览换图,忘了下方 tile。
- 模板 ID 改了,但图片 key 仍然旧值。
- 详情页使用模板图时没有 fallback 到分类图。
- 图片高度靠内容撑开,导致不同卡片高度不一致。
基础链路小结
这个项目的视觉资源管理思路是:页面传imageKey,组件统一渲染,资源层集中映射。模板图片、分类图片、样式图片都走同一套 key 体系。
对 ArkUI 卡片类应用来说,这种方式能让资源替换、模板扩展和页面复用都更可控。尤其是样式页这类同时存在 banner 和 tile 的场景,一定要把同一个业务 ID 的多套图片映射维护清楚。
图片资源章节要讲“资源 key、业务 id、兜底图”三件事
图片章节如果只讲“把 PNG 放进 media 目录”,就达不到第四章标准。Project028 的关键是CardImages.ets把业务 id、样式 id、图片 key 和资源对象隔离开了。页面拿到的通常是imageKey,最终通过cardImageResource()解析成$r('app.media.xxx')。这种间接层可以让页面不关心资源文件名,也能处理缺图兜底。
主题、样式、详情头图、市场头图都走同一套资源入口。比如桌面 Form 里保存的是themeImageKey,详情页里根据卡片类型解析头图,市场页摘要卡没有图片时回退marketLight。这些都说明图片资源不能散落在各页面,否则后续替换图片或生成新封面会非常难查。
Image(cardImageResource(this.themeImageKey)) .width('100%') .height('100%') Image(cardImageResource(this.heroImageKey())) .width('100%') .height('100%')真实项目里还要考虑审核截图。Project028 后续为了 AGC 宣传图、应用图标、桌面卡片预览生成了多批图片资产。这里需要把边界讲清楚:CSDN 展示图、AGC 宣传图、应用内资源图不要混用。CSDN 图偏讲解,AGC 图偏审核和商店展示,应用内图要控制尺寸、命名和资源引用稳定性。
落地检查清单
- 是否说明图片资源不要从页面硬编码
$r()。 - 是否解释
imageKey和业务 id 的区别。 - 是否覆盖缺图兜底,避免空白区域。
- 是否提示 AGC 宣传图和应用内资源图的用途不同。
- 是否覆盖真实路径:
CardImages.ets、ThemeStorePage.ets、CardStylePage.ets、DesktopCardForm.ets。