鸿蒙应用开发:图片渐进式加载Canvas渲染案例分享

一、前言

你在浏览网页时,一定见过这种效果: 一张高清大图,不是瞬间显示,而是从上到下、一行一行慢慢"刷"出来。

这种效果叫渐进式图片加载,既能提升视觉体验,又能大幅降低内存占用。今天这篇文章,带你用实现一模一样的效果!

实现亮点:

  • 图片从上到下逐行渲染
  • 等比缩放,不拉伸、不变形
  • 图片自动屏幕居中显示
  • 内存占用极低,高清大图不OOM

二、效果展示

加载完成效果 动态渐进渲染效果

三、实现原理

  1. 不一次性加载整张图片
  2. 每次只加载一小条区域
  3. 通过 Canvas 把这一小条画到屏幕上
  4. 不断重复,直到整张图片绘制完成
  5. 自动计算居中位置,让图片显示在屏幕正中间

四、实现思路

  1. 使用 ImageSource 加载图片资源
  2. 按区域增量创建 PixelMap
  3. 计算等比缩放尺寸与屏幕居中偏移量
  4. Canvas 逐行绘制图片条带
  5. 绘制完成后释放资源,避免内存泄漏

五、完整代码

javascript 复制代码
import { image } from '@kit.ImageKit';
import { BusinessError } from '@kit.BasicServicesKit';

@Entry
@Component
struct Index {
  private canvasCtx: CanvasRenderingContext2D = new CanvasRenderingContext2D();
  private imageSource: image.ImageSource | null = null;

  private imageWidth = 0;
  private imageHeight = 0;
  private currentLine = 0;

  // 每次绘制的高度 这两行代码控制绘制速度
  private readonly stepHeight = 10;
  // 绘制间隔时间
  private readonly delayMs = 5;

  private displayWidth = 0;
  private displayHeight = 0;
  private canvasViewWidth = 0;
  private canvasViewHeight = 0;

  private timerId: number | null = null;

  build() {
    Column() {
      Canvas(this.canvasCtx)
        .width('100%')
        .height('100%')
        .onReady(() => {
          this.canvasViewWidth = this.canvasCtx.width ?? 0;
          this.canvasViewHeight = this.canvasCtx.height ?? 0;
          this.startLoadProgressiveImage();
        })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#000')
  }

  aboutToDisappear() {
    this.clearTimer();
  }

  clearTimer() {
    if (this.timerId !== null) {
      clearTimeout(this.timerId);
      this.timerId = null;
    }
  }

  // 等比缩放计算
  calcFitSize(srcW: number, srcH: number, maxW: number, maxH: number) {
    const ratio = Math.min(maxW / srcW, maxH / srcH);
    this.displayWidth = srcW * ratio;
    this.displayHeight = srcH * ratio;
  }

  // 加载图片
  private async getImageSource(path: string): Promise<image.ImageSource | null> {
    try {
      const host = this.getUIContext().getHostContext();
      if (!host) return null;
      const mgr = host.resourceManager;
      const buf = await mgr.getRawFileContent(path);
      return image.createImageSource(buf.buffer);
    } catch (e) {
      console.error("图片加载失败", (e as BusinessError).message);
      return null;
    }
  }

  // 启动渐进式加载
  async startLoadProgressiveImage() {
    const img = await this.getImageSource("mimi.jpeg");
    if (!img) return;

    this.imageSource = img;
    const info = await img.getImageInfo();
    this.imageWidth = info.size.width;
    this.imageHeight = info.size.height;
    this.currentLine = 0;

    this.calcFitSize(this.imageWidth, this.imageHeight, this.canvasViewWidth, this.canvasViewHeight);
    this.drawLineByLine();
  }

  // 逐行绘制核心
  async drawLineByLine() {
    if (!this.imageSource || !this.canvasCtx) return;

    if (this.currentLine >= this.imageHeight) {
      console.log("图片居中绘制完成");
      this.clearTimer();
      return;
    }

    const decodeH = Math.min(this.currentLine + this.stepHeight, this.imageHeight);
    const region: image.Region = {
      x: 0,
      y: this.currentLine,
      size: { width: this.imageWidth, height: decodeH - this.currentLine }
    };

    const pixelMap = await this.imageSource.createPixelMap({
      desiredRegion: region,
      desiredPixelFormat: image.PixelMapFormat.RGBA_8888
    });
    if (!pixelMap) return;

    // 水平 + 垂直居中
    const offsetX = (this.canvasViewWidth - this.displayWidth) / 2;
    const offsetY = (this.canvasViewHeight - this.displayHeight) / 2;

    const drawY = offsetY + this.currentLine * (this.displayHeight / this.imageHeight);
    const drawH = region.size.height * (this.displayHeight / this.imageHeight);

    // 绘制图片
    this.canvasCtx.drawImage(pixelMap, offsetX, drawY, this.displayWidth, drawH);
    pixelMap.release();

    this.currentLine += this.stepHeight;

    this.timerId = setTimeout(() => {
      this.drawLineByLine();
    }, this.delayMs);
  }
}

六、关键代码说明

  1. 全屏 Canvas

    让画布占满整个屏幕,方便图片居中显示。

  2. 等比缩放

    自动计算宽高比,保证图片不变形、不拉伸

  3. 增量区域解码

    每次只解码图片的一小条区域,而不是整张图,内存占用极低

  4. 双向居中算法

    自动计算水平、垂直偏移量,让图片无论什么尺寸,都显示在屏幕正中间

  5. 资源安全释放

    页面退出时清除定时器,释放像素资源,避免内存泄漏

七、使用方法

  1. 将一张高清图片放入 src/main/resources/rawfile
  2. 代码中图片名称改为你的图片名称
  3. 直接运行即可看到效果

我是散修

专注鸿蒙原生开发、持续分享鸿蒙技术,带你实现好玩有趣实用的功能!

相关推荐
谷子在生长12 小时前
纯血鸿蒙自定义弹窗最佳实践:从「到处复制」到「一行调用」
前端·harmonyos
小魔女千千鱼20 小时前
把 Go 塞进鸿蒙PC:windows上用 c-shared 跑 2048
harmonyos
TrisighT20 小时前
Electron 跑在鸿蒙 PC 上,单窗口和多窗口内存差 800MB?我抓了 5 组数据
性能优化·electron·harmonyos
TrisighT2 天前
AI写埋点代码,35%覆盖率坑惨运营
harmonyos·arkts·arkui
Junerver5 天前
把 DevEco Code 的 HarmonyOS 开发能力装进口袋——harmonyos-dev-skill
harmonyos
程序猿追6 天前
那个右下角的小数字怎么“卡”住我打字——我用 HarmonyOS 自己写了一个字数限制输入框
pytorch·华为·harmonyos
古德new6 天前
鸿蒙PC使用electron迁移:Joplin Electron 桌面适配全记录
华为·electron·harmonyos
世人万千丶6 天前
桌面便签小应用 - HarmonyOS ArkUI 开发实战-TextArea与Flex布局-PC版本
华为·harmonyos·鸿蒙·鸿蒙系统
慧海灵舟6 天前
AGenUI 鸿蒙端实战踩坑录:从 Column 布局消失到异步组件宽度为 0
华为·harmonyos