HarmonyOS 鸿蒙Next中如何使用Canvas进行自定义绘图?

HarmonyOS 鸿蒙Next中如何使用Canvas进行自定义绘图? 如何使用Canvas进行自定义绘图?
如何绘制复杂图形、实现动画效果和手势交互?

3 回复

解决方案

1. Canvas基础绘图

@Entry
@Component
struct BasicCanvas {
  private settings: RenderingContextSettings = new RenderingContextSettings(true)
  private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)

  build() {
    Column() {
      Canvas(this.context)
        .width('100%')
        .height(300)
        .backgroundColor('#f5f5f5')
        .onReady(() => {
          this.drawBasicShapes()
        })
    }
  }

  private drawBasicShapes() {
    // 绘制矩形
    this.context.fillStyle = '#ff6b6b'
    this.context.fillRect(20, 20, 100, 60)

    // 绘制圆形
    this.context.fillStyle = '#4ecdc4'
    this.context.beginPath()
    this.context.arc(200, 50, 30, 0, Math.PI * 2)
    this.context.fill()

    // 绘制线条
    this.context.strokeStyle = '#45b7d1'
    this.context.lineWidth = 3
    this.context.beginPath()
    this.context.moveTo(20, 120)
    this.context.lineTo(120, 120)
    this.context.lineTo(70, 180)
    this.context.closePath()
    this.context.stroke()

    // 绘制文本
    this.context.font = '20px sans-serif'
    this.context.fillStyle = '#2d3436'
    this.context.fillText('Canvas绘图示例', 150, 150)
  }
}

2. 绘制渐变和阴影

@Component
struct GradientCanvas {
  private settings: RenderingContextSettings = new RenderingContextSettings(true)
  private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)

  build() {
    Canvas(this.context)
      .width('100%')
      .height(300)
      .onReady(() => {
        this.drawGradients()
      })
  }

  private drawGradients() {
    // 线性渐变
    let linearGradient = this.context.createLinearGradient(20, 20, 200, 20)
    linearGradient.addColorStop(0, '#667eea')
    linearGradient.addColorStop(1, '#764ba2')
    this.context.fillStyle = linearGradient
    this.context.fillRect(20, 20, 180, 80)

    // 径向渐变
    let radialGradient = this.context.createRadialGradient(150, 180, 10, 150, 180, 60)
    radialGradient.addColorStop(0, '#f093fb')
    radialGradient.addColorStop(1, '#f5576c')
    this.context.fillStyle = radialGradient
    this.context.fillRect(90, 120, 120, 120)

    // 阴影效果
    this.context.shadowColor = 'rgba(0, 0, 0, 0.3)'
    this.context.shadowBlur = 10
    this.context.shadowOffsetX = 5
    this.context.shadowOffsetY = 5
    this.context.fillStyle = '#4facfe'
    this.context.fillRect(240, 100, 80, 80)
  }
}

3. Canvas动画

@Component
struct AnimatedCanvas {
  private settings: RenderingContextSettings = new RenderingContextSettings(true)
  private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)
  @State private ballX: number = 50
  @State private ballY: number = 50
  private ballSpeedX: number = 3
  private ballSpeedY: number = 2
  private timer: number = -1

  build() {
    Canvas(this.context)
      .width('100%')
      .height(400)
      .backgroundColor('#1e3a8a')
      .onReady(() => {
        this.startAnimation()
      })
  }

  aboutToDisappear() {
    clearInterval(this.timer)
  }

  private startAnimation() {
    this.timer = setInterval(() => {
      this.updateBall()
      this.draw()
    }, 16) // 约60fps
  }

  private updateBall() {
    // 更新位置
    this.ballX += this.ballSpeedX
    this.ballY += this.ballSpeedY

    // 边界碰撞检测
    if (this.ballX <= 20 || this.ballX >= 340) {
      this.ballSpeedX = -this.ballSpeedX
    }
    if (this.ballY <= 20 || this.ballY >= 380) {
      this.ballSpeedY = -this.ballSpeedY
    }
  }

  private draw() {
    // 清空画布
    this.context.clearRect(0, 0, 360, 400)

    // 绘制小球
    this.context.fillStyle = '#fbbf24'
    this.context.beginPath()
    this.context.arc(this.ballX, this.ballY, 20, 0, Math.PI * 2)
    this.context.fill()

    // 添加光晕效果
    let gradient = this.context.createRadialGradient(
      this.ballX, this.ballY, 5,
      this.ballX, this.ballY, 20
    )
    gradient.addColorStop(0, 'rgba(251, 191, 36, 1)')
    gradient.addColorStop(1, 'rgba(251, 191, 36, 0.2)')
    this.context.fillStyle = gradient
    this.context.beginPath()
    this.context.arc(this.ballX, this.ballY, 20, 0, Math.PI * 2)
    this.context.fill()
  }
}

4. 手势交互绘图

@Component
struct DrawingCanvas {
  private settings: RenderingContextSettings = new RenderingContextSettings(true)
  private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)
  private isDrawing: boolean = false
  private lastX: number = 0
  private lastY: number = 0
  @State private lineColor: string = '#000000'
  @State private lineWidth: number = 3

  build() {
    Column({ space: 16 }) {
      // 工具栏
      Row({ space: 12 }) {
        Text('粗细:')
        Slider({
          value: this.lineWidth,
          min: 1,
          max: 20,
          step: 1
        })
          .width(120)
          .onChange((value) => {
            this.lineWidth = value
          })

        Button('清空')
          .onClick(() => {
            this.context.clearRect(0, 0, 360, 400)
          })
      }
      .padding(12)

      // 画布
      Canvas(this.context)
        .width('100%')
        .height(400)
        .backgroundColor(Color.White)
        .border({ width: 1, color: '#e0e0e0' })
        .onReady(() => {
          this.initCanvas()
        })
        .onTouch((event) => {
          this.handleTouch(event)
        })
    }
    .padding(16)
  }

  private initCanvas() {
    this.context.lineCap = 'round'
    this.context.lineJoin = 'round'
  }

  private handleTouch(event: TouchEvent) {
    const touch = event.touches[0]
    const x = touch.x
    const y = touch.y

    if (event.type === TouchType.Down) {
      this.isDrawing = true
      this.lastX = x
      this.lastY = y
    } else if (event.type === TouchType.Move && this.isDrawing) {
      this.drawLine(this.lastX, this.lastY, x, y)
      this.lastX = x
      this.lastY = y
    } else if (event.type === TouchType.Up) {
      this.isDrawing = false
    }
  }

  private drawLine(x1: number, y1: number, x2: number, y2: number) {
    this.context.strokeStyle = this.lineColor
    this.context.lineWidth = this.lineWidth
    this.context.beginPath()
    this.context.moveTo(x1, y1)
    this.context.lineTo(x2, y2)
    this.context.stroke()
  }
}

5. 绘制图表

interface ChartData {
  label: string
  value: number
  color: string
}

@Component
struct BarChart {
  private settings: RenderingContextSettings = new RenderingContextSettings(true)
  private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)
  private data: ChartData[] = [
    { label: '周一', value: 120, color: '#667eea' },
    { label: '周二', value: 200, color: '#764ba2' },
    { label: '周三', value: 150, color: '#f093fb' },
    { label: '周四', value: 180, color: '#4facfe' },
    { label: '周五', value: 220, color: '#00f2fe' }
  ]

  build() {
    Canvas(this.context)
      .width('100%')
      .height(300)
      .backgroundColor(Color.White)
      .onReady(() => {
        this.drawChart()
      })
  }

  private drawChart() {
    const padding = 40
    const chartWidth = 360 - padding * 2
    const chartHeight = 300 - padding * 2
    const barWidth = chartWidth / this.data.length - 10
    const maxValue = Math.max(...this.data.map(d => d.value))

    // 绘制坐标轴
    this.context.strokeStyle = '#666'
    this.context.lineWidth = 2
    this.context.beginPath()
    this.context.moveTo(padding, padding)
    this.context.lineTo(padding, padding + chartHeight)
    this.context.lineTo(padding + chartWidth, padding + chartHeight)
    this.context.stroke()

    // 绘制柱状图
    this.data.forEach((item, index) => {
      const barHeight = (item.value / maxValue) * chartHeight
      const x = padding + index * (barWidth + 10) + 5
      const y = padding + chartHeight - barHeight

      // 绘制柱子
      this.context.fillStyle = item.color
      this.context.fillRect(x, y, barWidth, barHeight)

      // 绘制标签
      this.context.fillStyle = '#333'
      this.context.font = '12px sans-serif'
      this.context.textAlign = 'center'
      this.context.fillText(item.label, x + barWidth / 2, padding + chartHeight + 20)

      // 绘制数值
      this.context.fillText(item.value.toString(), x + barWidth / 2, y - 5)
    })
  }
}

关键要点

  1. Canvas初始化: 必须创建RenderingContextSettings和CanvasRenderingContext2D
  2. 绘图时机: 在onReady回调中进行绘制操作
  3. 性能优化: 使用clearRect清空画布,避免重复绘制
  4. 动画实现: 使用setInterval配合clearRect实现流畅动画
  5. 手势交互: 通过onTouch事件处理用户绘图操作

最佳实践

  1. 资源管理: 组件销毁时清除定时器
  2. 性能优化: 复杂绘制使用离屏Canvas
  3. 坐标系: 注意Canvas坐标系原点在左上角
  4. 抗锯齿: 创建RenderingContextSettings时开启抗锯齿
  5. 响应式: 使用百分比宽度适配不同屏幕

更多关于HarmonyOS 鸿蒙Next中如何使用Canvas进行自定义绘图?的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


在HarmonyOS Next中,使用Canvas进行自定义绘图需通过ArkTS实现。首先,在UI组件中创建CanvasRenderingContext2D对象,并设置画布属性。然后,调用绘图API如fillRect绘制矩形、strokeText绘制文本等。可通过Path2D定义复杂路径,使用fill或stroke方法渲染。支持渐变、阴影等效果,通过变换矩阵实现图形变换。

在HarmonyOS Next中,使用Canvas进行自定义绘图主要通过CanvasRenderingContext2D API实现。以下是核心步骤和关键方法:

1. 创建Canvas组件并获取绘图上下文

在ArkTS的UI中声明Canvas组件,并通过getContext('2d')获取2D绘图上下文:

Canvas()
  .width('100%')
  .height('100%')
  .onReady((canvas: CanvasRenderingContext2D) => {
    // 在此执行绘图操作
  })

2. 基本绘图操作

  • 路径绘制:使用beginPath()moveTo()lineTo()arc()等方法创建路径,通过stroke()fill()渲染。
  • 样式设置:通过strokeStylefillStylelineWidth等属性设置颜色和线条样式。
  • 图形绘制:直接使用fillRect()strokeRect()等方法绘制矩形。

3. 绘制复杂图形

组合使用路径命令绘制多边形、曲线等复杂形状。例如,使用quadraticCurveTo()bezierCurveTo()绘制贝塞尔曲线。

4. 实现动画效果

onReady回调中结合requestAnimationFrame实现逐帧动画:

let angle = 0;
function drawFrame(ctx: CanvasRenderingContext2D) {
  ctx.clearRect(0, 0, width, height); // 清空画布
  // 根据角度更新绘制内容
  ctx.save();
  ctx.translate(centerX, centerY);
  ctx.rotate(angle);
  // 绘制图形
  ctx.restore();
  
  angle += 0.02;
  requestAnimationFrame(() => drawFrame(ctx)); // 循环下一帧
}

5. 手势交互处理

为Canvas组件绑定手势事件,如onTouchonPinch等,在事件回调中更新绘图数据并触发重绘:

Canvas()
  .onTouch((event: TouchEvent) => {
    const x = event.touches[0].x;
    const y = event.touches[0].y;
    // 根据触摸位置更新图形参数
    // 然后调用绘图函数
  })

关键提示

  • 使用save()restore()管理绘图状态,确保变换和样式不会相互干扰。
  • 复杂动画建议使用CanvasRenderingContext2D的变换方法(translaterotatescale)而非直接计算坐标。
  • 性能敏感场景可考虑离屏绘制技术。

通过上述方法,你可以灵活实现从静态图形到交互式动画的各种Canvas绘图需求。

回到顶部