HarmonyOS 图片列表滑动卡顿怎么办:中式美食菜谱封面怎么做占位、缓存和失败兜底

HarmonyOS 图片列表滑动卡顿怎么办:中式美食菜谱封面怎么做占位、缓存和失败兜底


中式美食的菜谱列表里,封面图不是装饰,它会直接影响用户愿不愿意继续往下滑。最常见的问题不是图片完全加载不出来,而是列表先出现一块空白,过一会儿图片突然顶出来,卡片高度跟着变;再差一点就是网络图失败以后,用一张尺寸不稳定的兜底图把整行挤乱。这个体验看起来像页面卡,实际上很多时候是图片占位、缓存 key 和失败状态没有管住。
我会把这件事拆成四个动作:卡片先固定高度,封面先走缓存,失败图保持同样尺寸,滚动过程中只刷新必要状态。这样写出来的列表没有那么"炫",但用户滑起来会稳,开发者排查起来也有明确入口。

本文导读

问题 处理方式
图片没回来时卡片高度跳 先给封面区域固定比例和占位背景
同一张封面重复解码 用 recipeId + coverUrl 生成稳定缓存 key
图片失败后列表变形 失败图复用同一尺寸,不让布局重新计算
滚动时状态反复刷新 图片状态放到卡片内部,列表数据不跟着重排

环境和文件边界

项目 内容
开发工具 DevEco Studio
HarmonyOS SDK API 12,Stage 模型
页面入口 entry/src/main/ets/features/home/RecipeListPage.ets
卡片组件 entry/src/main/ets/components/RecipeCard.ets
图片状态 entry/src/main/ets/components/CachedRecipeCover.ets
数据来源 entry/src/main/ets/data/repository/RecipeRepository.ets

先说一个错误写法:图片回来再撑开卡片

ts 复制代码
@Component
export struct RecipeCard {
  @Prop recipe: RecipeCardModel

  build() {
    Column() {
      Image(this.recipe.coverUrl)
        .width('100%')
        .objectFit(ImageFit.Cover)

      Text(this.recipe.title)
        .fontSize(16)
        .fontWeight(FontWeight.Medium)
    }
  }
}

这段代码看着很自然,但列表里会出问题。图片没加载回来时,封面区域没有稳定高度;图片一回来,卡片突然变高,下面的内容就会被顶一下。用户看到的不是"图片加载成功",而是"列表抖了一下"。

封面区域先固定住

ts 复制代码
@Component
export struct RecipeCard {
  @Prop recipe: RecipeCardModel

  build() {
    Column() {
      CachedRecipeCover({
        recipeId: this.recipe.id,
        coverUrl: this.recipe.coverUrl,
        title: this.recipe.title
      })
        .width('100%')
        .aspectRatio(1.32)

      Text(this.recipe.title)
        .fontSize(16)
        .fontWeight(FontWeight.Medium)
        .margin({ top: 8 })
    }
    .borderRadius(12)
    .backgroundColor('#FFFFFF')
    .clip(true)
  }
}

我这里不让 Image 自己决定高度,而是把封面区域交给 CachedRecipeCover,外层先定好宽高比例。图片回来之前、回来之后、失败以后,这块区域都不变。列表稳定性的第一步就是:不要让图片加载结果重新决定卡片高度。

CachedRecipeCover 只处理图片状态

ts 复制代码
type CoverState = 'loading' | 'success' | 'failed'

@Component
export struct CachedRecipeCover {
  @Prop recipeId: string
  @Prop coverUrl: string
  @Prop title: string
  @State state: CoverState = 'loading'

  build() {
    Stack() {
      if (this.state === 'loading') {
        CoverPlaceholder({ title: this.title })
      }

      Image(this.coverUrl)
        .width('100%')
        .height('100%')
        .objectFit(ImageFit.Cover)
        .onComplete(() => {
          this.state = 'success'
        })
        .onError(() => {
          this.state = 'failed'
        })

      if (this.state === 'failed') {
        CoverFallback({ title: this.title })
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F7F1E8')
  }
}

图片状态不要塞回整个列表的 ViewModel。列表只关心有哪些菜谱,卡片内部才关心封面是加载中、成功还是失败。这样做的好处很直接:某一张图片失败了,只重绘那张卡片的封面区域,不会把整条列表数据再刷新一遍。

缓存 key 不要只用文件名

ts 复制代码
export class RecipeCoverCache {
  private memory: Map<string, string> = new Map()

  buildKey(recipeId: string, coverUrl: string): string {
    const urlPart = coverUrl.trim()
    return `${recipeId}:${urlPart}`
  }

  get(recipeId: string, coverUrl: string): string | undefined {
    return this.memory.get(this.buildKey(recipeId, coverUrl))
  }

  put(recipeId: string, coverUrl: string, localPath: string): void {
    this.memory.set(this.buildKey(recipeId, coverUrl), localPath)
  }
}

这里不要偷懒只取图片文件名。很多菜谱图可能来自同一个默认图、同一个 CDN 路径规则,文件名撞了以后就会出现"鱼香肉丝显示成红烧肉封面"的问题。中式美食这里我更愿意用 recipeId + coverUrl,哪怕 key 长一点,也比错图强。

有缓存就先用缓存

ts 复制代码
export class RecipeCoverResolver {
  constructor(private cache: RecipeCoverCache) {}

  resolve(recipe: RecipeCardModel): string {
    const cached = this.cache.get(recipe.id, recipe.coverUrl)
    if (cached) {
      return cached
    }

    return recipe.coverUrl
  }
}

缓存不是为了让代码看起来高级,而是为了减少列表滚动时重复下载、重复解码。用户上下滑动时,同一批菜谱会反复进入可视区域。如果每次都重新走网络图,列表就容易抖,也更容易掉帧。

失败兜底也要固定尺寸

ts 复制代码
@Component
export struct CoverFallback {
  @Prop title: string

  build() {
    Column() {
      Text('封面暂时不可用')
        .fontSize(13)
        .fontColor('#8A5A3B')

      Text(this.title)
        .fontSize(15)
        .fontWeight(FontWeight.Medium)
        .fontColor('#2F2A24')
        .maxLines(2)
        .textOverflow({ overflow: TextOverflow.Ellipsis })
        .margin({ top: 6 })
    }
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
    .width('100%')
    .height('100%')
    .padding(12)
    .backgroundColor('#F7E7D6')
  }
}

失败图不要另起一个高矮不一样的区域。它应该和真实封面占同一块位置,只是内容换成兜底提示。这样就算某张图挂了,也不会把瀑布流或列表排版带乱。

列表层只做数据分发

ts 复制代码
@Component
export struct RecipeListPage {
  @State recipes: RecipeCardModel[] = []

  build() {
    List({ space: 12 }) {
      ForEach(this.recipes, (item: RecipeCardModel) => {
        ListItem() {
          RecipeCard({ recipe: item })
        }
      }, (item: RecipeCardModel) => item.id)
    }
    .cachedCount(8)
  }
}

列表层最容易犯的错,是把图片加载状态也放进 recipes 里。这样一张图状态变化,整个列表数据都像被改了一次。我的习惯是:列表数据保持干净,图片状态下沉到卡片组件;ForEach 的 key 用菜谱 id,避免滚动复用时错位。

验收记录

步骤 操作 预期
1 首次打开菜谱列表 图片出来前卡片高度不跳
2 快速向下滑动再滑回来 已经出现过的封面尽量直接展示
3 断网或让某张封面地址失败 失败兜底不撑乱列表
4 更新某个菜谱标题 只刷新对应卡片,不影响其他封面
5 切换分类后回到首页 封面缓存 key 不串图

排查顺序

顺序 先看什么 原因
1 封面区域有没有固定比例 没有固定高度就一定会跳
2 ForEach key 是否稳定 key 不稳定会导致卡片复用错位
3 失败兜底是否同尺寸 兜底图变高会直接挤乱列表
4 图片状态是否污染列表数据 状态上浮会让整页跟着刷新

边界和风险

场景 处理方式
本地缓存路径失效 回退到远程 coverUrl,不直接显示坏路径
封面比例差异很大 统一 ImageFit.Cover,让卡片稳定优先
网络图加载很慢 保持占位,不让用户看到空白抖动
低端设备滑动掉帧 减少整页状态刷新,优先观察图片解码次数

小结

图片列表卡顿,很多时候不是某一行 Image 写错了,而是几个小问题叠在一起:没有占位、缓存 key 不稳、失败图尺寸不一致、图片状态又反过来刷新整个列表。中式美食的菜谱列表我会先把封面区域固定住,再让缓存和失败兜底都服从这个尺寸,最后把图片状态留在卡片内部。
这样做的结果很朴素:用户滑动时列表不跳,失败时不乱,开发者排查时也知道先看高度、key、兜底图和状态边界。封面图做好了,菜谱列表才像一个稳定的产品页面,而不是一堆图片慢慢补出来的临时界面。

相关推荐
心中有国也有家2 小时前
AtomGit Flutter 鸿蒙客户端:零外部资源的应用打包策略
android·javascript·flutter·华为·harmonyos
最爱老式锅包肉2 小时前
HarmonyOS技术精讲-Camera Kit(相机服务)第3篇:实现相机预览功能
数码相机·华为·harmonyos
youtootech3 小时前
HarmonyOS《柚兔学伴》项目实战02-多模块架构设计与 HSP/HAR 模块化
华为·harmonyos
不才难以繁此生3 小时前
ArkUI Tabs 切换后状态丢失怎么办:中式美食分类页怎么保留关键词、筛选和滚动位置
harmonyos·状态管理·arkui·tabs·中式美食
爱吃大芒果16 小时前
复刻 HarmonyKit 工具箱:基于新版 @kit 开发本地工具 App
华为·harmonyos
风华圆舞17 小时前
鸿蒙上跑通第一个 AI Agent:5 分钟接入智谱/DeepSeek
人工智能·华为·harmonyos·arkts·arkui·toolregistry
xd18557855518 小时前
电影匹配引擎-基于鸿蒙的心情电影推荐应用开发实践
人工智能·安全·华为·harmonyos·鸿蒙
千逐6819 小时前
HarmonyOS 实战 | @State 不神秘——变量一变界面就跟着变
华为·harmonyos·鸿蒙
不才难以繁此生19 小时前
NavPathStack 返回错页怎么排查:中式美食详情页从搜索、收藏和推荐进入怎么回到原处
harmonyos·arkts·arkui·中式美食·navpathstack