适用:HarmonyOS API 12(SDK 5.0.0),ArkWeb(Web 组件)加载 H5 页面,需要把超过一屏的长页面完整截成一张长图并分享。
核心结论:不要用webPageSnapshot做滚动长截图,用「controller.scrollTo+getComponentSnapshot分屏截取 + 像素拼接」。
1. 需求与难点
把 Web 组件里的整篇 H5 长页面(可能长达上万 CSS px)生成一张完整长图:
- 不能截断(超过单张位图上限,系统侧约 16384 物理像素);
- 不能有重复内容(分块截图最容易踩的坑);
2. 方案选型:为什么不能用 webPageSnapshot
ArkWeb 提供两个截图 API,行为完全不同:
| controller.webPageSnapshot({ id, size }) | 让 Chromium 从文档顶部 y=0 重新绘制 一张位图,size 是"Web 绘制尺寸",与当前滚动位置无关 | 整页快照(页面简单、无吸顶元素、高度不超限) |
| getUIContext().getComponentSnapshot().get(id, { waitUntilRenderFinished: true }) | 截取组件当前在屏幕上的真实画面 | 滚动长截图(官方推荐) |
关键坑
- 传
size.height会从文档顶部重绘 :无论 JSwindow.scrollTo到哪里,每一块都从 y=0 开始画 → 分块必然重复。 - 不传 size:底层返回从当前 scrollTop 向下最多约 16384 物理像素的位图,块高不可控,仍无法与滚动步长对齐。
- 连续全量绘制内存压力大,有概率失败。
- 官方文档明确:网页含动态资源、固定标题头等复杂结构时,推荐使用滚动截图方案。
教训:看到 API 名字里有 "snapshot" 就以为是"截当前画面",实际 webPageSnapshot 是"重绘页面"。选型前必须看官方文档对参数语义的描述。
3. 最终方案:滚动截图三段式
对齐 Android 原生 WebView.draw(Canvas) 长截图思路:
arduino
逐块循环:
① 用 getComponentSnapshot 截 Web 组件当前屏幕画面(真实画面,不重绘)
② 记录截图时刻的实测 window.scrollY(块的真实起点)
③ controller.scrollTo 滚到下一屏,轮询实测 scrollY 确认滚动到位
拼接:
④ 每块 readPixels 读像素,按顺序拷进一块大 ArrayBuffer
⑤ image.createPixelMap(mergedBuffer, { size, pixelFormat }) 一次性建图
后处理:
⑥ 顶部非正文区域按 dpr 换算后物理裁剪
3.1 为什么块起点必须"截图前实测"
- 循环顺序是「截图 → 判断到底 → 滚动」,最后一块截图后不再滚动;
- 如果用单变量记录位置,末块的起点会错记成上一块的位置,导致末块裁剪高度算错 → 底部内容丢失;
- 正确做法:每块截图前读一次
window.scrollY存数组blockYs[],拼接末块时用pageH - blockYs[last]算真实剩余高度。
3.2 滚动控制:scrollTo 绝对位置 + 实测轮询
- 用
controller.scrollTo(x, y)(绝对位置),不要用scrollBy增量------多次增量累积误差,且补滚时语义混乱; - 调用后轮询 JS
window.scrollY(每次间隔 ~100ms,连续 3 次位置不变视为稳定),不能靠固定 sleep 猜时机; - 没滚够目标位置时按差值重试(最多 5 轮);
- 循环是否继续只看实测位置有没有前进 (
newY > startY + 1),不要依赖滚动方法的返回值提前 break,否则页面惯性滚动慢时会被误判成"到底" → 底部缺内容。
3.3 等待渲染完成
截图选项必须带 waitUntilRenderFinished: true;再配合每块 3 次重试(间隔 300ms),可消除偶发截图失败。
3.4 像素拼接(纯 ImageKit,无需 GraphicsKit)
SDK 5.0.0(12) 没有 @kit.GraphicsKit 的 Canvas,直接操作像素内存:
php
// 每块 ARGB_8888,每像素 4 字节
const stride = pw * 4
const dst = new ArrayBuffer(pw * totalH * 4)
const view = new Uint8Array(dst)
for (每块 i) {
const buf = new ArrayBuffer(pw * blockH_i * 4)
await pm.readPixels({
pixels: buf, offset: 0, stride,
region: { size: { width: pw, height: blockH_i }, x: 0, y: 0 }
})
view.set(new Uint8Array(buf), offsetBytes)
offsetBytes += blockH_i * stride
}
const result = await image.createPixelMap(dst, {
size: { width: pw, height: totalH }, pixelFormat
})
要点:
- 末块只拷有效高度 :
remainCss = pageH - blockYs[last],再换算物理像素,否则长图底部带一块空白; - 每块用完立即
pm.release(),避免大图 OOM; - 页面总高取
controller.getPageHeight()与 JS 内容高度的较大值:偏小会提前结束丢内容,偏大只是末块多一点底色。