HarmonyOS 鸿蒙Next中使用Canvas绘制折线图

HarmonyOS 鸿蒙Next中使用Canvas绘制折线图

通过PinchGesture进行缩放,缩放渲染过程中会出现闪烁(缩放越大越闪烁),这个问题应该如何解决 缩放过程中,如果内容宽度大于一屏,当前显示的位置要始终居中,又该怎么去写?

@Component
struct DrawCanvas8 {
  @Consume pathStack: NavPathStack

  // 获取上下文
  private settings: RenderingContextSettings = new RenderingContextSettings(true)
  private context2d: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)
  private context2daxis: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)
  private context2dline: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)

  private drawTotalScroll: Scroller = new Scroller()

  @Prop private comp_width: number = 0
  @Prop private comp_height: number = 0

  @State private scroll_width: number = 0

  @State private dataArray: number[] = []


  @State scaleValue: number = 1
  @State pinchValue: number = 1
  @State pinchX: number = 0
  @State pinchY: number = 0

  aboutToAppear() {
    this.loadData()
  }

  build() {
    NavDestination() {
      Column() {
        Stack() {
          Canvas(this.context2d)
            .width('100%')
            .height('100%')
            .id('bgcanvas')
            .onReady(() => {
              const width = this.context2d.width
              const height = this.context2d.height
              this.context2d.strokeStyle = '#DCDCDC'
              this.context2d.lineWidth = 1
              this.context2d.strokeRect(0, 0, width, height)

              for (let i = 1; i < 4; i++) {
                this.context2d.lineWidth = 0.5
                this.context2d.strokeStyle = '#F2F2F2'
                this.context2d.moveTo(0, height / 4 * i)
                this.context2d.lineTo(width, height / 4 * i)
              }
              this.context2d.stroke()

              this.comp_width = width
              this.comp_height = height

            })

          Canvas(this.context2daxis)
            .id('left')
            .width('100%')
            .height('100%')
            .onReady(() => {

            })


          Scroll(this.drawTotalScroll) {
            Column({ space: 0 }) {

              Canvas(this.context2dline)
                .id('lines')
                .width('100%')
                .height('100%')
                .onReady(() => {
                })

            }
            .height('100%')
            .width(this.scroll_width)
          }
          .id('totalscroll')
          .width('100%')
          .height('100%')
          .hitTestBehavior(HitTestMode.Transparent)
          .scrollable(ScrollDirection.Horizontal)
          .scrollBar(BarState.Off)
          .onDidScroll(() => {

          })
          .gesture(
            PinchGesture({ fingers: 2 })
              .onActionStart((event: GestureEvent) => {
                console.info('Pinch start')
              })
              .onActionUpdate((event: GestureEvent) => {
                if (event) {
                  this.scaleValue = this.pinchValue * event.scale
                  this.pinchX = event.pinchCenterX
                  this.pinchY = event.pinchCenterY
                  this.draw()
                }
              })
              .onActionEnd((event: GestureEvent) => {
                this.pinchValue = this.scaleValue
                console.info('Pinch end')
              })
          )
        }
        .width('100%')
        .height(300)
        .padding(10)
      }
      .width('100%')
      .height('100%')
    }
  }

  private loadData() {
    setTimeout(() => {
      let tempArray: number[] = []
      for (let index = 0; index < 100; index++) {
        tempArray.push(getRandomNumber(200))
      }
      this.dataArray = tempArray
      this.scroll_width = tempArray.length * 10;
      this.draw()
    }, 500)
  }

  private draw() {
    const min = Math.min(...this.dataArray.map((item: number) => item))
    const max = Math.max(...this.dataArray.map((item: number) => item))

    //Y轴
    this.context2daxis.reset()
    let sepValue = (max - min) / 4
    let yValues: number[] = []
    this.context2daxis.font = '8vp'
    this.context2daxis.fillStyle = '#333333'
    for (let index = 0; index < 5; index++) {
      let y = min + index * sepValue;
      if (index == 0) {
        this.context2daxis.textBaseline = 'bottom'
      } else if (index == 4) {
        this.context2daxis.textBaseline = 'top'
      } else {
        this.context2daxis.textBaseline = 'middle'
      }
      this.context2daxis.fillText(y.toFixed(2), 0, this.comp_height / 4 * (4 -index))
      yValues.push(y)
    }


    const w = 10 * this.scaleValue
    let total_width = w * this.dataArray.length
    if (total_width < this.comp_width) {
      this.scroll_width = this.comp_width
    } else {
      this.scroll_width = total_width
    }


    this.context2dline.reset()
    this.context2dline.beginPath()
    this.context2dline.strokeStyle = '#FF0000'
    this.context2dline.lineWidth = 1
    for (let index = 0; index < this.dataArray.length; index++) {
      const element = this.dataArray[index];
      let x = w * index
      let y = this.comp_height * (element - min) / (max - min)
      if (index == 0) {
        this.context2dline.moveTo(x, y)
      } else {
        this.context2dline.lineTo(x, y)
      }
    }
    this.context2dline.stroke()
  }

}

更多关于HarmonyOS 鸿蒙Next中使用Canvas绘制折线图的实战教程也可以访问 https://www.itying.com/category-93-b0.html

8 回复

开发者您好,可以采取以下方式解决:

【背景知识】

Canvas画布的缩放也就是大小改变的时候,之前在画布绘制的图形会消失,导致重新绘制。

【定位思路】

containerPinchGestureUpdate缩放监听,updateScale/updateContainerOffset手势数据处理,updateContainerOffset缩放绘制方法等一个或多个条件满足时之前绘制的图形会消失,这样就会导致频繁的绘制图形,造成卡顿。

【解决方案】

可以设置一个临界点,在手势捏合持续回调中,只有满足一定条件才重新绘制,尽可能的减少捏合手势持续调用绘制内容,通过捏合比例大于某个值得时候才重新绘制。

代码示例如下:

if (Math.abs(offsetX - this.lastOffsetX) < 0.5 && Math.abs(offsetY - this.lastOffsetY) < 0.5) {
  return
}
this.lastOffsetX = offsetX
this.lastOffsetY = offsetY
this.context.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
if (this.useTransform) {
  this.context.setTransform(1, 0, 0, 1, 0, 0);
  this.context.setTransform(scale, 0, 0, scale, 0, 0);
}

【常见FAQ】

Q:画布组件放大会导致app闪退是什么原因? A:每次放大画布组件都会频繁触发PinchGesture().onActionUpdate()回调,回调中的containerPinchGestureUpdate(event)方法的调用链中的containerScaleDidUpdate方法中的语句。

// 注释掉改行,建议不要每次都创建新的离屏画布
this.offCanvas = new OffscreenCanvas(this.canvasWidth, this.canvasHeight)

Q:Canvas画线超出屏幕长度后,画布一直闪烁是什么原因? A:动态画线每次更新一条线都要修改Canvas宽高,这样性能消耗很高,闪烁属于正常现象。建议给Canvas预设宽高,当画线达到后在设置。

if (this.x>=  this.canvasWidth) {
  this.canvasWidth = this.screenWidth*4+this.x
}

更多关于HarmonyOS 鸿蒙Next中使用Canvas绘制折线图的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


为什么不用arkweb 渲染echart,这样自定义起来非常方便

刚接触鸿蒙,不知道怎么用呢,之前用的也是单纯绘制,所以就按照这种方式写了,

找HarmonyOS工作还需要会Flutter技术的哦,有需要Flutter教程的可以学学大地老师的教程,很不错,B站免费学的哦:BV1S4411E7LY/?p=17

现阶段鸿蒙的报表组件还不完善,在业务开发中为了快速实现数据可视化的效果,可以使用arkWeb加载H5的形式实现报表,这样报表就可以使用各种各样的js库了,例如echart,可以看一下这篇文章这篇文章

也能画股票的K线图吗,

在HarmonyOS鸿蒙Next中使用Canvas绘制折线图的步骤如下:

  1. 创建Canvas组件并设置宽高
  2. 获取CanvasRenderingContext2D对象
  3. 使用moveTo和lineTo方法绘制折线
  4. 设置strokeStyle和lineWidth属性定义线条样式
  5. 调用stroke方法完成绘制

关键代码示例:

// 创建Canvas
Canvas(this.context)
  .width('100%')
  .height('100%')
// 绘制逻辑
const ctx = this.context.getContext('2d')
ctx.moveTo(x1, y1)
ctx.lineTo(x2, y2)
ctx.stroke()

注意坐标数据需自行计算处理。

针对HarmonyOS Next中使用Canvas绘制折线图的缩放闪烁问题,建议从以下方面优化:

  1. 闪烁问题解决方案:
  • 使用双缓冲技术:在缩放时先将内容绘制到离屏Canvas,再一次性渲染到主Canvas
  • 限制缩放频率:添加节流逻辑,避免频繁重绘
  • 优化绘制逻辑:在draw()方法中减少不必要的reset()调用
  1. 居中显示实现方案:
  • 在onActionUpdate回调中计算当前视口中心点
  • 根据缩放比例调整scrollTo参数
  • 使用Scroller的scrollTo方法实现精确定位

核心修改点示例:

.onActionUpdate((event: GestureEvent) => {
  if (event) {
    // 节流处理
    const now = Date.now();
    if (now - this.lastScaleTime < 16) return; // 60fps
    
    // 计算缩放中心偏移
    const centerOffsetX = (event.pinchCenterX / this.comp_width) * this.scroll_width;
    
    this.scaleValue = this.pinchValue * event.scale;
    this.draw();
    
    // 保持居中
    const newWidth = 10 * this.scaleValue * this.dataArray.length;
    const scrollX = centerOffsetX * (newWidth / this.scroll_width) - event.pinchCenterX;
    this.drawTotalScroll.scrollTo({x: scrollX, y: 0});
    
    this.lastScaleTime = now;
  }
})
  1. 性能优化建议:
  • 对大数据集进行分段绘制
  • 使用Path2D对象缓存路径数据
  • 避免在缩放过程中频繁计算极值(min/max)

这些修改可以有效减少闪烁并保持居中显示效果。

回到顶部