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.ets@Entry@Componentstruct Index{privatesettings:RenderingContextSettings=newRenderingContextSettings(true)privatecontext:CanvasRenderingContext2D=newCanvasRenderingContext2D(this.settings)build(){Row(){Canvas(this.context).width('100%').height('100%').onReady(()=>{this.drawLinearGradient()})}.width('100%').height('100%').backgroundColor('#F5F5F5')}drawLinearGradient(){constctx=this.context// 1. 创建渐变对象,定义起始和结束坐标constgradient=ctx.createLinearGradient(50,50,250,250)// 2. 设置颜色过渡点gradient.addColorStop(0,'#FF6B6B')// 起始色gradient.addColorStop(0.5,'#FFE66D')// 中间色gradient.addColorStop(1,'#4ECDC4')// 最终色// 3. 应用为填充样式ctx.fillStyle=gradient 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){// 径向渐变的参数:起始圆心、起始半径、终止圆心、终止半径// 这里起始圆心偏向左上,制造光源效果constradGrad=ctx.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.fillStyle=radGrad ctx.fill()}注意事项:
- 径向渐变的两个圆,如果起始半径和终止半径比例不对,容易产生“光圈”效应,看起来不像球体。经验值:起始半径是终止半径的 1/10 到 1/5。
- 起始圆心的偏移量决定了光照方向。偏移量太大,高光过于极端;太小,球体太平。
3. 阴影效果:发光文字
阴影属性在 Canvas 上是全局状态,一旦设置,后面所有绘制操作都会带上阴影,直到重置。
drawGlowText(ctx:CanvasRenderingContext2D){// 设置阴影ctx.shadowColor='#00BFFF'// 阴影颜色(发光色)ctx.shadowBlur=20// 模糊程度ctx.shadowOffsetX=0ctx.shadowOffsetY=0ctx.font='bold 48px sans-serif'ctx.fillStyle='#FFFFFF'ctx.textBaseline='top'ctx.fillText('HarmonyOS',50,100)// 重置阴影,避免影响后续绘制ctx.shadowColor='transparent'ctx.shadowBlur=0ctx.shadowOffsetX=0ctx.shadowOffsetY=0}真实项目中的坑:
在onReady回调里绘制一次没问题。如果页面有状态刷新触发重新绘制(比如配合@State变量重绘),你会发现阴影可能不见了。原因是 ArkUI 的 Canvas 上下文在某些场景下会被重新创建,而shadowColor这样的属性在重建后不会保持旧值。所以每次onReady或重绘时,务必重新设置所有上下文属性。
4. 滤镜:照片滤镜
同时还可以使用滤镜操作。
// 应用滤镜:照片效果applyPhotoFilter(ctx:CanvasRenderingContext2D){// 假设已经画好一张图片或内容// 设置滤镜ctx.filter='brightness(1.2) contrast(1.5) saturate(0.8)'// 在这个滤镜作用下绘制内容ctx.drawImage(imageBitmap,0,0,300,300)// 清除滤镜,避免影响后续ctx.filter='none'}参数说明:
| 滤镜函数 | 作用 | 参数范围(典型值) |
|---|---|---|
blur(px) | 高斯模糊 | 0 ~ 20px |
brightness(ratio) | 亮度 | 0 ~ 2(1为原图) |
contrast(ratio) | 对比度 | 0 ~ 2 |
saturate(ratio) | 饱和度 | 0 ~ 2(1为原图) |
grayscale(ratio) | 灰度 | 0 ~ 1(1完全灰度) |
sepia(ratio) | 怀旧色 | 0 ~ 1 |
注意:
filter属性在 HarmonyOS NEXT 上支持度不错,但多个滤镜链式调用时,性能下降明显。如果只是简单的亮度调整,建议用globalCompositeOperation替代。- 滤镜绘制完成后,一定要重置为
'none',否则后续所有绘制都会带滤镜。
5. 合成模式:镂空效果
globalCompositeOperation控制新绘制的图形如何与已有内容混合。
drawCutout(ctx:CanvasRenderingContext2D){// 先画一个蓝色矩形作为背景ctx.fillStyle='#0000FF'ctx.fillRect(50,50,200,200)// 设置合成模式为 'destination-out':新图形覆盖的区域变透明ctx.globalCompositeOperation='destination-out'// 在新的模式下,画一个圆ctx.beginPath()ctx.arc(150,150,60,0,Math.PI*2)ctx.fillStyle='#FF0000'// 颜色无所谓,'destination-out' 只看形状ctx.fill()// 重置合成模式ctx.globalCompositeOperation='source-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是否被正确设置。
坑2:Canvas.drawImage 不支持传入 PixelMap 对象
现象:想用滤镜处理一张图片,但drawImage只支持ImageBitmap类型,不支持直接传PixelMap。
原因:
ArkUI 的 Canvas API 设计时,drawImage的参数来源仅限于ImageBitmap(通过createImageBitmap创建)。从@ohos.multimedia.image获取的 PixelMap 并不能直接使用。
解决方案:
使用image.createImageBitmap(pixelMap)转换。注意createImageBitmap是异步的,需要在回调中绘制。
importimagefrom'@ohos.multimedia.image'asyncfunctiondrawImageFromPixelMap(ctx:CanvasRenderingContext2D,pixelMap:image.PixelMap){constbitmap=awaitimage.createImageBitmap(pixelMap)ctx.filter='brightness(1.5) contrast(1.2)'ctx.drawImage(bitmap,0,0,300,300)ctx.filter='none'}最佳实践
不要在
build()中频繁创建 Canvas 相关对象。// ❌ 错误示例:在 build 中每次刷新都新建build(){Canvas(newCanvasRenderingContext2D(this.settings))// 每次刷新都会新建}// ✅ 正确做法:context 在组件外创建一次privatecontext:CanvasRenderingContext2D=newCanvasRenderingContext2D(this.settings)原因:
build()在状态变化时会频繁执行,每次 new 都会销毁旧的上下文,导致已绘制的图形丢失。每次绘制前,显式设置阴影、滤镜等全局属性。
不要假设上一步重置了状态。ArkUI Canvas 的全局属性在两次
onReady之间可能不保持。安全做法是在draw函数开头完整设置一次。滤镜在实时绘制场景中,优先使用更轻量的方案。
例如动态亮度变化,用
brightness滤镜每帧重绘,CPU 消耗较高。如果只是固定滤镜效果,在首次绘制时初始化一次即可,不需要每帧都设置。
Demo 入口
所有代码集中在Index.ets文件中,包含以下效果:
- 线性渐变矩形
- 径向渐变3D球体
- 发光文字
- 照片滤镜(模糊+调色)
- 镂空合成模式
@Entry@Componentstruct Index{privatesettings:RenderingContextSettings=newRenderingContextSettings(true)privatecontext:CanvasRenderingContext2D=newCanvasRenderingContext2D(this.settings)build(){Scroll(){Column(){Canvas(this.context).width('100%').height(800).onReady(()=>{this.drawAll()})}}.width('100%').height('100%').backgroundColor('#2C3E50')}drawAll(){constctx=this.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 方法参见上文}FAQ
Q:为什么我设置了shadowBlur,但文字阴影没有模糊效果,只有实心色块?
A:检查shadowColor是否设置了非透明颜色。如果shadowColor是'white'且背景也是白色,阴影就看不见。另外,shadowBlur的值不宜过大,超过 100 在某些设备上会被忽略。
Q:filter和globalCompositeOperation可以一起用吗?
A:技术上可以,但顺序会影响结果。filter是对当前绘图操作施加滤镜,而globalCompositeOperation是控制新绘制的图形与画布已有内容的混合。建议先画完无滤镜内容,再使用globalCompositeOperation进行合成,避免滤镜干扰混合结果。
Q:为什么模拟器和真机的阴影颜色不一样?
A:模拟器的 GPU 驱动和真机不同,部分设备的阴影渲染颜色校准存在差异。建议以真机为准,并在真机上测试所有阴影效果的视觉一致性。
源码
示例代码地址:项目地址