》第一篇:初识ArkGraphics 2D——Canvas组件与基础绘制入门)
HarmonyOS技术精讲-ArkGraphics 2D方舟2D图形服务第一篇初识ArkGraphics 2D——Canvas组件与基础绘制入门1. 开篇从绘制一个矩形开始在HarmonyOS NEXT的应用开发中如果只是展示图片直接上Image组件就够了。但一旦涉及到动态图表、游戏界面、自定义仪表盘或者图表标注时你会发现没有Canvas还真不行。很多开发者第一次接触ArkGraphics 2D时第一反应是“这不就是安卓的Canvas吗” 但实际用起来会发现HarmonyOS的Canvas虽然功能类似但在接口设计、生命周期管理、以及和ArkUI状态绑定的方式上都有自己的“脾气”。官方文档虽然给出了基本API但对于初学者来说缺少一个能直接运行的、从零搭建的完整示例隐藏的坑不少。这篇文章不会讲复杂的三维渲染或粒子系统只聚焦一个目标让你在页面上成功落下一个“像素”并画出点、线、矩形、圆和文字。搞定这些2D绘图的大门就打开了。2. ArkGraphics 2D到底解决什么问题简单来说ArkGraphics 2D提供了一个2D渲染上下文允许开发者通过编程方式精确控制屏幕上每个像素的绘制。它解决的场景非常明确数据可视化绘制折线图、饼图、柱状图。游戏与交互实现小型游戏画面、自定义手势反馈。图像编辑实现简单的绘图板、图像裁剪标注。UI特效绘制复杂的不规则形状、渐变背景。不适合的场景如果你只是展示静态图片或简单的文字排版Image和Text组件更合适。它们有更好的性能优化如硬件加速但Canvas的渲染路径也需要手动优化并且天然支持无障碍服务。用Canvas做全界面文字渲染性能会成问题也不利于盲人用户使用。与类似方案对比方案适用场景性能易用性推荐程度ArkGraphics 2D (Canvas)动态、程序化、复杂自定义图形中需手动优化中需要理解绘制上下文⭐️⭐️⭐️⭐️⭐️第一选择Image组件显示静态图片高硬件加速高⭐️⭐️⭐️⭐️Shape/Path组件属于SVG类似适合静态简单矢量图高框架层优化高声明式⭐️⭐️⭐️⭐️xcomponent 自定义渲染引擎高性能图形/3D极高 (GPU)极低⭐️⭐️ (特定场景)绝大多数情况下Canvas是处理程序化、动态、复杂2D图形的首选方案。3. 环境说明DevEco Studio 版本DevEco Studio 6.1.0 及以上HarmonyOS SDK 版本HarmonyOS 6.1.0(23) 及以上目标设备手机语言ArkTS支持API 124. 核心实现创建一个能画画的页面我们将一步步构建一个页面包含一个全屏Canvas并在上面绘制点、线、矩形、圆和文本。4.1 创建 Canvas 组件首先在Component里创建一个Canvas组件。它需要一个context对象这个对象就是我们的“画笔”——CanvasRenderingContext2D。// pages/Index.etsEntryComponentstruct Index{// 1. 引用Canvas组件通过这个引用可以获取到CanvasRenderingContext2DprivatecanvasRef:CanvasRenderingContext2DnewCanvasRenderingContext2D();build(){Column(){// 2. 创建Canvas组件并将canvasRef绑定给它Canvas(this.canvasRef).width(100%).height(100%).backgroundColor(#FFFFFF)// 设置白色背景.onReady((){// 3. Canvas准备就绪后开始画图this.drawAll();})}.width(100%).height(100%)}drawAll(){// 获取上下文其实就是 this.canvasRefconstctxthis.canvasRef;// 在下一节我们将填充这个函数}}关键点CanvasRenderingContext2D是画布的核心所有绘制指令都通过它执行。.onReady()回调非常关键。Canvas必须在组件创建并布局完成后才能绘制。如果不等待onReady直接调用drawAll会出现绘制无效的情况。这是初学者最容易犯的错误之一。4.2 绘制基础形状现在我们在drawAll函数中添加绘制逻辑。官方文档重点提到了drawRect、drawCircle、drawText以及fillStyle、strokeStyle。drawAll(){constctxthis.canvasRef;if(!ctx)return;// 安全判断// --- 1. 绘制一个填充的红色矩形 ---ctx.fillStyle#FF0000;// 设置填充颜色ctx.fillRect(50,50,150,100);// drawRect和fillRect等价这里用fillRect更直观// 等价于 ctx.drawRect(50, 50, 150, 100);// --- 2. 绘制一个空心蓝色矩形边框 ---ctx.strokeStyle#0000FF;// 设置边框颜色ctx.lineWidth4;// 设置边框粗细ctx.strokeRect(250,50,150,100);// 绘制边框矩形// --- 3. 绘制一个填充的绿色圆形 ---ctx.fillStyle#00FF00;ctx.beginPath();// 开始新路径ctx.arc(440,100,50,0,Math.PI*2);// 圆心(440,100)半径50从0到2Πctx.fill();// 填充// --- 4. 绘制一条线段从起点到终点 ---ctx.strokeStyle#FF00FF;// 粉色ctx.lineWidth6;ctx.beginPath();ctx.moveTo(50,220);// 画笔移动到起点ctx.lineTo(300,220);// 绘制到终点ctx.stroke();// 描边// --- 5. 绘制带样式的文字 ---ctx.font30px sans-serif;// 字体大小和字体系列ctx.fillStyle#000000;// 黑色文字ctx.fillText(Hello ArkGraphics 2D,50,350);// 填充文本// --- 6. 绘制一个像素点通过绘制小矩形实现drawPoint在实际项目中更复杂 ---ctx.fillStyle#333333;ctx.fillRect(380,350,1,1);// 一个1x1的像素// --- 7. 擦除特定区域 ---// ctx.clearRect(0, 0, 500, 500); // 擦除整个canvas如果执行上面画的都会消失}关键点fillStyle/strokeStyle会影响后续所有绘制操作直到你再次修改。所以在绘制不同形状前需要先设置好。beginPath()至关重要。只要你绘制图形前不调用beginPath()新的路径就会和上一段路径连在一起导致绘制结果异常例如两个图形之间的线会被连接起来。这是一个高频踩坑点。arc()的用法很标准arc(x, y, 半径, 起始角度, 结束角度, 是否逆时针)。0和Math.PI * 2代表一个完整的圆。fillRect和strokeRect是独立的API分别对应“填充矩形”和“描边矩形”。drawRect和fillRect作用相同推荐优先使用fill开头的方法意图更明确。4.3 完整的可运行代码将以上代码合并得到一个完整可运行的页面。// pages/Index.etsEntryComponentstruct Index{privatecanvasRef:CanvasRenderingContext2DnewCanvasRenderingContext2D();build(){Column(){Canvas(this.canvasRef).width(100%).height(100%).backgroundColor(#FFFFFF).onReady((){this.drawAll();})}.width(100%).height(100%)}drawAll(){constctxthis.canvasRef;if(!ctx)return;// 1. 填充红色矩形ctx.fillStyle#FF0000;ctx.fillRect(50,50,150,100);// 2. 空心蓝色矩形边框ctx.strokeStyle#0000FF;ctx.lineWidth4;ctx.strokeRect(250,50,150,100);// 3. 填充绿色圆形ctx.fillStyle#00FF00;ctx.beginPath();ctx.arc(440,100,50,0,Math.PI*2);ctx.fill();// 4. 粉色线段ctx.strokeStyle#FF00FF;ctx.lineWidth6;ctx.beginPath();ctx.moveTo(50,220);ctx.lineTo(300,220);ctx.stroke();// 5. 填充文字ctx.font30px sans-serif;ctx.fillStyle#000000;ctx.fillText(Hello ArkGraphics 2D,50,350);// 6. 绘制一个像素点ctx.fillStyle#333333;ctx.fillRect(380,350,1,1);// 7. 绘制一个点另一种更专业的方式使用 drawPoint但需要更复杂的上下文设置// ctx.drawPoint(390, 350, 5); // drawPoint 的第三个参数是点的大小不填默认是1}}运行效果屏幕上会显示一个红色矩形、一个蓝色边框矩形、一个绿色圆形、一条粉色线段、一行黑色文字以及一个灰色的像素点。5. 踩坑记录这是给新手的避坑指南坑1Canvas 宽度设置失效图形显示不全或偏移现象你设置了Canvas(this.canvasRef).width(100%)然后ctx.fillRect(50, 50, 150, 100)但在真机上图形可能显示在左上角或者部分在画布外。原因width(100%)是声明式写法绑定的是父容器的百分比。但在onReady回调触发时Canvas的实际像素宽度逻辑像素可能还未被最终确定或者你绘制的坐标是基于第一次布局的物理尺寸。如果你的父容器布局发生变化例如键盘弹起Canvas尺寸会变但你的绘制坐标是写死的。解法在onReady中先获取 Canvas 的准确尺寸再进行绘制。.onReady((){// 获取Canvas组件的实际宽高constcanvasthis.canvasRef;constwidthcanvas.width;// 这是Canvas的逻辑像素宽度constheightcanvas.height;// 这是Canvas的逻辑像素高度// 现在你可以根据 width 和 height 动态计算坐标this.drawAll(width,height);})如果Canvas尺寸在页面加载后变化你需要监听onAreaChange事件并在onAreaChange中重新绘制。坑2beginPath()漏写导致图形错乱现象你画了一个圆arc紧接着画了一条线lineTo然后发现圆和线之间有一条莫名其妙的直线连接着或者下一个图形覆盖了上一个图形的描边。原因Canvas的绘图模型是基于“路径”的。如果你不调用beginPath()下一次moveTo或lineTo会建立在上一次路径的末尾。例如画完圆后画笔停在圆的最后一个点你直接moveTo(0,0)然后lineTo(100,100)实际上是画了从圆上某点到(100,100)的线。解法绘制每个独立图形前强制调用ctx.beginPath()。这是一个好习惯。ctx.beginPath();// 每次画新图形前必写ctx.arc(...);ctx.fill();ctx.beginPath();// 画线前再写一次ctx.moveTo(...);ctx.lineTo(...);ctx.stroke();6. 最佳实践不要在build()中频繁创建对象CanvasRenderingContext2D实例应该在Component中创建如果用Canvas(this.canvasRef)的方式或者使用State装饰器管理。千万不要在build里写new CanvasRenderingContext2D()ArkUI会频繁触发组件重建导致画布不断重绘性能极差。把样式配置抽成函数如果你的页面需要绘制多种颜色、线型的图形不要在每个绘制步骤里重复写ctx.fillStyle #000; ctx.strokeStyle #FFF;。抽取一个函数比如applyStyle(style: {fill: string, stroke: string, lineWidth: number})接收配置对象进行设置。这样代码更清晰也方便复用。异步回调里不要直接修改Canvas的UI状态比如你在网络请求的回调里想去给Canvas重新绘制。但此时页面可能已经销毁了例如用户返回了上一个页面直接操作ctx可能会导致崩溃。一定要检查Canvas的引用是否有效或者在onDestroy生命周期中清理掉对ctx的外部引用。7. Demo 入口把你的Index.ets文件替换成上面的代码直接运行即可。这是一个最简单的入门示例。// entry/src/main/ets/pages/Index.ets// 整体代码如上所示8. FAQ真实开发视角Q为什么真机显示正常但模拟器上绘制的文字显示不全A模拟器和真机的字体文件可能不同。在模拟器上sans-serif可能被映射成一个不支持中文的字体或者字号渲染机制有差异。建议真机测试并在font设置中明确指定字体例如ctx.font 30px HarmonyOS Sans SC。Q为什么页面返回后再重新进入Canvas 是空的A因为页面被销毁onPageHide后Canvas组件也被销毁。再次进入时onReady会再次执行你需要确保你的绘制数据比如绘制参数没有被销毁。建议使用State或AppStorage存储数据在onReady中恢复绘制。Q为什么我的Canvas.clearRect()清除区域后页面变黑了ACanvas.clearRect()会把指定矩形区域内的像素设置为透明全透明黑色。如果你的Canvas没有设置背景色backgroundColor就会看到黑色。解决方案在Canvas组件上设置.backgroundColor(#FFFFFF)或者使用ctx.fillStyle #FFFFFF; ctx.fillRect(0, 0, canvas.width, canvas.height)来绘制背景色。示例代码地址项目地址如果你也遇到类似问题可以重点检查Canvas的onReady时机和beginPath的调用位置。这一点在实际项目调试中会反复出现。