鸿蒙5 ArkUI Canvas组件创意开发:绘制自定义图形实战

暗雨OL
发布于 2025-6-30 01:54
浏览
0收藏

鸿蒙5的ArkUI框架提供了强大的Canvas组件,允许开发者通过代码绘制各种自定义图形和创意视觉效果。本文将深入探讨Canvas组件的使用,从基础绘制到高级动画效果,带你掌握鸿蒙平台上的图形绘制技术。

Canvas组件基础
Canvas是ArkUI中用于绘制2D图形的组件,它提供了丰富的API来绘制路径、形状、文本和图像。让我们从一个简单的例子开始:

// pages/BasicCanvasPage.ets
@Entry
@Component
struct BasicCanvasPage {
private settings: RenderingContextSettings = new RenderingContextSettings(true)
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)

build() {
Column() {
Canvas(this.context)
.width(‘100%’)
.height(‘60%’)
.backgroundColor(‘#F5F5F5’)
.onReady(() => {
this.drawBasicShapes()
})
}
.width(‘100%’)
.height(‘100%’)
}

private drawBasicShapes() {
// 绘制矩形
this.context.fillStyle = ‘#FF5722’
this.context.fillRect(50, 50, 200, 150)

// 绘制圆形
this.context.beginPath()
this.context.arc(300, 125, 75, 0, Math.PI * 2)
this.context.fillStyle = '#4CAF50'
this.context.fill()

// 绘制线条
this.context.beginPath()
this.context.moveTo(400, 50)
this.context.lineTo(500, 200)
this.context.lineTo(600, 50)
this.context.strokeStyle = '#2196F3'
this.context.lineWidth = 5
this.context.stroke()

// 绘制文本
this.context.font = '30px sans-serif'
this.context.fillStyle = '#333333'
this.context.fillText('鸿蒙Canvas', 50, 250)

}
}
这个基础示例展示了:

如何创建Canvas组件并获取绘图上下文
使用onReady回调确保Canvas准备就绪后再绘制
绘制基本形状(矩形、圆形、线条)
设置样式(颜色、线宽、字体)
绘制文本内容
高级绘图技术

  1. 路径绘制与贝塞尔曲线
    // pages/PathDrawingPage.ets
    @Entry
    @Component
    struct PathDrawingPage {
    private settings: RenderingContextSettings = new RenderingContextSettings(true)
    private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)

build() {
Column() {
Canvas(this.context)
.width(‘100%’)
.height(‘70%’)
.backgroundColor(‘#F5F5F5’)
.onReady(() => {
this.drawComplexPath()
})
}
.width(‘100%’)
.height(‘100%’)
}

private drawComplexPath() {
// 绘制复杂路径
this.context.beginPath()
this.context.moveTo(50, 50)
this.context.bezierCurveTo(100, 25, 150, 75, 200, 50)
this.context.quadraticCurveTo(250, 25, 300, 50)
this.context.lineTo(300, 150)
this.context.bezierCurveTo(250, 175, 200, 125, 150, 150)
this.context.quadraticCurveTo(100, 175, 50, 150)
this.context.closePath()

// 设置渐变填充
const gradient = this.context.createLinearGradient(50, 50, 300, 150)
gradient.addColorStop(0, '#FF9800')
gradient.addColorStop(1, '#E91E63')
this.context.fillStyle = gradient
this.context.fill()

// 设置描边样式
this.context.strokeStyle = '#3F51B5'
this.context.lineWidth = 3
this.context.stroke()

// 绘制控制点标记
this.drawControlPoints()

}

private drawControlPoints() {
// 绘制贝塞尔曲线控制点
this.context.fillStyle = ‘#FF0000’
this.context.beginPath()
this.context.arc(100, 25, 5, 0, Math.PI * 2)
this.context.arc(150, 75, 5, 0, Math.PI * 2)
this.context.arc(250, 25, 5, 0, Math.PI * 2)
this.context.fill()

// 绘制二次贝塞尔曲线控制点
this.context.fillStyle = '#00FF00'
this.context.beginPath()
this.context.arc(100, 175, 5, 0, Math.PI * 2)
this.context.arc(250, 175, 5, 0, Math.PI * 2)
this.context.fill()

}
}
2. 图像处理与变换
// pages/ImageTransformPage.ets
@Entry
@Component
struct ImageTransformPage {
private settings: RenderingContextSettings = new RenderingContextSettings(true)
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)
private img: ImageBitmap = new ImageBitmap()

build() {
Column() {
Canvas(this.context)
.width(‘100%’)
.height(‘70%’)
.backgroundColor(‘#F5F5F5’)
.onReady(() => {
// 加载图片资源
this.img.src = $r(‘app.media.harmonyos’)
this.img.onload = () => {
this.drawImageWithTransform()
}
})
}
.width(‘100%’)
.height(‘100%’)
}

private drawImageWithTransform() {
const centerX = 200
const centerY = 200

// 保存当前画布状态
this.context.save()

// 平移坐标系到中心点
this.context.translate(centerX, centerY)

// 旋转45度
this.context.rotate(Math.PI / 4)

// 缩放图像
this.context.scale(0.8, 0.8)

// 绘制图像(坐标已相对于新坐标系)
this.context.drawImage(this.img, -50, -50, 100, 100)

// 恢复画布状态
this.context.restore()

// 绘制裁剪后的图像
this.context.beginPath()
this.context.arc(400, 200, 80, 0, Math.PI * 2)
this.context.clip()
this.context.drawImage(this.img, 320, 120, 160, 160)

}
}
动画效果实现
Canvas的强大之处在于可以实现各种动态效果。下面我们创建一个粒子动画示例:

// pages/ParticleAnimationPage.ets
@Entry
@Component
struct ParticleAnimationPage {
private settings: RenderingContextSettings = new RenderingContextSettings(true)
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)
private particles: Particle[] = []
private animationId: number = 0

// 粒子类
private class Particle {
x: number
y: number
size: number
speedX: number
speedY: number
color: string

constructor(width: number, height: number) {
  this.x = Math.random() * width
  this.y = Math.random() * height
  this.size = Math.random() * 5 + 1
  this.speedX = Math.random() * 3 - 1.5
  this.speedY = Math.random() * 3 - 1.5
  this.color = `hsl(${Math.random() * 360}, 100%, 50%)`
}

update(width: number, height: number) {
  this.x += this.speedX
  this.y += this.speedY
  
  // 边界检查
  if (this.x < 0 || this.x > width) this.speedX *= -1
  if (this.y < 0 || this.y > height) this.speedY *= -1
}

draw(ctx: CanvasRenderingContext2D) {
  ctx.beginPath()
  ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2)
  ctx.fillStyle = this.color
  ctx.fill()
}

}

build() {
Column() {
Canvas(this.context)
.width(‘100%’)
.height(‘70%’)
.backgroundColor(‘#111111’)
.onReady(() => {
this.initParticles()
this.animate()
})
.onDisappear(() => {
// 组件消失时取消动画
cancelAnimationFrame(this.animationId)
})
}
.width(‘100%’)
.height(‘100%’)
}

private initParticles() {
const canvas = this.context.canvas
for (let i = 0; i < 100; i++) {
this.particles.push(new Particle(canvas.width, canvas.height))
}
}

private animate() {
const canvas = this.context.canvas

// 清除画布
this.context.clearRect(0, 0, canvas.width, canvas.height)

// 更新并绘制所有粒子
this.particles.forEach(particle => {
  particle.update(canvas.width, canvas.height)
  particle.draw(this.context)
})

// 绘制粒子间的连线
this.drawConnections()

// 继续动画循环
this.animationId = requestAnimationFrame(() => this.animate())

}

private drawConnections() {
const canvas = this.context.canvas
const maxDistance = 100

for (let i = 0; i < this.particles.length; i++) {
  for (let j = i + 1; j < this.particles.length; j++) {
    const dx = this.particles[i].x - this.particles[j].x
    const dy = this.particles[i].y - this.particles[j].y
    const distance = Math.sqrt(dx * dx + dy * dy)
    
    if (distance < maxDistance) {
      this.context.beginPath()
      this.context.strokeStyle = `rgba(255, 255, 255, ${1 - distance / maxDistance})`
      this.context.lineWidth = 0.5
      this.context.moveTo(this.particles[i].x, this.particles[i].y)
      this.context.lineTo(this.particles[j].x, this.particles[j].y)
      this.context.stroke()
    }
  }
}

}
}
实战案例:绘制动态图表
让我们结合Canvas的绘图能力和动画特性,创建一个动态数据图表:

// pages/DynamicChartPage.ets
@Entry
@Component
struct DynamicChartPage {
private settings: RenderingContextSettings = new RenderingContextSettings(true)
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)
private data: number[] = []
private animationId: number = 0
@State private chartTitle: string = ‘实时数据监控’

build() {
Column({ space: 10 }) {
Text(this.chartTitle)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.margin({ top: 10 })

  Canvas(this.context)
    .width('100%')
    .height('70%')
    .backgroundColor('#FFFFFF')
    .border({ width: 1, color: '#EEEEEE' })
    .onReady(() => {
      this.initData()
      this.animateChart()
    })
    .onDisappear(() => {
      cancelAnimationFrame(this.animationId)
    })
  
  Button('切换数据类型')
    .width('60%')
    .height(40)
    .margin({ bottom: 20 })
    .onClick(() => {
      this.chartTitle = this.chartTitle === '实时数据监控' 
        ? 'CPU使用率(%)' 
        : '实时数据监控'
    })
}
.width('100%')
.height('100%')

}

private initData() {
// 初始化随机数据
for (let i = 0; i < 20; i++) {
this.data.push(Math.random() * 80 + 20)
}
}

private animateChart() {
const canvas = this.context.canvas
const width = canvas.width
const height = canvas.height
const padding = 40

// 清除画布
this.context.clearRect(0, 0, width, height)

// 更新数据 - 移除第一个数据点并添加新的随机数据
this.data.shift()
this.data.push(Math.random() * 80 + 20)

// 绘制图表背景
this.drawChartBackground(width, height, padding)

// 绘制数据线
this.drawDataLine(width, height, padding)

// 绘制数据点
this.drawDataPoints(width, height, padding)

// 继续动画
this.animationId = requestAnimationFrame(() => this.animateChart())

}

private drawChartBackground(width: number, height: number, padding: number) {
// 绘制坐标轴
this.context.beginPath()
this.context.strokeStyle = ‘#CCCCCC’
this.context.lineWidth = 1
this.context.moveTo(padding, padding)
this.context.lineTo(padding, height - padding)
this.context.lineTo(width - padding, height - padding)
this.context.stroke()

// 绘制网格线
const gridCount = 5
const gridStepY = (height - 2 * padding) / gridCount
const gridStepX = (width - 2 * padding) / 10

this.context.strokeStyle = '#EEEEEE'
for (let i = 0; i <= gridCount; i++) {
  const y = height - padding - i * gridStepY
  this.context.beginPath()
  this.context.moveTo(padding, y)
  this.context.lineTo(width - padding, y)
  this.context.stroke()
  
  // Y轴刻度
  this.context.fillStyle = '#666666'
  this.context.font = '12px sans-serif'
  this.context.textAlign = 'right'
  this.context.fillText(`${i * 20}`, padding - 10, y + 4)
}

// X轴刻度
for (let i = 0; i <= 10; i++) {
  const x = padding + i * gridStepX
  this.context.beginPath()
  this.context.moveTo(x, height - padding)
  this.context.lineTo(x, height - padding + 5)
  this.context.stroke()
  
  this.context.fillStyle = '#666666'
  this.context.textAlign = 'center'
  this.context.fillText(`${i * 2}s`, x, height - padding + 20)
}

}

private drawDataLine(width: number, height: number, padding: number) {
const stepX = (width - 2 * padding) / (this.data.length - 1)
const maxValue = 100

this.context.beginPath()
this.context.strokeStyle = '#4285F4'
this.context.lineWidth = 2

for (let i = 0; i < this.data.length; i++) {
  const x = padding + i * stepX
  const y = height - padding - (this.data[i] / maxValue) * (height - 2 * padding)
  
  if (i === 0) {
    this.context.moveTo(x, y)
  } else {
    this.context.lineTo(x, y)
  }
}

this.context.stroke()

}

private drawDataPoints(width: number, height: number, padding: number) {
const stepX = (width - 2 * padding) / (this.data.length - 1)
const maxValue = 100

for (let i = 0; i < this.data.length; i++) {
  const x = padding + i * stepX
  const y = height - padding - (this.data[i] / maxValue) * (height - 2 * padding)
  
  // 绘制数据点
  this.context.beginPath()
  this.context.arc(x, y, 4, 0, Math.PI * 2)
  this.context.fillStyle = '#EA4335'
  this.context.fill()
  
  // 显示数值
  if (i % 2 === 0) {
    this.context.fillStyle = '#333333'
    this.context.font = '10px sans-serif'
    this.context.textAlign = 'center'
    this.context.fillText(this.data[i].toFixed(1), x, y - 10)
  }
}

}
}
总结
鸿蒙5的ArkUI Canvas组件为开发者提供了强大的图形绘制能力,本文涵盖了:

​​基础绘图技术​​:形状、路径、文本和图像绘制
​​高级特性​​:贝塞尔曲线、图像变换、裁剪和合成
​​动画实现​​:使用requestAnimationFrame创建流畅动画
​​实战案例​​:粒子系统和动态数据图表
Canvas组件的性能经过ArkCompiler的优化,能够高效执行复杂的绘图操作。开发者可以结合这些技术:

创建自定义数据可视化图表
开发游戏和交互式图形应用
实现独特的UI效果和过渡动画
构建绘图和图像编辑工具

分类
标签
收藏
回复
举报
回复
    相关推荐