目录

List嵌套Canvas滑动抖动问题

问题现象

在List组件中嵌套Canvas,滑动页面会出现轻微抖动。

问题代码示例参考如下:

import { display } from '@kit.ArkUI'

@ComponentV2
@Entry
export struct CanvasInListComponent {
  // 动画
  @Local radianTest: number = 0
  private settings: RenderingContextSettings = new RenderingContextSettings(true)
  private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)
  private offCanvas: OffscreenCanvas = new OffscreenCanvas(600, 600)
  canvasTest = (): void => {
    let offContext = this.offCanvas.getContext('2d', this.settings)
    offContext.lineCap = 'round'
    offContext.lineWidth = 14
    offContext.beginPath()
    offContext.arc(
      (this.getUIContext().px2vp(display.getDefaultDisplaySync().width) - 2 * 14) / 2.0,
      180,
      100,
      (225 - 90) * Math.PI / 180,
      (135 - 90) * Math.PI / 180
    )
    offContext.strokeStyle = '#EAF2FF'
    offContext.stroke()
    offContext.beginPath()
    offContext.arc(
      (this.getUIContext().px2vp(display.getDefaultDisplaySync().width) - 2 * 14) / 2.0,
      180,
      100,
      (225 - 90) * (Math.PI / 180),
      this.radianTest === 0 ? (135 - 90) * (Math.PI / 180) : (135 - 270 * (1 - this.radianTest) - 90) * (Math.PI / 180)
    )
    offContext.strokeStyle = '#337DFF'
    offContext.stroke()
    let image = this.offCanvas.transferToImageBitmap()
    this.context.transferFromImageBitmap(image)
    // 使用setTimeout模拟帧刷新
    setTimeout(() => {
      this.radianTest = Number(this.radianTest + 0.005)
      if (this.radianTest > 1) {
        this.radianTest = 0
      } else {
        this.canvasTest()
      }
    }, 10)
  }

  build() {
    List() {
      ForEach([1, 2, 3, 4, 5, 6, 7, 8, 9], (item: number) => {
        ListItem() {
          Row() {
            Text(item.toString())
          }
          .height(200)
        }
      })
      ListItem() {
        Canvas(this.context)
          .width('100%')
          .height(300)
          .onReady(this.canvasTest)
          .position({ x: 0, y: 0 })
      }
      .onClick(() => {
        this.canvasTest()
      })
    }
    .width('100%')
    .height('100%')
    .alignListItem(ListItemAlign.Center)
  }
}

问题效果预览:

效果预览

背景知识

问题定位

  1. 通过抓取性能Trace图,观察到在橙色标记的时间段内,存在多个方法执行耗时超出预期的情况。其中,Canvas的FireReadyEvent方法出现在关键路径上,表明当前帧的绘制任务未能按时完成,导致渲染延迟,从而引发界面卡顿。

  2. 分析onReady中绘制方法:排查到使用了OffscreenCanvasRenderingContext2D方法,根据背景知识了解到离屏绘制的绘制速度较慢,尝试修改为在屏绘制,抖动消失。

分析结论

Canvas采用了离屏绘制,使用CPU进行绘制,绘制速度较慢,导致List滑动阻塞。

修改建议

将Canvas绘制方法修改为在屏绘制。

完整示例参考如下:

import { display } from '@kit.ArkUI';

@ComponentV2
@Entry
struct DrawWithScreen {
  // 动画
  @Local radianTest: number = 0;
  private settings: RenderingContextSettings = new RenderingContextSettings(true);
  private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
  private offCanvas: OffscreenCanvas = new OffscreenCanvas(600, 600);
  canvasTest = (): void => {
    // 原先的离屏绘制改为在屏绘制
    let ctx = this.context;
    ctx.reset();
    ctx.lineCap = 'round';
    ctx.lineWidth = 14;
    ctx.beginPath();
    ctx.arc(
      (this.getUIContext().px2vp(display.getDefaultDisplaySync().width) - 2 * 14) / 2.0,
      180,
      100,
      (225 - 90) * Math.PI / 180,
      (135 - 90) * Math.PI / 180
    );
    ctx.strokeStyle = '#EAF2FF';
    ctx.stroke();
    ctx.beginPath();
    ctx.arc(
      (this.getUIContext().px2vp(display.getDefaultDisplaySync().width) - 2 * 14) / 2.0,
      180,
      100,
      (225 - 90) * (Math.PI / 180),
      this.radianTest === 0 ? (135 - 90) * (Math.PI / 180) : (135 - 270 * (1 - this.radianTest) - 90) * (Math.PI / 180),
    );
    ctx.strokeStyle = '#337DFF';
    ctx.stroke();
    // 使用setTimeout模拟帧刷新
    setTimeout(() => {
      this.radianTest = Number(this.radianTest + 0.05);
      if (this.radianTest > 1) {
        this.radianTest = 0;
      } else {
        this.canvasTest();
      }
    }, 20);
  };

  build() {
    List() {
      ForEach([1, 2, 3, 4, 5, 6, 7, 8, 9], (item: number) => {
        ListItem() {
          Row() {
            Text(item.toString());
          }
          .height(200);
        };
      });
      ListItem() {
        Canvas(this.context)
          .width('100%')
          .height(300)
          .onReady(this.canvasTest)
          .position({ x: 0, y: 0 });
      }
      .onClick(() => {
        this.canvasTest();
      });
    }
    .width('100%')
    .height('100%')
    .alignListItem(ListItemAlign.Center);
  }
}