在鸿蒙HarmonyOSArkUI 框架中Canvas组件是实现高度自定义 2D 图表与报表的核心利器。它提供了与 Web Canvas 高度相似的 API让开发者能够精确控制每一个像素的绘制。一、 核心绘制流程与上下文管理Canvas 绘图并非简单的 UI 声明而是一个命令式的绘制过程需要严格遵循其生命周期。上下文初始化在组件中创建CanvasRenderingContext2D实例并通过RenderingContextSettings(true)开启抗锯齿这对图表的渲染质量至关重要。onReady回调所有绘制操作必须在Canvas的onReady回调中进行。此时画布尺寸已确定可以安全地获取context.width和context.height。数据驱动重绘图表数据变化时不应直接调用绘制函数。推荐使用Watch装饰器监听数据源在回调中先调用clearRect清空画布再执行完整的重绘逻辑确保画面一致性。// CanvasChartBase.ets Entry Component struct CanvasChartBase { // 1. 开启抗锯齿提升图表渲染质量 private settings: RenderingContextSettings new RenderingContextSettings(true); private context: CanvasRenderingContext2D new CanvasRenderingContext2D(this.settings); State Watch(redrawChart) chartData: number[] [10, 25, 18, 30, 22]; // 2. 数据变化时触发重绘 private redrawChart() { if (!this.context) return; // 清空画布 this.context.clearRect(0, 0, this.context.width, this.context.height); // 执行重绘逻辑 this.drawAxes(); this.drawData(); } private drawAxes() { /* 绘制坐标轴逻辑 */ } private drawData() { /* 绘制数据逻辑 */ } build() { // 3. 必须在 onReady 中获取宽高并执行首次绘制 Canvas(this.context) .width(100%) .height(300) .onReady(() { this.redrawChart(); }) } }二、 复杂图表绘制实战1. 雷达图坐标计算与路径绘制雷达图的核心是将多维数据映射到二维平面的极坐标系中。顶点计算根据画布中心点、半径和角度偏移量2π / 维度数利用Math.sin和Math.cos计算每个维度的顶点坐标。路径构建使用Path2D对象通过moveTo和lineTo连接各顶点最后closePath闭合形成网格或数据区域。文本定位根据顶点坐标结合文本的宽高微调fillText的绘制位置确保标签不重叠且对齐美观。2. 分时图/K线图数据映射与交互金融图表需要处理大量数据点和复杂的交互。坐标系映射定义绘图区域padding将数据值如价格通过线性插值映射到 Y 轴像素坐标将数据索引映射到 X 轴像素坐标。动态刷新使用Prop Watch监听数据数组变化增量或全量重绘。手势交互结合PanGesture和PinchGesture通过修改数据窗口的起始索引leftOffset和显示数量displayCount实现图表的左右平移和缩放。// RadarChart.ets export class RadarChart { // 雷达图顶点计算与路径构建 public static drawRadar(ctx: CanvasRenderingContext2D, data: number[], cx: number, cy: number, radius: number) { const count data.length; const angleOffset (Math.PI * 2) / count; const path new Path2D(); data.forEach((val, index) { const angle index * angleOffset - Math.PI / 2; // 从12点钟方向开始 const x cx radius * val * Math.cos(angle); const y cy radius * val * Math.sin(angle); if (index 0) { path.moveTo(x, y); } else { path.lineTo(x, y); } }); path.closePath(); ctx.strokeStyle #007DFF; ctx.lineWidth 2; ctx.stroke(path); ctx.fillStyle rgba(0, 125, 255, 0.2); ctx.fill(path); } } // TimeLineChart.ets export class TimeLineChart { // 分时图数据值 - 像素坐标映射 public static mapY(value: number, min: number, max: number, chartHeight: number, padding: number): number { const ratio (value - min) / (max - min || 1); return padding chartHeight * (1 - ratio); // Y轴翻转 } public static mapX(index: number, total: number, chartWidth: number, padding: number): number { const step chartWidth / (total - 1 || 1); return padding index * step; } }三、 性能优化Canvas 重绘成本较高不当的使用方式极易导致卡顿。避免高频重绘切勿在onActionUpdate等高频手势回调中无脑重绘。应使用节流throttle或脏标记dirty flag机制确保只在必要时重绘。离屏缓存OffscreenCanvas对于网格线、坐标轴等不随数据变化的静态元素绘制一次到OffscreenCanvas上后续通过drawImage直接贴图可大幅减少重复计算。批量绘制绘制大量相似图形如K线蜡烛、散点时先beginPath循环调用moveTo/lineTo或rect最后统一stroke或fill减少绘制指令的调用次数。入场动画使用requestAnimationFrame而非setInterval来驱动入场动画它能与屏幕刷新率同步保证动画的极致流畅。// ChartPerformance.ets export class ChartPerformance { // 1. 离屏缓存绘制静态网格线 public static createGridCache(width: number, height: number, gridCount: number): CanvasRenderingContext2D { const offscreen new OffscreenCanvas(width, height); const ctx offscreen.getContext(2d)!; ctx.strokeStyle #E0E0E0; ctx.lineWidth 0.5; for (let i 0; i gridCount; i) { const y (height / gridCount) * i; ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(width, y); ctx.stroke(); } return ctx; } // 2. 批量绘制K线蜡烛图 public static drawCandles(ctx: CanvasRenderingContext2D, candles: any[], candleWidth: number) { ctx.beginPath(); for (const c of candles) { // 合并路径减少 stroke 调用次数 ctx.rect(c.x, c.high, candleWidth, c.low - c.high); } ctx.fillStyle #FF0000; ctx.fill(); } // 3. 入场动画使用 requestAnimationFrame 保证丝滑 public static animateChart(ctx: CanvasRenderingContext2D, drawFn: (progress: number) void) { let startTime: number | null null; const duration 1000; // 1秒动画 const animate (timestamp: number) { if (!startTime) startTime timestamp; const progress Math.min((timestamp - startTime) / duration, 1); ctx.clearRect(0, 0, ctx.width, ctx.height); drawFn(progress); // 传入 0~1 的进度 if (progress 1) { requestAnimationFrame(animate); } }; requestAnimationFrame(animate); } }四、 高级渲染特效渐变、阴影与文本排版在绘制精美的报表时单调的纯色填充往往缺乏质感。Canvas 2D 提供了丰富的样式 API 来提升视觉表现力。线性与径向渐变通过createLinearGradient或createRadialGradient创建渐变对象并利用addColorStop定义颜色节点。在绘制柱状图或面积图时将渐变对象赋值给fillStyle可实现极具现代感的 UI 效果。阴影与光晕通过配置shadowOffsetX/Y、shadowBlur和shadowColor可以为图表元素添加柔和的阴影。这在深色模式下的数据看板中尤为常用能营造出数据“悬浮”的立体感。文本自适应与截断在绘制紧凑的表格或图例时需使用measureText方法动态计算文本宽度。若文本超出可用空间需手动截断并拼接省略号...防止文本溢出破坏整体布局。// ChartVisualEffects.ets export class ChartVisualEffects { // 1. 线性渐变与阴影配置 public static applyGradientAndShadow(ctx: CanvasRenderingContext2D, width: number, height: number) { // 创建从上到下的线性渐变 const gradient ctx.createLinearGradient(0, 0, 0, height); gradient.addColorStop(0, rgba(0, 125, 255, 0.8)); gradient.addColorStop(1, rgba(0, 125, 255, 0.1)); ctx.fillStyle gradient; // 配置柔和的悬浮阴影 ctx.shadowColor rgba(0, 125, 255, 0.5); ctx.shadowBlur 10; ctx.shadowOffsetX 0; ctx.shadowOffsetY 5; } // 2. 文本自适应与截断防溢出 public static drawEllipsisText( ctx: CanvasRenderingContext2D, text: string, x: number, y: number, maxWidth: number ) { let currentText text; // 动态测量文本宽度 while (ctx.measureText(currentText).width maxWidth currentText.length 0) { currentText currentText.slice(0, -1); } // 若文本被截断则拼接省略号 if (currentText.length text.length) { currentText ...; } ctx.fillText(currentText, x, y); } }五、 复杂报表场景多图表联动与动态布局企业级报表往往不是单一图表而是多图表的组合与联动。统一坐标系与网格系统对于上下对齐的柱状图与折线图必须建立统一的 X 轴刻度映射算法。确保不同图表的数据点在水平方向上严格对齐提升数据对比的准确性。响应式布局计算在onReady回调中应根据context.width动态计算各个子图表的宽高与间距。当设备横竖屏切换或折叠屏展开时通过监听尺寸变化触发重绘实现完美的自适应。联动高亮当用户触摸或鼠标悬停在某个数据点时通过全局状态变量通知所有关联图表同步绘制垂直参考线Crosshair并高亮对应数据提供沉浸式的分析体验。// LinkedChartsDemo.ets Entry Component struct LinkedChartsDemo { private settings: RenderingContextSettings new RenderingContextSettings(true); private ctxTop: CanvasRenderingContext2D new CanvasRenderingContext2D(this.settings); private ctxBottom: CanvasRenderingContext2D new CanvasRenderingContext2D(this.settings); // 全局联动状态记录当前触摸的 X 轴坐标 State touchX: number -1; build() { Column() { // 上方折线图 Canvas(this.ctxTop) .width(100%).height(150) .onReady(() { /* 绘制折线及 touchX 对应的垂直参考线 */ }) // 下方柱状图 Canvas(this.ctxBottom) .width(100%).height(150) .onReady(() { /* 绘制柱状图及 touchX 对应的垂直参考线 */ }) } // 统一的手势监听驱动所有图表联动 .onTouch((event) { if (event.type TouchType.Move || event.type TouchType.Down) { this.touchX event.touches[0].x; } else if (event.type TouchType.Up) { this.touchX -1; // 抬起手指时隐藏参考线 } }) } }六、 进阶性能优化Web Worker 与离屏渲染当报表数据量达到万级甚至十万级时主线程的计算压力会急剧增加。Web Worker 数据预处理将数据的排序、过滤、极值计算以及坐标映射等纯逻辑操作剥离到 Web Worker 线程中执行。主线程仅接收 Worker 传回的计算结果如最终的像素坐标数组进行渲染彻底杜绝因数据处理导致的 UI 掉帧。视口裁剪Viewport Culling对于超长列表或超宽图表仅计算并绘制当前屏幕可视区域内的数据元素。当用户滚动或平移时动态更新可视窗口并触发局部重绘将渲染开销降至最低。位图缓存Bitmap Caching对于极其复杂且静态的图表背景如带有大量纹理和复杂网格的底图可将其渲染至OffscreenCanvas后调用createImageBitmap转为位图缓存。后续绘制时直接使用drawImage贴图相比反复执行路径绘制指令性能可提升数倍。// AdvancedCanvasOptimization.ets export class AdvancedCanvasOptimization { private static bgBitmap: ImageBitmap | null null; // 1. 将复杂的静态背景渲染至 OffscreenCanvas 并转为位图缓存 public static async cacheStaticBackground(width: number, height: number) { const offscreen new OffscreenCanvas(width, height); const ctx offscreen.getContext(2d)!; // 执行耗时的复杂网格或纹理绘制 ctx.strokeStyle #E0E0E0; for (let i 0; i width; i 50) { ctx.beginPath(); ctx.moveTo(i, 0); ctx.lineTo(i, height); ctx.stroke(); } // 核心转换为 ImageBitmap 进行缓存 this.bgBitmap offscreen.transferToImageBitmap(); } // 2. 在主画布中直接贴图避免重复路径计算 public static drawCachedBackground(mainCtx: CanvasRenderingContext2D) { if (this.bgBitmap) { // 性能提升数倍的贴图操作 mainCtx.transferFromImageBitmap(this.bgBitmap); } } }
画布绘图:Canvas 2D绘制复杂图表与报表(190)
在鸿蒙HarmonyOSArkUI 框架中Canvas组件是实现高度自定义 2D 图表与报表的核心利器。它提供了与 Web Canvas 高度相似的 API让开发者能够精确控制每一个像素的绘制。一、 核心绘制流程与上下文管理Canvas 绘图并非简单的 UI 声明而是一个命令式的绘制过程需要严格遵循其生命周期。上下文初始化在组件中创建CanvasRenderingContext2D实例并通过RenderingContextSettings(true)开启抗锯齿这对图表的渲染质量至关重要。onReady回调所有绘制操作必须在Canvas的onReady回调中进行。此时画布尺寸已确定可以安全地获取context.width和context.height。数据驱动重绘图表数据变化时不应直接调用绘制函数。推荐使用Watch装饰器监听数据源在回调中先调用clearRect清空画布再执行完整的重绘逻辑确保画面一致性。// CanvasChartBase.ets Entry Component struct CanvasChartBase { // 1. 开启抗锯齿提升图表渲染质量 private settings: RenderingContextSettings new RenderingContextSettings(true); private context: CanvasRenderingContext2D new CanvasRenderingContext2D(this.settings); State Watch(redrawChart) chartData: number[] [10, 25, 18, 30, 22]; // 2. 数据变化时触发重绘 private redrawChart() { if (!this.context) return; // 清空画布 this.context.clearRect(0, 0, this.context.width, this.context.height); // 执行重绘逻辑 this.drawAxes(); this.drawData(); } private drawAxes() { /* 绘制坐标轴逻辑 */ } private drawData() { /* 绘制数据逻辑 */ } build() { // 3. 必须在 onReady 中获取宽高并执行首次绘制 Canvas(this.context) .width(100%) .height(300) .onReady(() { this.redrawChart(); }) } }二、 复杂图表绘制实战1. 雷达图坐标计算与路径绘制雷达图的核心是将多维数据映射到二维平面的极坐标系中。顶点计算根据画布中心点、半径和角度偏移量2π / 维度数利用Math.sin和Math.cos计算每个维度的顶点坐标。路径构建使用Path2D对象通过moveTo和lineTo连接各顶点最后closePath闭合形成网格或数据区域。文本定位根据顶点坐标结合文本的宽高微调fillText的绘制位置确保标签不重叠且对齐美观。2. 分时图/K线图数据映射与交互金融图表需要处理大量数据点和复杂的交互。坐标系映射定义绘图区域padding将数据值如价格通过线性插值映射到 Y 轴像素坐标将数据索引映射到 X 轴像素坐标。动态刷新使用Prop Watch监听数据数组变化增量或全量重绘。手势交互结合PanGesture和PinchGesture通过修改数据窗口的起始索引leftOffset和显示数量displayCount实现图表的左右平移和缩放。// RadarChart.ets export class RadarChart { // 雷达图顶点计算与路径构建 public static drawRadar(ctx: CanvasRenderingContext2D, data: number[], cx: number, cy: number, radius: number) { const count data.length; const angleOffset (Math.PI * 2) / count; const path new Path2D(); data.forEach((val, index) { const angle index * angleOffset - Math.PI / 2; // 从12点钟方向开始 const x cx radius * val * Math.cos(angle); const y cy radius * val * Math.sin(angle); if (index 0) { path.moveTo(x, y); } else { path.lineTo(x, y); } }); path.closePath(); ctx.strokeStyle #007DFF; ctx.lineWidth 2; ctx.stroke(path); ctx.fillStyle rgba(0, 125, 255, 0.2); ctx.fill(path); } } // TimeLineChart.ets export class TimeLineChart { // 分时图数据值 - 像素坐标映射 public static mapY(value: number, min: number, max: number, chartHeight: number, padding: number): number { const ratio (value - min) / (max - min || 1); return padding chartHeight * (1 - ratio); // Y轴翻转 } public static mapX(index: number, total: number, chartWidth: number, padding: number): number { const step chartWidth / (total - 1 || 1); return padding index * step; } }三、 性能优化Canvas 重绘成本较高不当的使用方式极易导致卡顿。避免高频重绘切勿在onActionUpdate等高频手势回调中无脑重绘。应使用节流throttle或脏标记dirty flag机制确保只在必要时重绘。离屏缓存OffscreenCanvas对于网格线、坐标轴等不随数据变化的静态元素绘制一次到OffscreenCanvas上后续通过drawImage直接贴图可大幅减少重复计算。批量绘制绘制大量相似图形如K线蜡烛、散点时先beginPath循环调用moveTo/lineTo或rect最后统一stroke或fill减少绘制指令的调用次数。入场动画使用requestAnimationFrame而非setInterval来驱动入场动画它能与屏幕刷新率同步保证动画的极致流畅。// ChartPerformance.ets export class ChartPerformance { // 1. 离屏缓存绘制静态网格线 public static createGridCache(width: number, height: number, gridCount: number): CanvasRenderingContext2D { const offscreen new OffscreenCanvas(width, height); const ctx offscreen.getContext(2d)!; ctx.strokeStyle #E0E0E0; ctx.lineWidth 0.5; for (let i 0; i gridCount; i) { const y (height / gridCount) * i; ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(width, y); ctx.stroke(); } return ctx; } // 2. 批量绘制K线蜡烛图 public static drawCandles(ctx: CanvasRenderingContext2D, candles: any[], candleWidth: number) { ctx.beginPath(); for (const c of candles) { // 合并路径减少 stroke 调用次数 ctx.rect(c.x, c.high, candleWidth, c.low - c.high); } ctx.fillStyle #FF0000; ctx.fill(); } // 3. 入场动画使用 requestAnimationFrame 保证丝滑 public static animateChart(ctx: CanvasRenderingContext2D, drawFn: (progress: number) void) { let startTime: number | null null; const duration 1000; // 1秒动画 const animate (timestamp: number) { if (!startTime) startTime timestamp; const progress Math.min((timestamp - startTime) / duration, 1); ctx.clearRect(0, 0, ctx.width, ctx.height); drawFn(progress); // 传入 0~1 的进度 if (progress 1) { requestAnimationFrame(animate); } }; requestAnimationFrame(animate); } }四、 高级渲染特效渐变、阴影与文本排版在绘制精美的报表时单调的纯色填充往往缺乏质感。Canvas 2D 提供了丰富的样式 API 来提升视觉表现力。线性与径向渐变通过createLinearGradient或createRadialGradient创建渐变对象并利用addColorStop定义颜色节点。在绘制柱状图或面积图时将渐变对象赋值给fillStyle可实现极具现代感的 UI 效果。阴影与光晕通过配置shadowOffsetX/Y、shadowBlur和shadowColor可以为图表元素添加柔和的阴影。这在深色模式下的数据看板中尤为常用能营造出数据“悬浮”的立体感。文本自适应与截断在绘制紧凑的表格或图例时需使用measureText方法动态计算文本宽度。若文本超出可用空间需手动截断并拼接省略号...防止文本溢出破坏整体布局。// ChartVisualEffects.ets export class ChartVisualEffects { // 1. 线性渐变与阴影配置 public static applyGradientAndShadow(ctx: CanvasRenderingContext2D, width: number, height: number) { // 创建从上到下的线性渐变 const gradient ctx.createLinearGradient(0, 0, 0, height); gradient.addColorStop(0, rgba(0, 125, 255, 0.8)); gradient.addColorStop(1, rgba(0, 125, 255, 0.1)); ctx.fillStyle gradient; // 配置柔和的悬浮阴影 ctx.shadowColor rgba(0, 125, 255, 0.5); ctx.shadowBlur 10; ctx.shadowOffsetX 0; ctx.shadowOffsetY 5; } // 2. 文本自适应与截断防溢出 public static drawEllipsisText( ctx: CanvasRenderingContext2D, text: string, x: number, y: number, maxWidth: number ) { let currentText text; // 动态测量文本宽度 while (ctx.measureText(currentText).width maxWidth currentText.length 0) { currentText currentText.slice(0, -1); } // 若文本被截断则拼接省略号 if (currentText.length text.length) { currentText ...; } ctx.fillText(currentText, x, y); } }五、 复杂报表场景多图表联动与动态布局企业级报表往往不是单一图表而是多图表的组合与联动。统一坐标系与网格系统对于上下对齐的柱状图与折线图必须建立统一的 X 轴刻度映射算法。确保不同图表的数据点在水平方向上严格对齐提升数据对比的准确性。响应式布局计算在onReady回调中应根据context.width动态计算各个子图表的宽高与间距。当设备横竖屏切换或折叠屏展开时通过监听尺寸变化触发重绘实现完美的自适应。联动高亮当用户触摸或鼠标悬停在某个数据点时通过全局状态变量通知所有关联图表同步绘制垂直参考线Crosshair并高亮对应数据提供沉浸式的分析体验。// LinkedChartsDemo.ets Entry Component struct LinkedChartsDemo { private settings: RenderingContextSettings new RenderingContextSettings(true); private ctxTop: CanvasRenderingContext2D new CanvasRenderingContext2D(this.settings); private ctxBottom: CanvasRenderingContext2D new CanvasRenderingContext2D(this.settings); // 全局联动状态记录当前触摸的 X 轴坐标 State touchX: number -1; build() { Column() { // 上方折线图 Canvas(this.ctxTop) .width(100%).height(150) .onReady(() { /* 绘制折线及 touchX 对应的垂直参考线 */ }) // 下方柱状图 Canvas(this.ctxBottom) .width(100%).height(150) .onReady(() { /* 绘制柱状图及 touchX 对应的垂直参考线 */ }) } // 统一的手势监听驱动所有图表联动 .onTouch((event) { if (event.type TouchType.Move || event.type TouchType.Down) { this.touchX event.touches[0].x; } else if (event.type TouchType.Up) { this.touchX -1; // 抬起手指时隐藏参考线 } }) } }六、 进阶性能优化Web Worker 与离屏渲染当报表数据量达到万级甚至十万级时主线程的计算压力会急剧增加。Web Worker 数据预处理将数据的排序、过滤、极值计算以及坐标映射等纯逻辑操作剥离到 Web Worker 线程中执行。主线程仅接收 Worker 传回的计算结果如最终的像素坐标数组进行渲染彻底杜绝因数据处理导致的 UI 掉帧。视口裁剪Viewport Culling对于超长列表或超宽图表仅计算并绘制当前屏幕可视区域内的数据元素。当用户滚动或平移时动态更新可视窗口并触发局部重绘将渲染开销降至最低。位图缓存Bitmap Caching对于极其复杂且静态的图表背景如带有大量纹理和复杂网格的底图可将其渲染至OffscreenCanvas后调用createImageBitmap转为位图缓存。后续绘制时直接使用drawImage贴图相比反复执行路径绘制指令性能可提升数倍。// AdvancedCanvasOptimization.ets export class AdvancedCanvasOptimization { private static bgBitmap: ImageBitmap | null null; // 1. 将复杂的静态背景渲染至 OffscreenCanvas 并转为位图缓存 public static async cacheStaticBackground(width: number, height: number) { const offscreen new OffscreenCanvas(width, height); const ctx offscreen.getContext(2d)!; // 执行耗时的复杂网格或纹理绘制 ctx.strokeStyle #E0E0E0; for (let i 0; i width; i 50) { ctx.beginPath(); ctx.moveTo(i, 0); ctx.lineTo(i, height); ctx.stroke(); } // 核心转换为 ImageBitmap 进行缓存 this.bgBitmap offscreen.transferToImageBitmap(); } // 2. 在主画布中直接贴图避免重复路径计算 public static drawCachedBackground(mainCtx: CanvasRenderingContext2D) { if (this.bgBitmap) { // 性能提升数倍的贴图操作 mainCtx.transferFromImageBitmap(this.bgBitmap); } } }