
鸿蒙5 ArkUI Canvas组件创意开发:绘制自定义图形实战
鸿蒙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准备就绪后再绘制
绘制基本形状(矩形、圆形、线条)
设置样式(颜色、线宽、字体)
绘制文本内容
高级绘图技术
- 路径绘制与贝塞尔曲线
// 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效果和过渡动画
构建绘图和图像编辑工具
