一起学习HarmonyOS开发中的SVG渲染
一起学习HarmonyOS开发中的SVG渲染
核心要点:SVG是移动端矢量图形的「最优解」——无限缩放不失真、体积小、支持动画、可程序化操控。本文从SVG的解析与渲染机制讲起,深入HarmonyOS中SVG的加载方式、动画实现和矢量图标体系,帮你彻底掌握矢量图形在鸿蒙应用中的正确打开方式。
| 项目 | 说明 |
|---|---|
| 核心API | @ohos.multimedia.image、Image组件、Canvas |
一、背景与动机
你有没有遇到过这样的场景——设计给了一套图标,1x、2x、3x三套图,加起来几十个文件,APK体积蹭蹭往上涨。更烦人的是,某个图标在高分屏上还是有点糊,设计师说「再加个4x的吧」……
这就是位图的天然缺陷:固定分辨率,缩放就失真。
SVG(Scalable Vector Graphics)完美解决了这个问题。它用数学公式描述图形,而不是像素点。一个圆就是<circle cx="50" cy="50" r="40"/>,不管你放大到多大,它永远清晰。而且文件体积通常只有位图的几分之一。
在HarmonyOS中,SVG不仅可以通过Image组件直接渲染,还支持通过Canvas自定义绘制、通过代码动态修改属性、甚至实现动画效果。今天咱们就把SVG在鸿蒙上的玩法全部搞清楚。
二、核心原理
2.1 SVG渲染管线
SVG从文件到屏幕,经历解析、构建渲染树、光栅化三个阶段:

2.2 HarmonyOS中SVG的加载方式
| 加载方式 | 适用场景 | 特点 |
|---|---|---|
| Image组件 + 资源引用 | 静态图标、简单展示 | 最简单,支持color fill |
| Image组件 + ArrayBuffer | 网络下载的SVG | 需要先解码 |
| Canvas自定义绘制 | 需要交互、动画 | 灵活但代码量大 |
| PixelMap解码 | 需要像素级操作 | 转为位图后可像素操作 |
2.3 SVG动画原理
SVG动画有两种实现方式:
- SMIL动画:SVG原生动画,通过
<animate>、<animateTransform>等标签声明 - CSS动画:通过CSS的
@keyframes和animation属性驱动 - JavaScript动画:通过代码动态修改SVG属性
HarmonyOS的Image组件对SMIL动画的支持有限,推荐使用Canvas + 定时器的方式实现SVG动画。
三、代码实战
3.1 SVG资源加载与渲染
这是最基础也最常用的方式——通过Image组件加载SVG资源。
/**
* SVG基础加载演示
* 展示Image组件加载SVG的多种方式
*/
@Entry
@Component
struct SvgBasicDemo {
// 方式一:直接引用资源文件中的SVG
@State svgResource: Resource = $r('app.media.ic_icon_svg');
// 方式二:通过rawfile加载SVG
@State svgRawfile: string = 'resource:///rawfile/icons/home.svg';
// 方式三:通过沙箱路径加载SVG
@State svgFilePath: string = '';
// SVG填充色(动态修改图标颜色)
@State svgColor: string = '#4FC3F7';
// SVG缩放尺寸
@State svgSize: number = 48;
aboutToAppear(): void {
// 模拟从沙箱加载SVG文件路径
this.svgFilePath = '/data/storage/el2/base/files/custom_icon.svg';
}
build() {
Scroll() {
Column() {
Text('SVG基础加载')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 20 })
// 方式一:资源引用
Text('1. 资源文件引用')
.fontSize(16)
.fontColor('#AAAAAA')
.margin({ bottom: 8 })
Image(this.svgResource)
.width(this.svgSize)
.height(this.svgSize)
.fillColor(this.svgColor) // 动态修改SVG填充色
.margin({ bottom: 16 })
// 方式二:rawfile加载
Text('2. Rawfile加载')
.fontSize(16)
.fontColor('#AAAAAA')
.margin({ bottom: 8 })
Image($rawfile('icons/home.svg'))
.width(this.svgSize)
.height(this.svgSize)
.fillColor(this.svgColor)
.margin({ bottom: 16 })
// 颜色控制面板
Text('动态颜色控制')
.fontSize(16)
.fontColor('#AAAAAA')
.margin({ bottom: 8 })
Row() {
ForEach(['#4FC3F7', '#FFB74D', '#EF5350', '#81C784', '#CE93D8'], (color: string) => {
Circle()
.width(36)
.height(36)
.fill(color)
.onClick(() => { this.svgColor = color; })
})
}
.width('100%')
.justifyContent(FlexAlign.SpaceEvenly)
.margin({ bottom: 16 })
// 尺寸控制
Text(`当前尺寸: ${this.svgSize}vp`)
.fontSize(14)
.fontColor('#999999')
.margin({ bottom: 8 })
Slider({
value: this.svgSize,
min: 24,
max: 120,
step: 4
})
.width('100%')
.onChange((value: number) => { this.svgSize = value; })
.selectedColor('#4FC3F7')
}
.width('100%')
.padding(16)
}
.width('100%')
.height('100%')
.backgroundColor('#0D0D1A')
}
}
3.2 SVG解析与Canvas自定义渲染
当Image组件的能力不够时(比如需要动画、交互、自定义效果),就需要用Canvas手动解析和渲染SVG。
import { image } from '@kit.ImageKit';
/**
* SVG解析与Canvas渲染工具
* 将SVG文件解析为PixelMap,然后在Canvas上绘制
*/
export class SvgCanvasRenderer {
private canvasContext: CanvasRenderingContext2D | null = null;
// 缓存已解析的SVG PixelMap
private svgPixelMapCache: Map<string, image.PixelMap> = new Map();
/**
* 初始化Canvas上下文
*/
setContext(context: CanvasRenderingContext2D): void {
this.canvasContext = context;
}
/**
* 从资源加载SVG并渲染到Canvas
* @param svgData SVG的ArrayBuffer数据
* @param x 绘制位置X
* @param y 绘制位置Y
* @param width 绘制宽度
* @param height 绘制高度
*/
async renderSvg(svgData: ArrayBuffer, x: number, y: number, width: number, height: number): Promise<void> {
if (this.canvasContext === null) {
console.error('[SvgRenderer] Canvas上下文未初始化');
return;
}
// 创建ImageSource并解码SVG
const imageSource = image.createImageSource(svgData);
const pixelMap = await imageSource.createPixelMap({
editable: false,
desiredSize: { width: width, height: height },
desiredPixelFormat: image.PixelMapFormat.RGBA_8888
});
imageSource.release();
// 在Canvas上绘制PixelMap
this.canvasContext.drawImage(pixelMap, x, y, width, height);
// 缓存PixelMap
const cacheKey = `${width}x${height}`;
this.svgPixelMapCache.set(cacheKey, pixelMap);
console.info(`[SvgRenderer] SVG渲染完成: 位置(${x},${y}), 尺寸${width}x${height}`);
}
/**
* 渲染SVG并应用旋转变换
* @param svgData SVG数据
* @param centerX 旋转中心X
* @param centerY 旋转中心Y
* @param angle 旋转角度(度)
* @param width 绘制宽度
* @param height 绘制高度
*/
async renderSvgWithRotation(svgData: ArrayBuffer, centerX: number, centerY: number, angle: number, width: number, height: number): Promise<void> {
if (this.canvasContext === null) return;
const imageSource = image.createImageSource(svgData);
const pixelMap = await imageSource.createPixelMap({
editable: false,
desiredSize: { width: width, height: height }
});
imageSource.release();
// 保存Canvas状态
this.canvasContext.save();
// 平移到旋转中心
this.canvasContext.translate(centerX, centerY);
// 旋转
this.canvasContext.rotate(angle * Math.PI / 180);
// 绘制(以中心为原点)
this.canvasContext.drawImage(pixelMap, -width / 2, -height / 2, width, height);
// 恢复Canvas状态
this.canvasContext.restore();
}
/**
* 清空缓存
*/
clearCache(): void {
this.svgPixelMapCache.clear();
}
}
3.3 SVG动画实现
在Canvas上实现SVG动画——旋转的加载指示器、脉冲效果、路径动画。
import { image } from '@kit.ImageKit';
/**
* SVG动画控制器
* 通过Canvas定时器驱动SVG动画
*/
@Entry
@Component
struct SvgAnimationDemo {
private settings: RenderingContextSettings = new RenderingContextSettings(true);
private canvasContext: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
// 动画相关状态
@State rotationAngle: number = 0;
@State pulseScale: number = 1.0;
@State pathProgress: number = 0;
// 动画定时器ID
private animationTimer: number = -1;
// 动画是否运行中
@State isAnimating: boolean = false;
// 当前动画模式
@State animMode: string = 'rotate';
aboutToDisappear(): void {
this.stopAnimation();
}
/**
* 启动动画
*/
startAnimation(): void {
if (this.isAnimating) return;
this.isAnimating = true;
this.animationTimer = setInterval(() => {
if (this.animMode === 'rotate') {
// 旋转动画:每帧旋转3度
this.rotationAngle = (this.rotationAngle + 3) % 360;
} else if (this.animMode === 'pulse') {
// 脉冲动画:正弦波缩放
this.pulseScale = 1.0 + 0.3 * Math.sin(Date.now() / 300);
} else if (this.animMode === 'path') {
// 路径动画:线性递增
this.pathProgress = (this.pathProgress + 0.02) % 1.0;
}
this.drawAnimationFrame();
}, 16); // 约60fps
}
/**
* 停止动画
*/
stopAnimation(): void {
if (this.animationTimer !== -1) {
clearInterval(this.animationTimer);
this.animationTimer = -1;
}
this.isAnimating = false;
}
/**
* 绘制动画帧
*/
drawAnimationFrame(): void {
const canvas = this.canvasContext;
const centerX = 150;
const centerY = 150;
// 清空画布
canvas.clearRect(0, 0, 300, 300);
if (this.animMode === 'rotate') {
this.drawRotateAnimation(canvas, centerX, centerY);
} else if (this.animMode === 'pulse') {
this.drawPulseAnimation(canvas, centerX, centerY);
} else if (this.animMode === 'path') {
this.drawPathAnimation(canvas, centerX, centerY);
}
}
/**
* 旋转加载动画
* 模拟常见的loading spinner
*/
drawRotateAnimation(canvas: CanvasRenderingContext2D, cx: number, cy: number): void {
canvas.save();
canvas.translate(cx, cy);
canvas.rotate(this.rotationAngle * Math.PI / 180);
// 绘制8个圆弧段,模拟spinner
for (let i = 0; i < 8; i++) {
const angle = (i * 45) * Math.PI / 180;
const opacity = 1.0 - (i * 0.1); // 逐渐变淡
const radius = 40;
canvas.beginPath();
canvas.arc(
Math.cos(angle) * radius,
Math.sin(angle) * radius,
6, 0, Math.PI * 2
);
canvas.fillStyle = `rgba(79, 195, 247, ${opacity})`;
canvas.fill();
}
canvas.restore();
}
/**
* 脉冲动画
* 图标从中心向外脉冲扩散
*/
drawPulseAnimation(canvas: CanvasRenderingContext2D, cx: number, cy: number): void {
// 外圈脉冲
const outerRadius = 50 * this.pulseScale;
canvas.beginPath();
canvas.arc(cx, cy, outerRadius, 0, Math.PI * 2);
canvas.strokeStyle = `rgba(79, 195, 247, ${1.0 - (this.pulseScale - 1.0) / 0.3 * 0.7})`;
canvas.lineWidth = 2;
canvas.stroke();
// 中心图标(用圆形模拟)
canvas.beginPath();
canvas.arc(cx, cy, 20, 0, Math.PI * 2);
canvas.fillStyle = '#4FC3F7';
canvas.fill();
// 内部图标细节
canvas.beginPath();
canvas.moveTo(cx - 8, cy);
canvas.lineTo(cx + 8, cy);
canvas.moveTo(cx, cy - 8);
canvas.lineTo(cx, cy + 8);
canvas.strokeStyle = '#0D0D1A';
canvas.lineWidth = 3;
canvas.stroke();
}
/**
* 路径动画
* 沿贝塞尔曲线运动的圆点
*/
drawPathAnimation(canvas: CanvasRenderingContext2D, cx: number, cy: number): void {
// 绘制贝塞尔曲线路径
const startX = cx - 80;
const startY = cy + 40;
const cp1X = cx - 40;
const cp1Y = cy - 60;
const cp2X = cx + 40;
const cp2Y = cy + 60;
const endX = cx + 80;
const endY = cy - 40;
// 绘制路径(半透明)
canvas.beginPath();
canvas.moveTo(startX, startY);
canvas.bezierCurveTo(cp1X, cp1Y, cp2X, cp2Y, endX, endY);
canvas.strokeStyle = 'rgba(79, 195, 247, 0.3)';
canvas.lineWidth = 2;
canvas.stroke();
// 计算当前进度对应的贝塞尔曲线上的点
const t = this.pathProgress;
const pointX = Math.pow(1 - t, 3) * startX +
3 * Math.pow(1 - t, 2) * t * cp1X +
3 * (1 - t) * Math.pow(t, 2) * cp2X +
Math.pow(t, 3) * endX;
const pointY = Math.pow(1 - t, 3) * startY +
3 * Math.pow(1 - t, 2) * t * cp1Y +
3 * (1 - t) * Math.pow(t, 2) * cp2Y +
Math.pow(t, 3) * endY;
// 绘制运动圆点
canvas.beginPath();
canvas.arc(pointX, pointY, 8, 0, Math.PI * 2);
canvas.fillStyle = '#4FC3F7';
canvas.fill();
// 绘制运动轨迹(已走过的部分)
canvas.beginPath();
canvas.moveTo(startX, startY);
// 使用多段直线模拟已走过的曲线
const steps = Math.floor(t * 50);
for (let i = 1; i <= steps; i++) {
const st = i / 50;
const sx = Math.pow(1 - st, 3) * startX +
3 * Math.pow(1 - st, 2) * st * cp1X +
3 * (1 - st) * Math.pow(st, 2) * cp2X +
Math.pow(st, 3) * endX;
const sy = Math.pow(1 - st, 3) * startY +
3 * Math.pow(1 - st, 2) * st * cp1Y +
3 * (1 - st) * Math.pow(st, 2) * cp2Y +
Math.pow(st, 3) * endY;
canvas.lineTo(sx, sy);
}
canvas.strokeStyle = '#4FC3F7';
canvas.lineWidth = 3;
canvas.stroke();
}
build() {
Column() {
Text('SVG动画演示')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 16 })
// Canvas动画区域
Canvas(this.canvasContext)
.width(300)
.height(300)
.backgroundColor('#1A1A2E')
.borderRadius(12)
// 动画模式选择
Row() {
Button('旋转')
.backgroundColor(this.animMode === 'rotate' ? '#4FC3F7' : '#333333')
.fontColor(this.animMode === 'rotate' ? '#000000' : '#AAAAAA')
.onClick(() => {
this.animMode = 'rotate';
this.rotationAngle = 0;
})
Button('脉冲')
.backgroundColor(this.animMode === 'pulse' ? '#CE93D8' : '#333333')
.fontColor(this.animMode === 'pulse' ? '#000000' : '#AAAAAA')
.onClick(() => {
this.animMode = 'pulse';
this.pulseScale = 1.0;
})
Button('路径')
.backgroundColor(this.animMode === 'path' ? '#81C784' : '#333333')
.fontColor(this.animMode === 'path' ? '#000000' : '#AAAAAA')
.onClick(() => {
this.animMode = 'path';
this.pathProgress = 0;
})
}
.width('100%')
.justifyContent(FlexAlign.SpaceEvenly)
.margin({ top: 16 })
// 播放控制
Row() {
Button(this.isAnimating ? '暂停' : '播放')
.backgroundColor(this.isAnimating ? '#EF5350' : '#81C784')
.fontColor('#FFFFFF')
.onClick(() => {
if (this.isAnimating) {
this.stopAnimation();
} else {
this.startAnimation();
}
})
Button('重置')
.backgroundColor('#FFB74D')
.fontColor('#000000')
.onClick(() => {
this.stopAnimation();
this.rotationAngle = 0;
this.pulseScale = 1.0;
this.pathProgress = 0;
this.canvasContext.clearRect(0, 0, 300, 300);
})
}
.width('100%')
.justifyContent(FlexAlign.SpaceEvenly)
.margin({ top: 12 })
}
.width('100%')
.height('100%')
.padding(16)
.backgroundColor('#0D0D1A')
}
}
3.4 SVG矢量图标体系
构建一套完整的矢量图标体系,支持动态颜色、尺寸和主题切换。
/**
* SVG矢量图标体系
* 统一管理应用中的所有矢量图标
*/
// 图标定义:每个图标对应一个SVG路径数据
export class SvgIconRegistry {
// 图标路径数据缓存
private static icons: Map<string, string> = new Map();
/**
* 注册图标
* @param name 图标名称
* @param svgPath SVG路径数据(d属性值)
*/
static register(name: string, svgPath: string): void {
SvgIconRegistry.icons.set(name, svgPath);
}
/**
* 获取图标路径
*/
static get(name: string): string | undefined {
return SvgIconRegistry.icons.get(name);
}
/**
* 生成完整的SVG字符串
* @param name 图标名称
* @param size 尺寸
* @param color 填充色
*/
static toSvgString(name: string, size: number, color: string): string {
const path = SvgIconRegistry.icons.get(name);
if (!path) {
console.warn(`[SvgIcon] 图标未注册: ${name}`);
return '';
}
return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="${color}"><path d="${path}"/></svg>`;
}
}
// 注册常用图标(Material Design风格路径)
SvgIconRegistry.register('home', 'M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z');
SvgIconRegistry.register('search', 'M15.5 14h-.79l-.28-.27A6.471 6.471 0 0016 9.5 6.5 6.5 0 109.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z');
SvgIconRegistry.register('settings', 'M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58a.49.49 0 00.12-.61l-1.92-3.32a.488.488 0 00-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54a.484.484 0 00-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.07.62-.07.94s.02.64.07.94l-2.03 1.58a.49.49 0 00-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6A3.6 3.6 0 1115.6 12 3.611 3.611 0 0112 15.6z');
SvgIconRegistry.register('favorite', 'M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z');
SvgIconRegistry.register('person', 'M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z');
/**
* 矢量图标组件
* 可复用的SVG图标组件,支持动态颜色和尺寸
*/
@Component
export struct SvgIcon {
// 图标名称
@Prop iconName: string = 'home';
// 图标尺寸
@Prop iconSize: number = 24;
// 图标颜色
@Prop iconColor: string = '#FFFFFF';
// 点击回调
onIconClick?: () => void;
build() {
Image($r('app.media.ic_icon_svg'))
.width(this.iconSize)
.height(this.iconSize)
.fillColor(this.iconColor)
.onClick(() => {
if (this.onIconClick) {
this.onIconClick();
}
})
}
}
/**
* 图标工具栏示例
* 展示矢量图标在实际UI中的应用
*/
@Entry
@Component
struct SvgIconBarDemo {
@State activeTab: number = 0;
@State themeColor: string = '#4FC3F7';
// 底部导航图标配置
private tabItems: Array<{ name: string; icon: string; label: string }> = [
{ name: 'home', icon: 'home', label: '首页' },
{ name: 'search', icon: 'search', label: '搜索' },
{ name: 'favorite', icon: 'favorite', label: '收藏' },
{ name: 'person', icon: 'person', label: '我的' }
];
build() {
Column() {
// 内容区域
Column() {
Text(this.tabItems[this.activeTab].label)
.fontSize(28)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('SVG矢量图标体系演示')
.fontSize(14)
.fontColor('#999999')
.margin({ top: 8 })
}
.width('100%')
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
// 底部导航栏
Row() {
ForEach(this.tabItems, (item: { name: string; icon: string; label: string }, index: number) => {
Column() {
Image($r('app.media.ic_icon_svg'))
.width(24)
.height(24)
.fillColor(this.activeTab === index ? this.themeColor : '#666666')
Text(item.label)
.fontSize(12)
.fontColor(this.activeTab === index ? this.themeColor : '#666666')
.margin({ top: 4 })
}
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
.onClick(() => { this.activeTab = index; })
})
}
.width('100%')
.height(64)
.backgroundColor('#1A1A2E')
.border({ width: { top: 1 }, color: '#333333' })
}
.width('100%')
.height('100%')
.backgroundColor('#0D0D1A')
}
}
四、踩坑与注意事项
4.1 SVG兼容性问题
HarmonyOS的Image组件对SVG的支持不是完整的。以下特性可能不支持或行为异常:
| SVG特性 | 支持情况 | 备注 |
|---|---|---|
| 基本图形(rect, circle, path) | ✅ 完全支持 | |
| 渐变(linearGradient) | ⚠️ 部分支持 | 某些复杂渐变可能渲染异常 |
| 滤镜(filter, blur) | ❌ 不支持 | 需要用Canvas模拟 |
| 文字(text) | ⚠️ 部分支持 | 中文字体可能缺失 |
| SMIL动画 | ❌ 不支持 | 需要用Canvas+定时器实现 |
| CSS动画 | ❌ 不支持 | 同上 |
| clipPath | ⚠️ 部分支持 | 简单裁剪可用 |
| mask | ❌ 不支持 | 需要用Canvas的globalCompositeOperation |
建议:对于复杂的SVG效果,优先使用Canvas手动绘制。
4.2 fillColor只对单色SVG有效
Image组件的fillColor属性,本质上是将SVG中所有fill属性替换为指定颜色。如果你的SVG本身包含多种颜色(渐变、多色图标),fillColor会把所有颜色都覆盖掉。
解决方案:
- 多色图标不要用
fillColor,直接原样渲染 - 或者将多色图标拆分为多个单色图层
4.3 SVG文件体积优化
SVG虽然通常比位图小,但如果不注意优化,文件体积也可能很大:
- 删除编辑器元数据:Illustrator、Inkscape导出的SVG包含大量无用元数据
- 简化路径:使用
svgo工具优化路径数据 - 合并相同样式:将重复的fill/stroke属性提取为class
- 避免嵌入位图:SVG中嵌入的
<image>标签会大幅增加体积
4.4 SVG解码性能
SVG解码比位图慢,因为需要XML解析 + 渲染树构建 + 光栅化。对于频繁使用的图标,建议:
- 首次加载后缓存解码后的PixelMap
- 避免在列表项中直接使用SVG(改用缓存后的PixelMap)
- 预加载关键SVG图标
4.5 viewBox与实际尺寸的关系
SVG的viewBox定义了坐标系统,实际渲染尺寸由Image组件的width/height决定。如果viewBox和渲染尺寸的宽高比不一致,需要设置objectFit:
Image($r('app.media.icon_svg'))
.width(48)
.height(48)
.objectFit(ImageFit.Contain) // 保持宽高比
五、HarmonyOS 6适配
5.1 SVG渲染引擎升级
HarmonyOS 6对SVG渲染引擎做了重大升级:
| 变化项 | HarmonyOS 5 | HarmonyOS 6 |
|---|---|---|
| 渐变支持 | linearGradient部分支持 | linearGradient + radialGradient完全支持 |
| 滤镜支持 | 不支持 | 支持blur、drop-shadow |
| 文字渲染 | 英文OK,中文可能缺失 | 支持系统字体渲染 |
| 动画支持 | 不支持SMIL | 支持SMIL基本动画(animate, animateTransform) |
| 性能 | 首次解码较慢 | GPU加速,解码速度提升3倍 |
5.2 新增SVG动画API
// HarmonyOS 6新增:SVG动画支持
// Image组件可以直接播放SVG中的SMIL动画
Image($r('app.media.animated_icon'))
.width(48)
.height(48)
.autoPlay(true) // 自动播放SVG动画
.repeatCount(-1) // 无限循环
5.3 迁移建议
- 原来用Canvas实现的SVG动画,可以尝试迁移到Image组件的SMIL动画支持
- 原来不支持渐变的SVG图标,可以重新导出带渐变的版本
- 利用GPU加速特性,减少Canvas手动绘制的场景
六、总结
| 知识点 | 核心内容 |
|---|---|
| SVG加载方式 | Image组件资源引用、rawfile、沙箱文件;Canvas自定义绘制;PixelMap解码 |
| SVG解析 | 通过image.createImageSource解码SVG为PixelMap,支持指定尺寸和像素格式 |
| SVG渲染 | Image组件直接渲染(最简单);Canvas自定义渲染(最灵活) |
| SVG动画 | Canvas+定时器实现旋转、脉冲、路径动画;HarmonyOS 6支持SMIL原生动画 |
| 矢量图标体系 | 统一注册管理SVG路径数据,支持动态颜色、尺寸、主题切换 |
| fillColor限制 | 只对单色SVG有效,多色图标会被覆盖;多色图标需拆分或原样渲染 |
| 性能优化 | 缓存解码后的PixelMap、简化SVG路径、避免列表中直接使用SVG |
| HarmonyOS 6 | 渐变完全支持、滤镜支持、SMIL动画支持、GPU加速解码 |
一句话总结:SVG在HarmonyOS中的正确用法是「简单场景用Image组件,复杂场景用Canvas,动画用定时器驱动,图标用统一体系管理」——选对工具,事半功倍。
- 点赞
- 收藏
- 关注作者
评论(0)