《HarmonyOS技术精讲-ArkGraphics 2D(方舟2D图形服务)》第五篇:色彩与风格——渐变、阴影与滤镜效果

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
@Component
struct Index {
private settings: RenderingContextSettings = new RenderingContextSettings(true)
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)
build() {
Row() {
Canvas(this.context)
.width('100%')
.height('100%')
.onReady(() => {
this.drawLinearGradient()
})
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
drawLinearGradient() {
const ctx = this.context
// 1. 创建渐变对象,定义起始和结束坐标
const gradient = 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) {
// 径向渐变的参数:起始圆心、起始半径、终止圆心、终止半径
// 这里起始圆心偏向左上,制造光源效果
const radGrad = 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 = 0
ctx.shadowOffsetY = 0
ctx.font = 'bold 48px sans-serif'
ctx.fillStyle = '#FFFFFF'
ctx.textBaseline = 'top'
ctx.fillText('HarmonyOS', 50, 100)
// 重置阴影,避免影响后续绘制
ctx.shadowColor = 'transparent'
ctx.shadowBlur = 0
ctx.shadowOffsetX = 0
ctx.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 是异步的,需要在回调中绘制。
import image from '@ohos.multimedia.image'
async function drawImageFromPixelMap(ctx: CanvasRenderingContext2D, pixelMap: image.PixelMap) {
const bitmap = await image.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(new CanvasRenderingContext2D(this.settings)) // 每次刷新都会新建 }// ✅ 正确做法:context 在组件外创建一次 private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)原因:
build()在状态变化时会频繁执行,每次 new 都会销毁旧的上下文,导致已绘制的图形丢失。 -
每次绘制前,显式设置阴影、滤镜等全局属性。
不要假设上一步重置了状态。ArkUI Canvas 的全局属性在两次
onReady之间可能不保持。安全做法是在draw函数开头完整设置一次。 -
滤镜在实时绘制场景中,优先使用更轻量的方案。
例如动态亮度变化,用
brightness滤镜每帧重绘,CPU 消耗较高。如果只是固定滤镜效果,在首次绘制时初始化一次即可,不需要每帧都设置。
Demo 入口
所有代码集中在 Index.ets 文件中,包含以下效果:
- 线性渐变矩形
- 径向渐变3D球体
- 发光文字
- 照片滤镜(模糊+调色)
- 镂空合成模式
@Entry
@Component
struct Index {
private settings: RenderingContextSettings = new RenderingContextSettings(true)
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)
build() {
Scroll() {
Column() {
Canvas(this.context)
.width('100%')
.height(800)
.onReady(() => {
this.drawAll()
})
}
}
.width('100%')
.height('100%')
.backgroundColor('#2C3E50')
}
drawAll() {
const ctx = 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 驱动和真机不同,部分设备的阴影渲染颜色校准存在差异。建议以真机为准,并在真机上测试所有阴影效果的视觉一致性。
源码
示例代码地址:项目地址
更多推荐

所有评论(0)