鸿蒙应用开发:图片渐进式加载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. 直接运行即可看到效果

我是散修

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

相关推荐
代码飞一会儿3 小时前
Harmony OS开发之沉浸式模式设计学习
harmonyos·敏捷开发
liulian09164 小时前
Flutter 三方库 flutter_local_auth 的鸿蒙化适配指南
flutter·华为·学习方法·harmonyos
SuperHeroWu75 小时前
【鸿蒙基础入门】概念理解和学习方法论说明
前端·学习·华为·开源·harmonyos·鸿蒙·移动端
亘元有量-流量变现5 小时前
抓住鸿蒙流量红利!2026华为应用商店ASO优化全解
华为·harmonyos·aso优化
特立独行的猫a6 小时前
使用 vcpkg 为OpenHarmony(鸿蒙PC)构建 OpenSSH 命令行工具
harmonyos·openharmony·命令行·openssh·vcpkg·鸿蒙pc
音视频牛哥6 小时前
纯血鸿蒙(HarmonyOS NEXT)下,如何实现低延迟RTSP、RTMP播放器音视频解码?
华为·音视频·harmonyos·鸿蒙rtmp播放器·鸿蒙rtsp播放器·harmonyos rtsp·鸿蒙next播放器
特立独行的猫a7 小时前
OpenSSH 介绍及使用Lycium框架移植到鸿蒙 PC(OpenHarmony)平台的实践总结
harmonyos·openssh·鸿蒙pc·lycium_plusplus·三分库移植
轻口味7 小时前
HarmonyOS 6 轻相机应用开发2:贴纸效果实现
音视频·harmonyos·鸿蒙·播放器
HwJack208 小时前
跨模块资源共享的破局之道:HarmonyOS HSP 资源访问“避坑与升华”指南
华为·harmonyos
liulian09168 小时前
【Flutter for OpenHarmony】原生卡片 Widget 集成实战:从零构建待办清单桌面组件
flutter·华为·学习方法·harmonyos