在HarmonyOS 6的AI旅行助手应用中,用户生成攻略后最痛的点是“分享难”。动态海报生成慢且耗Token,直接截图又受限于屏幕高度。此前我们实现了基础的滚动截图,但在真机实测中,“抖动重影”“Web空白”两大问题频发。本文将基于componentSnapshotgetWebSnapshot,重构一套像素级精准的长截图架构,彻底解决异步渲染导致的拼接Bug。

一、长截图的两大“顽疾”与根因分析

1. 问题现场:为何截出来是“废片”?

在AI助手的高频使用场景中,长截图失败通常表现为两种形态:

场景

现象

根因

List/Column滚动

图片拼接处出现错位、重影

滚动动画未结束就截图,截取了中间帧

Web组件截图

截取到空白上一屏内容

未启用全页绘制,且未等待onPageEnd回调

2. 核心机制:Snapshot的“瞬时性”陷阱

componentSnapshot.get()​ 是一个瞬时操作,它只捕获当前渲染帧。如果此时列表正在执行scrollTo动画,或者Web页面还在加载,捕获到的就是不完整的过渡帧

关键结论:长截图的本质不是“截图”,而是“等待渲染稳定”

二、List/Column长截图重构:防抖滚动算法

1. 核心思路:帧等待(Frame Waiting)

在每次滚动后,必须插入一个等待周期,让滚动动画彻底完成,渲染树稳定后再截图。直接使用sleep是低效且不准确的,我们采用Promise + 递归的防抖算法。

2. 重构后的代码(ETS版)

import componentSnapshot from '@ohos.component.snapshot';

@Entry
@Component
struct AITravelList {
  @State isCapturing: boolean = false;
  private listScroller: Scroller = new Scroller();

  // 核心:带等待的滚动函数
  private async scrollWithWait(offset: number): Promise<void> {
    return new Promise<void>((resolve) => {
      // 1. 监听滚动结束事件(HarmonyOS 6 Scroller新特性)
      this.listScroller.onScrollEnd(() => {
        resolve();
      });

      // 2. 执行滚动(禁用动画,使用瞬时跳转避免过度帧)
      this.listScroller.scrollTo({
        xOffset: 0,
        yOffset: offset,
        duration: 0 // 关键:duration设为0,直接跳转而非动画
      });

      // 3. 兜底:如果onScrollEnd未触发,200ms后强制继续
      setTimeout(resolve, 200);
    });
  }

  // 主截图流程
  async takeLongSnapshot(): Promise<image.PixelMap> {
    if (this.isCapturing) {
      return;
    }
    this.isCapturing = true;

    const snapshots: image.PixelMap[] = [];
    const scrollStep = 800; // 每次滚动的高度(根据屏幕密度调整)

    try {
      // 1. 滚动到顶部(重置状态)
      await this.scrollWithWait(0);

      // 2. 获取第一屏
      let firstSnap = await componentSnapshot.get(this.listRef);
      snapshots.push(firstSnap);

      // 3. 计算总高度(假设已知,可通过ListState获取)
      const totalHeight = this.getListTotalHeight();
      let currentScroll = scrollStep;

      // 4. 循环滚动截图
      while (currentScroll < totalHeight) {
        // 4.1 滚动到下一屏(等待稳定)
        await this.scrollWithWait(currentScroll);
        
        // 4.2 截取当前视口
        let snap = await componentSnapshot.get(this.listRef);
        
        // 4.3 **关键:只保留新增部分(防重影)**
        // 计算上一张图底部与当前图的重叠区域,只截取非重叠部分
        let croppedSnap = this.cropOverlap(snap, scrollStep);
        snapshots.push(croppedSnap);
        
        currentScroll += scrollStep;
      }

      // 5. 纵向拼接所有图片
      return await this.mergeImagesVertically(snapshots);
    } finally {
      this.isCapturing = false;
    }
  }

  build() {
    List({ scroller: this.listScroller }) {
      // ... 列表内容
    }
    .height('100%')
    .onReachEnd(() => {
      // 加载更多(需在截图时暂停)
    })
  }
}

3. 避坑指南:List截图的“三不”原则

操作

正确做法

错误做法

滚动方式

scrollTo+ duration: 0(瞬时跳转)

使用动画滚动(易截到模糊帧)

截图时机

onScrollEnd回调后

滚动后立即截图

内容处理

裁剪掉上一屏的底部重叠部分

全图拼接(导致重复内容)

三、Web组件长截图:全页绘制与加载监听

1. 核心痛点:enableWholeWebPageDrawing的“开关”时机

Web组件的截图依赖getWebSnapshot(),但必须提前开启全页绘制模式,且必须等待页面完全加载

2. 完整Web截图流程

import webview from '@ohos.web.webview';

@Entry
@Component
struct AIWebGuide {
  private webController: webview.WebviewController = new webview.WebviewController();
  private isPageLoaded: boolean = false;

  aboutToAppear() {
    // !!!必须在Web组件初始化时就开启,否则无效
    this.webController.enableWholeWebPageDrawing(true);
  }

  async takeWebSnapshot(): Promise<image.PixelMap> {
    // 1. 检查加载状态(必须)
    if (!this.isPageLoaded) {
      console.error('Web page not loaded, cannot snapshot');
      return;
    }

    // 2. 直接获取全页截图(无需滚动,enableWholeWebPageDrawing已生效)
    return await this.webController.getWebSnapshot();
  }

  build() {
    Column() {
      Web({
        src: this.guideUrl,
        controller: this.webController
      })
      .onPageEnd(() => {
        // !!!必须在页面加载完成后才允许截图
        this.isPageLoaded = true;
      })
    }
  }
}

3. Web截图避坑表

配置项

作用

缺失后果

enableWholeWebPageDrawing(true)

允许截取整个网页(包括非可视区域)

只能截取可视区域,长图失效

onPageEnd回调

确保DOM渲染完成

截取到空白或半加载页面

注意enableWholeWebPageDrawing必须在Web初始化时调用,在onPageEnd中设置可能已晚。

四、性能与体验平衡:SaveButton的“安全”使用

1. 为何必须用SaveButton?

HarmonyOS 6对相册写入权限管控严格,普通按钮无法直接写入。SaveButton是系统提供的安全控件,它会自动处理授权弹窗。

2. 预览与保存的最佳实践

// 在build中
SaveButton(this.previewImage) // previewImage是PixelMap类型
  .onClick(() => {
    // 用户点击保存后,系统会自动处理授权和写入
  })

// 生成预览图的方法
async generatePreview() {
  let longImage = await this.takeLongSnapshot();
  this.previewImage = longImage; // 赋值给SaveButton的源
}

五、总结:长截图的“像素级”法则

  1. List/Column截图禁用滚动动画duration: 0),通过onScrollEnd等待渲染稳定,并裁剪重叠区域

  2. Web截图:在aboutToAppear中立即开启enableWholeWebPageDrawing,并严格在onPageEnd回调后截图。

  3. 保存环节:必须使用SaveButton,它内置了相册写入的安全逻辑。

通过这套重构后的“防抖”架构,AI旅行助手的攻略分享将不再受抖动与空白困扰,实现真正的一键高清长图分享

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐