Skip to content

HarmonyOS 图标方案选型与实战:SVG/IconFont/Emoji 完整对比

本文详解 HarmonyOS 应用中图标的三种主流方案,给出可落地的代码片段和选型建议。

核心结论

生产项目推荐方案排序:

  1. 矢量图标(SVG)+ Image 组件 + 模板渲染 - 主推方案
  2. 自定义 IconFont - 适合大量单色图标
  3. 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 文件准备

推荐工具:

导出设置:

格式: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

  1. 选择图标并加入购物车
  2. 下载代码
  3. 解压得到 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 + ImageIconFontEmoji
清晰度⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
可缩放⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
着色能力⭐⭐⭐⭐⭐⭐⭐⭐⭐
多色支持⭐⭐⭐⭐⭐⭐⭐⭐⭐
主题适配⭐⭐⭐⭐⭐⭐⭐⭐⭐
文件体积⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
开发成本⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
维护成本⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
跨设备一致性⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐

最佳实践

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) // 复用
})

总结

核心要点

  1. SVG + Image(模板渲染) 是生产项目的首选方案
  2. IconFont 适合大量单色图标场景
  3. Emoji 仅用于快速原型,不建议生产使用
  4. 模板渲染 + fillColor 是主题化的关键
  5. 资源命名规范 决定项目可维护性

选型建议

项目类型推荐方案理由
正式产品SVG + Image清晰、可缩放、主题化
快速原型Emoji零成本,快速开发
大量图标IconFont体积小,易管理
多色图标SVG完美支持多色
主题切换SVG + Template动态着色能力

推荐工作流

1. 设计师提供 SVG 图标

2. 优化 SVG(移除复杂特性)

3. 放入 resources/base/media/

4. 使用 $r('app.media.xxx') 引用

5. 配合 renderMode + fillColor 实现主题化

下一步学习


相关资源:

本站总访问