HarmonyOS技术精讲-ArkGraphics 2D方舟2D图形服务第五篇色彩与风格——渐变、阴影与滤镜效果开篇别让画面太“素”HarmonyOS NEXT 开发中画布 API 的渐变、阴影和滤镜能力很多人只是简单用过。但实际开发时你会发现官方示例里一个渐变圆画得很漂亮但放到真实项目中阴影没出来、滤镜不生效、合成模式结果和预期完全相反——这些问题并不少见。这篇文章主要解决三个问题渐变线性渐变和径向渐变在 ArkUI Canvas 下的正确使用方式特别是addColorStop的坐标计算。阴影shadowBlur、shadowOffset在实际应用中的坑比如为什么阴影在真机上会消失。滤镜与合成filter和globalCompositeOperation的常见误用以及如何做照片滤镜和镂空效果。目标很明确你跟着做完能直接在项目里用。它解决什么问题先搞清楚本质。能力解决什么问题适用场景不适用场景渐变Linear/Radial用颜色过渡代替纯色填充增加立体感和质感按钮背景、进度条、仪表盘、弹窗遮罩简单色块、大量渐变叠加CPU开销大阴影模拟光照下的投影或发光效果文字标题、图标漂浮感、卡片层级区分动态高频刷新场景阴影触发重绘慢滤镜对已绘制的像素进行后处理模糊、亮度、对比度图片美化、实时滤镜效果、模糊背景高精度图像处理Canvas filter精度有限合成模式控制新绘图形与已有内容的混合方式遮罩、相册叠加效果、切片叠影复杂颜色叠加运算结果难预测ArkGraphics 2D 的 Canvas 能力基本对标 Web Canvas 的 2D 接口但在状态管理上和 ArkUI 的组件刷新机制有绑定所以有些行为需要额外注意。环境说明DevEco Studio 版本DevEco Studio 6.1.0 及以上 HarmonyOS SDK 版本HarmonyOS 6.1.0(23) 及以上 目标设备手机 / 平板核心实现我们从最简单的渐变开始。1. 基础渐变线性渐变首先创建一个 Canvas 组件然后在其onReady()回调中获取CanvasRenderingContext2D。// 入口页面 Index.etsEntryComponentstruct Index{privatesettings:RenderingContextSettingsnewRenderingContextSettings(true)privatecontext:CanvasRenderingContext2DnewCanvasRenderingContext2D(this.settings)build(){Row(){Canvas(this.context).width(100%).height(100%).onReady((){this.drawLinearGradient()})}.width(100%).height(100%).backgroundColor(#F5F5F5)}drawLinearGradient(){constctxthis.context// 1. 创建渐变对象定义起始和结束坐标constgradientctx.createLinearGradient(50,50,250,250)// 2. 设置颜色过渡点gradient.addColorStop(0,#FF6B6B)// 起始色gradient.addColorStop(0.5,#FFE66D)// 中间色gradient.addColorStop(1,#4ECDC4)// 最终色// 3. 应用为填充样式ctx.fillStylegradient ctx.fillRect(50,50,200,200)}}关键说明createLinearGradient(x0, y0, x1, y1)的坐标是相对于 Canvas 左上角原点而不是相对填充图形位置。很多人以为addColorStop传百分比是相对矩形实际上它是相对渐变的整体插值范围。如果你想让渐变从矩形左上角到右下角渐变的起止坐标应该和矩形顶点对齐而不是随意写。addColorStop的第一个参数是 0 到 1 的浮点数代表渐变插值位置。超出范围的会被截断。2. 径向渐变做一个3D球体径向渐变常用来模拟球体的光照效果。// 画一个3D球体drawSphere(ctx:CanvasRenderingContext2D,cx:number,cy:number,r:number){// 径向渐变的参数起始圆心、起始半径、终止圆心、终止半径// 这里起始圆心偏向左上制造光源效果constradGradctx.createRadialGradient(cx-r*0.3,cy-r*0.3,r*0.1,// 光源高光点位置及小半径cx,cy,r// 球体中心及半径)radGrad.addColorStop(0,#FFFFFF)// 高光白色radGrad.addColorStop(0.3,#FF6B6B)// 中间过渡radGrad.addColorStop(1,#8B0000)// 暗部深红ctx.beginPath()ctx.arc(cx,cy,r,0,Math.PI*2)ctx.fillStyleradGrad ctx.fill()}注意事项径向渐变的两个圆如果起始半径和终止半径比例不对容易产生“光圈”效应看起来不像球体。经验值起始半径是终止半径的 1/10 到 1/5。起始圆心的偏移量决定了光照方向。偏移量太大高光过于极端太小球体太平。3. 阴影效果发光文字阴影属性在 Canvas 上是全局状态一旦设置后面所有绘制操作都会带上阴影直到重置。drawGlowText(ctx:CanvasRenderingContext2D){// 设置阴影ctx.shadowColor#00BFFF// 阴影颜色发光色ctx.shadowBlur20// 模糊程度ctx.shadowOffsetX0ctx.shadowOffsetY0ctx.fontbold 48px sans-serifctx.fillStyle#FFFFFFctx.textBaselinetopctx.fillText(HarmonyOS,50,100)// 重置阴影避免影响后续绘制ctx.shadowColortransparentctx.shadowBlur0ctx.shadowOffsetX0ctx.shadowOffsetY0}真实项目中的坑在onReady回调里绘制一次没问题。如果页面有状态刷新触发重新绘制比如配合State变量重绘你会发现阴影可能不见了。原因是 ArkUI 的 Canvas 上下文在某些场景下会被重新创建而shadowColor这样的属性在重建后不会保持旧值。所以每次onReady或重绘时务必重新设置所有上下文属性。4. 滤镜照片滤镜同时还可以使用滤镜操作。// 应用滤镜照片效果applyPhotoFilter(ctx:CanvasRenderingContext2D){// 假设已经画好一张图片或内容// 设置滤镜ctx.filterbrightness(1.2) contrast(1.5) saturate(0.8)// 在这个滤镜作用下绘制内容ctx.drawImage(imageBitmap,0,0,300,300)// 清除滤镜避免影响后续ctx.filternone}参数说明滤镜函数作用参数范围典型值blur(px)高斯模糊0 ~ 20pxbrightness(ratio)亮度0 ~ 21为原图contrast(ratio)对比度0 ~ 2saturate(ratio)饱和度0 ~ 21为原图grayscale(ratio)灰度0 ~ 11完全灰度sepia(ratio)怀旧色0 ~ 1注意filter属性在 HarmonyOS NEXT 上支持度不错但多个滤镜链式调用时性能下降明显。如果只是简单的亮度调整建议用globalCompositeOperation替代。滤镜绘制完成后一定要重置为none否则后续所有绘制都会带滤镜。5. 合成模式镂空效果globalCompositeOperation控制新绘制的图形如何与已有内容混合。drawCutout(ctx:CanvasRenderingContext2D){// 先画一个蓝色矩形作为背景ctx.fillStyle#0000FFctx.fillRect(50,50,200,200)// 设置合成模式为 destination-out新图形覆盖的区域变透明ctx.globalCompositeOperationdestination-out// 在新的模式下画一个圆ctx.beginPath()ctx.arc(150,150,60,0,Math.PI*2)ctx.fillStyle#FF0000// 颜色无所谓destination-out 只看形状ctx.fill()// 重置合成模式ctx.globalCompositeOperationsource-over}这段代码运行后蓝色矩形中间会有一个圆形透明区域实现镂空效果。常见模式对比模式效果source-over默认新图形覆盖旧图形destination-out新图形区域使旧图形透明source-atop新图形只绘制在旧图形存在的区域lighter颜色叠加变亮multiply正片叠底变暗踩坑记录坑1全局Shadow在真机上部分机型不生效现象模拟器上阴影正常显示但在部分真机上尤其是中低端设备阴影完全消失只有图形本身。原因Canvas 的阴影渲染在 GPU 上依赖硬件加速。部分设备在低功耗模式下Canvas 硬件加速被降级或者shadowBlur值过大导致超出硬件纹理限制。官方文档提到阴影最大模糊半径是 150px但实际测试中超过 100px 就会在某些设备上不渲染。解决方案将shadowBlur控制在 80px 以内。如果必须大模糊半径改用filter: blur()方式实现发光效果更稳定。在onReady回调中显式设置 Canvas 的硬件加速属性如果有或通过context读取shadowBlur是否被正确设置。坑2Canvas.drawImage 不支持传入 PixelMap 对象现象想用滤镜处理一张图片但drawImage只支持ImageBitmap类型不支持直接传PixelMap。原因ArkUI 的 Canvas API 设计时drawImage的参数来源仅限于ImageBitmap通过createImageBitmap创建。从ohos.multimedia.image获取的 PixelMap 并不能直接使用。解决方案使用image.createImageBitmap(pixelMap)转换。注意createImageBitmap是异步的需要在回调中绘制。importimagefromohos.multimedia.imageasyncfunctiondrawImageFromPixelMap(ctx:CanvasRenderingContext2D,pixelMap:image.PixelMap){constbitmapawaitimage.createImageBitmap(pixelMap)ctx.filterbrightness(1.5) contrast(1.2)ctx.drawImage(bitmap,0,0,300,300)ctx.filternone}最佳实践不要在build()中频繁创建 Canvas 相关对象。// ❌ 错误示例在 build 中每次刷新都新建build(){Canvas(newCanvasRenderingContext2D(this.settings))// 每次刷新都会新建}// ✅ 正确做法context 在组件外创建一次privatecontext:CanvasRenderingContext2DnewCanvasRenderingContext2D(this.settings)原因build()在状态变化时会频繁执行每次 new 都会销毁旧的上下文导致已绘制的图形丢失。每次绘制前显式设置阴影、滤镜等全局属性。不要假设上一步重置了状态。ArkUI Canvas 的全局属性在两次onReady之间可能不保持。安全做法是在draw函数开头完整设置一次。滤镜在实时绘制场景中优先使用更轻量的方案。例如动态亮度变化用brightness滤镜每帧重绘CPU 消耗较高。如果只是固定滤镜效果在首次绘制时初始化一次即可不需要每帧都设置。Demo 入口所有代码集中在Index.ets文件中包含以下效果线性渐变矩形径向渐变3D球体发光文字照片滤镜模糊调色镂空合成模式EntryComponentstruct Index{privatesettings:RenderingContextSettingsnewRenderingContextSettings(true)privatecontext:CanvasRenderingContext2DnewCanvasRenderingContext2D(this.settings)build(){Scroll(){Column(){Canvas(this.context).width(100%).height(800).onReady((){this.drawAll()})}}.width(100%).height(100%).backgroundColor(#2C3E50)}drawAll(){constctxthis.context ctx.clearRect(0,0,400,800)this.drawLinearGradient(ctx)this.drawSphere(ctx,120,300,80)this.drawGlowText(ctx)this.drawPhotoFilter(ctx)this.drawCutout(ctx)}// 其他 draw 方法参见上文}FAQQ为什么我设置了shadowBlur但文字阴影没有模糊效果只有实心色块A检查shadowColor是否设置了非透明颜色。如果shadowColor是white且背景也是白色阴影就看不见。另外shadowBlur的值不宜过大超过 100 在某些设备上会被忽略。Qfilter和globalCompositeOperation可以一起用吗A技术上可以但顺序会影响结果。filter是对当前绘图操作施加滤镜而globalCompositeOperation是控制新绘制的图形与画布已有内容的混合。建议先画完无滤镜内容再使用globalCompositeOperation进行合成避免滤镜干扰混合结果。Q为什么模拟器和真机的阴影颜色不一样A模拟器的 GPU 驱动和真机不同部分设备的阴影渲染颜色校准存在差异。建议以真机为准并在真机上测试所有阴影效果的视觉一致性。源码示例代码地址项目地址