Appearance
HarmonyOS 图标方案选型与实战:SVG/IconFont/Emoji 完整对比
本文详解 HarmonyOS 应用中图标的三种主流方案,给出可落地的代码片段和选型建议。
核心结论
生产项目推荐方案排序:
- ⭐ 矢量图标(SVG)+ Image 组件 + 模板渲染 - 主推方案
- 自定义 IconFont - 适合大量单色图标
- Emoji 表情 - 快速原型,但不稳定
方案 1:SVG/模板图标 + Image 组件(推荐)
核心优势
- ✅ 清晰可缩放:矢量图标,任意尺寸不失真
- ✅ 统一着色:通过
renderMode + fillColor实现主题色统一 - ✅ 易于管理:资源文件统一放在
resources/base/media/ - ✅ 多格式支持:SVG、PNG、WEBP、GIF 等
- ✅ 主题适配:配合系统主题,动态调整颜色
资源目录结构
entry/src/main/resources/
├── base/
│ ├── media/
│ │ ├── icon_chat.svg # 聊天图标
│ │ ├── icon_voice.svg # 语音图标
│ │ ├── icon_video.svg # 视频图标
│ │ ├── icon_settings.svg # 设置图标
│ │ └── ...基础用法
typescript
import { Image } from '@kit.ArkUI';
@Entry
@Component
struct IconExample {
build() {
Column({ space: 20 }) {
// 基础 SVG 图标
Image($r('app.media.icon_chat'))
.width(40)
.height(40)
// 带着色的模板图标
Image($r('app.media.icon_chat'))
.width(40)
.height(40)
.renderMode(ImageRenderMode.Template) // 模板渲染
.fillColor('#4A90E2') // 统一设定颜色
}
}
}模板渲染与着色
typescript
@Entry
@Component
struct ThemedIconExample {
@State themeColor: string = '#4A90E2';
build() {
Column({ space: 20 }) {
// 复杂逻辑:使用 SVG 模板图标,可随主题色统一着色
Image($r('app.media.icon_chat'))
.width(80)
.height(80)
.renderMode(ImageRenderMode.Template) // 将矢量当模板渲染
.fillColor(this.themeColor) // 统一设定颜色
.borderRadius(16)
.backgroundColor('#F5F5F5')
// 主题色切换按钮
Row({ space: 10 }) {
Button('蓝色')
.onClick(() => this.themeColor = '#4A90E2')
Button('红色')
.onClick(() => this.themeColor = '#E74C3C')
Button('绿色')
.onClick(() => this.themeColor = '#2ECC71')
}
}
.width('100%')
.padding(20)
}
}实战示例:功能卡片
typescript
interface FeatureItem {
icon: Resource; // 图标资源
name: string; // 功能名称
color: string; // 主题色
}
@Entry
@Component
struct FeatureGrid {
private features: FeatureItem[] = [
{ icon: $r('app.media.icon_chat'), name: '聊天', color: '#4A90E2' },
{ icon: $r('app.media.icon_voice'), name: '语音', color: '#E74C3C' },
{ icon: $r('app.media.icon_video'), name: '视频', color: '#9B59B6' },
{ icon: $r('app.media.icon_settings'), name: '设置', color: '#95A5A6' }
];
@Builder
FeatureCard(item: FeatureItem) {
Column() {
// 图标
Image(item.icon)
.width(48)
.height(48)
.renderMode(ImageRenderMode.Template)
.fillColor(item.color)
// 标题
Text(item.name)
.fontSize(14)
.fontColor('#333333')
.margin({ top: 8 })
}
.width(80)
.height(100)
.backgroundColor('#F5F5F5')
.borderRadius(12)
.justifyContent(FlexAlign.Center)
.onClick(() => {
console.info(`点击了:${item.name}`);
})
}
build() {
Grid() {
ForEach(this.features, (item: FeatureItem) => {
GridItem() {
this.FeatureCard(item)
}
})
}
.columnsTemplate('1fr 1fr 1fr 1fr')
.rowsGap(20)
.columnsGap(20)
.padding(20)
}
}完整实战:Tab 导航栏
typescript
interface TabItem {
icon: Resource;
activeIcon: Resource;
label: string;
}
@Entry
@Component
struct TabBarExample {
@State currentIndex: number = 0;
private tabs: TabItem[] = [
{
icon: $r('app.media.icon_home'),
activeIcon: $r('app.media.icon_home_filled'),
label: '首页'
},
{
icon: $r('app.media.icon_discover'),
activeIcon: $r('app.media.icon_discover_filled'),
label: '发现'
},
{
icon: $r('app.media.icon_message'),
activeIcon: $r('app.media.icon_message_filled'),
label: '消息'
},
{
icon: $r('app.media.icon_profile'),
activeIcon: $r('app.media.icon_profile_filled'),
label: '我的'
}
];
@Builder
TabIcon(index: number, item: TabItem) {
Column() {
Image(this.currentIndex === index ? item.activeIcon : item.icon)
.width(24)
.height(24)
.renderMode(ImageRenderMode.Template)
.fillColor(this.currentIndex === index ? '#4A90E2' : '#999999')
Text(item.label)
.fontSize(12)
.fontColor(this.currentIndex === index ? '#4A90E2' : '#999999')
.margin({ top: 4 })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.onClick(() => {
this.currentIndex = index;
})
}
build() {
Column() {
// 内容区域
Text(`当前页面:${this.tabs[this.currentIndex].label}`)
.fontSize(24)
.layoutWeight(1)
.textAlign(TextAlign.Center)
// 底部 Tab 栏
Row() {
ForEach(this.tabs, (item: TabItem, index: number) => {
this.TabIcon(index, item)
})
}
.width('100%')
.height(60)
.backgroundColor('#FFFFFF')
.border({
width: { top: 0.5 },
color: '#E0E0E0'
})
}
.height('100%')
}
}SVG 文件准备
推荐工具:
- iconfont.cn - 阿里巴巴矢量图标库
- iconify - 开源图标集合
导出设置:
格式:SVG
尺寸:建议 24x24 或 48x48
颜色:单色(#000000),方便后续着色文件命名规范:
icon_[功能名]_[状态].svg
示例:
- icon_home.svg # 首页(未激活)
- icon_home_filled.svg # 首页(激活)
- icon_message.svg # 消息
- icon_settings.svg # 设置方案 2:自定义 IconFont(适合大量单色图标)
核心优势
- ✅ 体积小:字体文件比多个图片文件小
- ✅ 文本排版:与文字对齐友好
- ✅ 易着色:CSS/样式控制颜色
- ⚠️ 多色受限:不适合多色图标
- ⚠️ 需管理码点:需维护图标与 Unicode 的映射
资源准备
1. 生成字体文件
访问 iconfont.cn:
- 选择图标并加入购物车
- 下载代码
- 解压得到
iconfont.ttf
2. 放置资源
entry/src/main/resources/
├── rawfile/
│ └── fonts/
│ └── iconfont.ttf注册字体
typescript
import { font } from '@kit.ArkUI';
@Entry
@Component
struct IconFontExample {
aboutToAppear(): void {
// 复杂逻辑:注册自定义图标字体
font.registerFont({
familyName: 'IconFont',
familySrc: '/fonts/iconfont.ttf' // rawfile 路径
});
}
build() {
Column({ space: 20 }) {
// 使用 IconFont
Text('\ue001') // 具体码点从 iconfont 获取
.fontFamily('IconFont')
.fontSize(40)
.fontColor('#4A90E2')
Text('\ue002')
.fontFamily('IconFont')
.fontSize(40)
.fontColor('#E74C3C')
}
.padding(20)
}
}实战示例:图标库
typescript
// 图标映射表
class IconFontMap {
static readonly HOME = '\ue001';
static readonly MESSAGE = '\ue002';
static readonly SETTINGS = '\ue003';
static readonly USER = '\ue004';
static readonly SEARCH = '\ue005';
}
@Entry
@Component
struct IconFontDemo {
aboutToAppear(): void {
font.registerFont({
familyName: 'IconFont',
familySrc: '/fonts/iconfont.ttf'
});
}
@Builder
IconItem(icon: string, label: string, color: string) {
Column() {
Text(icon)
.fontFamily('IconFont')
.fontSize(32)
.fontColor(color)
Text(label)
.fontSize(12)
.fontColor('#666666')
.margin({ top: 4 })
}
.width(80)
.padding(10)
}
build() {
Column({ space: 20 }) {
Text('IconFont 图标库')
.fontSize(20)
.fontWeight(FontWeight.Bold)
Row({ space: 10 }) {
this.IconItem(IconFontMap.HOME, '首页', '#4A90E2')
this.IconItem(IconFontMap.MESSAGE, '消息', '#E74C3C')
this.IconItem(IconFontMap.SETTINGS, '设置', '#95A5A6')
this.IconItem(IconFontMap.USER, '用户', '#2ECC71')
}
}
.width('100%')
.padding(20)
}
}新版 API(API 11+)
typescript
import { UIContext } from '@kit.ArkUI';
@Entry
@Component
struct IconFontNewAPI {
aboutToAppear(): void {
// 新版 API:通过 UIContext 注册字体
const uiContext: UIContext = this.getUIContext();
const fontManager = uiContext.getFontManager();
fontManager.registerFont({
familyName: 'IconFont',
familySrc: '/fonts/iconfont.ttf'
});
}
build() {
Column() {
Text('\ue001')
.fontFamily('IconFont')
.fontSize(40)
}
}
}方案 3:Emoji 表情(快速原型)
核心优势
- ✅ 零成本:无需额外资源
- ✅ 快速开发:直接写在代码中
- ⚠️ 不一致:不同设备显示不同
- ⚠️ 字体依赖:部分环境显示为方框
基础用法
typescript
@Entry
@Component
struct EmojiExample {
build() {
Column({ space: 20 }) {
// 直接使用 Emoji
Text('💬')
.fontSize(40)
Text('🎙️')
.fontSize(40)
Text('📹')
.fontSize(40)
}
.padding(20)
}
}实战示例(仅适合原型)
typescript
interface QuickFeature {
icon: string; // Emoji
name: string;
}
@Entry
@Component
struct EmojiPrototype {
private features: QuickFeature[] = [
{ icon: '💬', name: '聊天' },
{ icon: '🎙️', name: '语音' },
{ icon: '📹', name: '视频' },
{ icon: '⚙️', name: '设置' }
];
build() {
Column({ space: 20 }) {
Text('⚠️ 仅用于快速原型')
.fontSize(14)
.fontColor('#E74C3C')
ForEach(this.features, (item: QuickFeature) => {
Row({ space: 10 }) {
Text(item.icon)
.fontSize(24)
Text(item.name)
.fontSize(16)
}
.width('100%')
.padding(15)
.backgroundColor('#F5F5F5')
.borderRadius(8)
})
}
.padding(20)
}
}Emoji 的问题
typescript
@Entry
@Component
struct EmojiProblems {
build() {
Column({ space: 20 }) {
Text('不同系统的 Emoji 显示效果')
.fontSize(16)
.fontWeight(FontWeight.Bold)
// iOS/Mac 上显示为彩色圆形
// Android 上显示为不同风格
// Windows 上可能显示为方框
Text('😀 😁 😂 🤣')
.fontSize(32)
Text('⚠️ 注意事项')
.fontSize(14)
.fontColor('#E74C3C')
.margin({ top: 20 })
Column({ space: 8 }) {
Text('• 不同设备字形不一致')
Text('• 颜色无法统一控制')
Text('• 部分设备显示为方框')
Text('• 不适合生产环境')
}
.alignItems(HorizontalAlign.Start)
.width('100%')
.fontSize(12)
.fontColor('#666666')
}
.padding(20)
}
}方案选型对比
快速决策表
| 需求 | 推荐方案 | 原因 |
|---|---|---|
| 生产项目 | ⭐ SVG + Image | 清晰、可缩放、统一着色、易主题化 |
| 大量单色图标 | IconFont | 体积小、文本排版友好 |
| 多色图标 | SVG + Image | 支持多色,清晰度高 |
| 主题切换 | SVG + Template | 模板渲染 + fillColor 动态着色 |
| 快速原型 | Emoji | 零成本,但不稳定 |
| 跨设备一致性 | SVG + Image | 矢量图标,任意设备清晰 |
详细对比
| 维度 | SVG + Image | IconFont | Emoji |
|---|---|---|---|
| 清晰度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐ |
| 可缩放 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| 着色能力 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ❌ |
| 多色支持 | ⭐⭐⭐⭐⭐ | ⭐ | ⭐⭐⭐⭐ |
| 主题适配 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ❌ |
| 文件体积 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 开发成本 | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 维护成本 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
| 跨设备一致性 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐ |
最佳实践
1. 资源命名规范
# SVG 图标
icon_[功能]_[状态].svg
示例:
- icon_home.svg # 首页(默认)
- icon_home_filled.svg # 首页(填充)
- icon_home_outlined.svg # 首页(轮廓)
# 按模块分类
icon_nav_home.svg # 导航-首页
icon_tab_message.svg # Tab-消息
icon_action_delete.svg # 操作-删除2. 主题色管理
typescript
// 主题色配置
class AppTheme {
static readonly PRIMARY = '#4A90E2';
static readonly SUCCESS = '#2ECC71';
static readonly WARNING = '#F39C12';
static readonly DANGER = '#E74C3C';
static readonly INFO = '#95A5A6';
}
@Entry
@Component
struct ThemedIcons {
@StorageLink('themeColor') themeColor: string = AppTheme.PRIMARY;
build() {
Column() {
Image($r('app.media.icon_like'))
.width(32)
.height(32)
.renderMode(ImageRenderMode.Template)
.fillColor(this.themeColor) // 使用主题色
}
}
}3. 封装通用组件
typescript
@Component
export struct IconButton {
@Prop icon: Resource;
@Prop label: string;
@Prop color: string = '#4A90E2';
@Prop size: number = 24;
onTap?: () => void;
build() {
Column({ space: 4 }) {
Image(this.icon)
.width(this.size)
.height(this.size)
.renderMode(ImageRenderMode.Template)
.fillColor(this.color)
Text(this.label)
.fontSize(12)
.fontColor(this.color)
}
.padding(8)
.onClick(() => {
this.onTap?.();
})
}
}
// 使用
@Entry
@Component
struct IconButtonDemo {
build() {
Row({ space: 10 }) {
IconButton({
icon: $r('app.media.icon_like'),
label: '点赞',
color: '#E74C3C',
onTap: () => {
console.info('点赞');
}
})
IconButton({
icon: $r('app.media.icon_share'),
label: '分享',
color: '#4A90E2',
onTap: () => {
console.info('分享');
}
})
}
}
}4. 响应式尺寸
typescript
@Entry
@Component
struct ResponsiveIcon {
// 根据设备宽度调整图标尺寸
@State iconSize: number = 24;
aboutToAppear(): void {
// 获取屏幕宽度
const screenWidth = 360; // 实际项目中从设备信息获取
if (screenWidth < 360) {
this.iconSize = 20;
} else if (screenWidth < 480) {
this.iconSize = 24;
} else {
this.iconSize = 32;
}
}
build() {
Image($r('app.media.icon_home'))
.width(this.iconSize)
.height(this.iconSize)
.renderMode(ImageRenderMode.Template)
.fillColor('#4A90E2')
}
}5. 动态加载与缓存
typescript
@Entry
@Component
struct DynamicIconLoading {
@State iconList: Resource[] = [];
@State isLoading: boolean = true;
async aboutToAppear() {
// 模拟异步加载图标配置
await this.loadIcons();
}
async loadIcons(): Promise<void> {
this.isLoading = true;
// 实际项目中从服务器或配置文件加载
this.iconList = [
$r('app.media.icon_home'),
$r('app.media.icon_message'),
$r('app.media.icon_settings')
];
this.isLoading = false;
}
build() {
Column() {
if (this.isLoading) {
Text('加载中...')
} else {
ForEach(this.iconList, (icon: Resource) => {
Image(icon)
.width(32)
.height(32)
.margin(10)
})
}
}
}
}常见问题与解决方案
问题 1:SVG 显示异常
typescript
// ❌ 错误:SVG 文件包含不支持的特性
// 解决:简化 SVG,移除复杂效果
// 推荐的 SVG 格式
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M12 2L2 7v10c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V7l-10-5z" />
</svg>问题 2:IconFont 显示为方框
typescript
// ❌ 字体未注册或路径错误
aboutToAppear(): void {
// ✅ 确保路径正确
font.registerFont({
familyName: 'IconFont',
familySrc: '/fonts/iconfont.ttf' // 相对于 rawfile 目录
});
}
// ✅ 确认字体文件存在
// entry/src/main/resources/rawfile/fonts/iconfont.ttf问题 3:图标着色不生效
typescript
// ❌ 错误:未设置 renderMode
Image($r("app.media.icon_home")).fillColor("#4A90E2"); // 无效
// ✅ 正确:必须先设置 renderMode
Image($r("app.media.icon_home"))
.renderMode(ImageRenderMode.Template) // 关键
.fillColor("#4A90E2"); // 生效问题 4:性能优化
typescript
// ❌ 重复加载图标
ForEach(this.items, (item: Item) => {
Image($r('app.media.icon_home')) // 每次都加载
})
// ✅ 缓存图标资源
@State cachedIcon: Resource = $r('app.media.icon_home');
ForEach(this.items, (item: Item) => {
Image(this.cachedIcon) // 复用
})总结
核心要点
- SVG + Image(模板渲染) 是生产项目的首选方案
- IconFont 适合大量单色图标场景
- Emoji 仅用于快速原型,不建议生产使用
- 模板渲染 + fillColor 是主题化的关键
- 资源命名规范 决定项目可维护性
选型建议
| 项目类型 | 推荐方案 | 理由 |
|---|---|---|
| 正式产品 | SVG + Image | 清晰、可缩放、主题化 |
| 快速原型 | Emoji | 零成本,快速开发 |
| 大量图标 | IconFont | 体积小,易管理 |
| 多色图标 | SVG | 完美支持多色 |
| 主题切换 | SVG + Template | 动态着色能力 |
推荐工作流
1. 设计师提供 SVG 图标
↓
2. 优化 SVG(移除复杂特性)
↓
3. 放入 resources/base/media/
↓
4. 使用 $r('app.media.xxx') 引用
↓
5. 配合 renderMode + fillColor 实现主题化下一步学习
相关资源: