图标是UI里最高频的资源。一个中型项目,各种图标加起来几十上百个:导航栏、列表项、操作按钮、状态提示。很多项目一开始直接往media目录里丢PNG,用的时候$r(‘app.media.icon_xxx’)引用。图标一多,问题就来了:同一个"返回"箭头在三个目录里有三个版本,颜色还不一样;想统一改成主题色,得重新导出几十张图;换肤功能根本做不了。

SVG矢量图标能解决这些问题:体积小、任意缩放不失真、颜色可以通过代码改。本文讲清楚鸿蒙里SVG怎么引入、怎么统一封装成图标库,代码可以直接复制到项目里用。

01 为什么用SVG而不是PNG

先说清楚为什么值得花时间做SVG图标库。

**体积小。**一个简单图标的SVG文件通常只有几百字节,PNG为了适配多倍屏,要准备1x、2x、3x三套,加起来几十KB。图标多了,包体积差距很明显。

**任意缩放不失真。**PNG放大就糊,需要准备多套尺寸。SVG是矢量,定义的是路径,80的图标和8的图标一样清晰。折叠屏、平板大屏适配的时候,不用再补图。

**颜色可以动态改。**单色SVG可以通过Image的fillColor属性改填充色,同一个图标能适配不同主题、不同状态(正常、选中、禁用),不用准备多份。

**好维护。**设计改了个描边粗细,重新导出一份SVG替换就行,不用批量重新切图。

代价是SVG渲染比PNG稍微多一点性能开销,但图标这种小尺寸图形完全感知不到。列表里几百个图标同时滚动这种极端场景,才需要考虑优化。

02 SVG引入方式

鸿蒙里引入SVG很简单,把文件放到resources/base/media目录下,用$r引用就行。系统会自动识别SVG格式。

entry/src/main/resources/
└── base/
    └── media/
        ├── ic_back.svg
        ├── ic_search.svg
        ├── ic_setting.svg
        └── ic_user.svg

页面里直接用Image组件展示:

@Entry
@Component
struct IconDemo {
  build() {
    Column({ space: 20 }) {
      // 直接引用资源
      Image($r('app.media.ic_back'))
        .width(24)
        .height(24)

      // 指定颜色(仅单色SVG有效)
      Image($r('app.media.ic_search'))
        .width(24)
        .height(24)
        .fillColor('#0066FF')

      // 用objectFit保持比例
      Image($r('app.media.ic_setting'))
        .width(32)
        .height(32)
        .objectFit(ImageFit.Contain)
    }
    .padding(20)
  }
}

fillColor是SVG图标最重要的能力。它会把SVG里所有填充替换成指定颜色,实现"一个图标文件,多种状态色"。但有前提:SVG必须是单色的。如果图标有多个颜色、有渐变、有阴影,fillColor不会生效,或者只替换部分。

所以做图标库的时候,规范要定死:所有图标统一用单色,靠fillColor改色。多色图标(比如品牌Logo)单独放,不走这套体系。

03 SVG文件规范

从设计那边拿到的SVG,不能直接用,要先检查几个东西,不然会出现各种奇怪问题。

**第一,viewBox要规整。**理想的SVG,viewBox是"0 0 24 24"或"0 0 32 32"这种正方形。如果是"0 0 24 18",图标本身就不是正方形,放到正方形容器里会留白或者被拉伸。导出的时候让设计统一按24x24的画板切。

**第二,去掉多余属性。**设计工具导出的SVG经常带一堆无用信息:命名空间、编辑器注释、隐藏图层。这些不影响渲染,但会让文件变大,还可能带上设计稿的绝对坐标导致偏移。

<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
  <path d="M12 4l-8 8h5v8h6v-8h5z" fill="#000000"/>
</svg>

保留width、height、viewBox、path就够了。

**第三,区分fill和stroke。**有的图标是填充图形(fill),有的是线条描边(stroke)。fillColor只改fill,不改stroke。如果图标是描边的,fillColor可能不起作用,需要改成fill形式,或者在SVG里把stroke色也统一。

**第四,不要动画和滤镜。**鸿蒙的SVG渲染对动画、滤镜、渐变支持有限。SMIL动画、复杂的filter效果在这些场景下不会生效。图标就做静态的、单色的,效果最稳。

04 统一图标库封装

直接在每个页面写$r(‘app.media.ic_xxx’)有几个问题:资源名容易写错、尺寸和颜色每个页面自己定、图标改了名要全项目搜替换。封装一个统一的Icon组件,这些问题都能解决。

思路是:把所有图标资源集中在一个枚举里,页面只传图标名,尺寸、颜色由组件统一处理。

// common/components/icon/IconName.ets
export enum IconName {
  BACK = 'ic_back',
  SEARCH = 'ic_search',
  SETTING = 'ic_setting',
  USER = 'ic_user',
  HOME = 'ic_home',
  CART = 'ic_cart',
  CLOSE = 'ic_close',
  ARROW_RIGHT = 'ic_arrow_right',
  SUCCESS = 'ic_success',
  ERROR = 'ic_error'
}

枚举值就是资源文件名。加新图标的时候,往media里放文件,枚举里加一行,其他地方就能用了。

然后是封装Icon组件:

// common/components/icon/AppIcon.ets
import { IconName } from './IconName';

@Component
export struct AppIcon {
  // 图标名,必传
  name: IconName = IconName.BACK;
  // 尺寸,默认24
  size: number = 24;
  // 颜色,默认主题色
  color: string = '#333333';
  // 是否可点击
  onClick?: () => void;

  build() {
    Image($r(`app.media.${this.name}`))
      .width(this.size)
      .height(this.size)
      .fillColor(this.color)
      .objectFit(ImageFit.Contain)
      .onClick(() => {
        if (this.onClick) {
          this.onClick();
        }
      })
  }
}

注意这里$r用的是模板字符串拼接,资源名从枚举里来。这样页面写的时候不可能拼错,枚举有代码提示,改名字也是全局改。

页面里用起来就是一行:

import { AppIcon, IconName } from '../common/components/icon';

@Entry
@Component
struct HomePage {
  build() {
    Column({ space: 16 }) {
      // 默认24大小,默认颜色
      AppIcon({ name: IconName.SEARCH })

      // 自定义尺寸和颜色
      AppIcon({ name: IconName.SETTING, size: 28, color: '#0066FF' })

      // 带点击事件
      AppIcon({
        name: IconName.BACK,
        size: 24,
        color: '#333333',
        onClick: () => {
          console.info('返回');
        }
      })
    }
    .padding(20)
  }
}

这样一个组件覆盖了90%的图标使用场景。尺寸统一用数字传,不用每个页面写width、height两行。

05 主题色与状态管理

图标库真正有价值的地方,是能跟着主题和状态自动变。比如深色模式切换的时候,图标颜色要跟着从深变浅;Tab栏选中和未选中,图标颜色不一样。

可以把颜色做成统一的配置,不散落在各个页面。

// common/components/icon/IconColor.ets
export class IconColor {
  // 主色
  static readonly PRIMARY = '#0066FF';
  // 正文色
  static readonly TEXT = '#333333';
  // 次要文字
  static readonly SECONDARY = '#666666';
  // 禁用态
  static readonly DISABLED = '#CCCCCC';
  // 反白
  static readonly WHITE = '#FFFFFF';
}

再进一步,做深色模式适配。用一个状态管理当前主题,Icon组件自动读当前色值。

@Component
export struct AppIcon {
  @Prop name: IconName = IconName.BACK;
  @Prop size: number = 24;
  // 不传颜色时,按主题自动取
  @Prop color: string = '';
  @StorageProp('isDarkMode') isDarkMode: boolean = false;

  private getIconColor(): string {
    if (this.color !== '') return this.color;
    return this.isDarkMode ? '#FFFFFF' : '#333333';
  }

  build() {
    Image($r(`app.media.${this.name}`))
      .width(this.size)
      .height(this.size)
      .fillColor(this.getIconColor())
      .objectFit(ImageFit.Contain)
  }
}

页面里不传color的时候,图标自动跟随当前主题。这种设计让换肤、深色模式变成零成本——切一下全局状态,所有图标自动变色。

Tab栏这种选中态场景,也可以直接传不同颜色:

AppIcon({
  name: IconName.HOME,
  size: 24,
  color: this.currentTab === 0 ? IconColor.PRIMARY : IconColor.SECONDARY
})

06 图标库目录组织

图标多了之后,文件命名和目录结构要定规则,不然很快就乱。

resources/base/media/
├── ic_back.svg
├── ic_search.svg
├── ic_setting.svg
├── ic_tab_home.svg
├── ic_tab_home_active.svg
└── ic_logo_brand.svg        # 多色图标单独命名

common/components/icon/
├── IconName.ets             # 图标枚举
├── IconColor.ets            # 颜色常量
├── AppIcon.ets              # 通用图标组件
└── Index.ets                # 统一导出

命名规则统一用ic_模块_功能的格式:

  • ic_:前缀固定,一眼区分图标和其他图片资源

  • tab_home:模块名,Tab栏首页

  • active:状态后缀,表示选中态

不要用icon1、icon2这种名字,也不要中英文混着起。半年后自己都不知道ic_new_2是干嘛的。

Index.ets统一导出,外部只从一个入口进:

// common/components/icon/Index.ets
export { AppIcon } from './AppIcon';
export { IconName } from './IconName';
export { IconColor } from './IconColor';

// 外部使用:
// import { AppIcon, IconName, IconColor } from '../common/components/icon';

这样以后图标组件内部怎么拆分、怎么重构,外部调用代码都不用改。模块化项目里,这一层收口尤其重要。

落地注意事项

**图标尺寸用偶数。**20、24、28这种整数值,不要用23.5这种小数。小数会导致渲染时半像素偏移,边缘看起来发虚。

**一个图标一个文件。**不要把多个图标塞进一个SVG里。虽然技术上能做,但fillColor会一起改所有图形的颜色,没法单独控制。

**fillColor不生效先检查文件。**如果改了颜色没反应,先用文本编辑器打开SVG,看是fill还是stroke,看有没有多个颜色。这是90%的原因。

**多色图标单独处理。**品牌Logo这类必须保留原色的,不走AppIcon组件,直接Image引用,也不要设fillColor。

**不要给图标加阴影和圆角。**SVG本身不支持CSS式的圆角,圆角要在图形里画。复杂效果建议让设计重新导出,不要在前端用滤镜凑。

小结

SVG图标库的价值不在于"用了矢量图",而在于统一。统一资源命名、统一尺寸、统一颜色来源、统一调用方式。页面里不再出现零散的$r引用,而是AppIcon加IconName这种结构化的写法。

实际落地分三步:先把现有PNG图标转成单色SVG,统一定好24x24的画板;再做Icon枚举和AppIcon组件,把页面里的图标引用逐步替换;最后接上主题色配置,让图标跟着深色模式和选中态自动变。走完这三步,图标这块基本就不用再操心了。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐