前言

图标这类组件,平时看起来很小,真正影响体验的地方却很多。按钮按下时有没有反馈,状态切换时顺不顺,禁用态是不是一眼能看懂,这些细节都会直接影响用户对应用质感的判断。

鸿蒙的 SymbolGlyph 从 API version 11 开始支持,主要用来渲染系统图标小符号。到了 API 20,这个组件补进了几项非常实用的能力,ReplaceEffectType.CROSS_FADE 用来做快速替换动效,SLASH_OVERLAY 用来做禁用态斜杠遮罩,symbolShadow 用来直接给图标加阴影,shaderStyle 则把渐变填充放进了图标渲染链路里。这样一来,很多原来要靠多层布局或自定义绘制才能做出来的效果,现在可以直接落在组件本身。

一、这次升级最值得关注的四项能力

先看快速替换动效。ReplaceEffectType.CROSS_FADE 的效果很直接,旧图标淡出的同时,新图标淡入,切换过程更连续,视觉上不会出现突然跳变。它很适合点赞、收藏、播放模式切换、勾选状态切换这类高频交互。华为开发者文档里对这个枚举的说明也很明确,CROSS_FADE 就是快速替换动效。

再看阴影。symbolShadow 从 API 20 开始可用,作用也很清楚,直接给 SymbolGlyph 增加原生阴影能力。开发时不需要额外包一层 Stack 或自己画影子,图标层次感可以直接交给组件本身处理。这个能力很适合用在重要操作按钮、深色背景上的主图标、需要做轻微悬浮感的入口图标。

第三项是禁用态动效。SLASH_OVERLAY 会在图标上覆盖一层斜杠遮罩,用来表示当前功能不可用或暂时不可操作。这个效果的表达很直接,用户看到后不需要额外理解成本。系统图标文档里对这一点写得很清楚,斜杠遮罩就是典型的禁用或非活动状态表达。

第四项是渐变填充。shaderStyle 在 API 20 这条线接进了 SymbolGlyph,线性渐变、纯色 shader 这些能力都可以直接作用在图标上。品牌色图标、活动页主视觉、需要强调情绪化表达的场景,都可以直接吃到这条能力。

二、快速替换动效和阴影

这两项能力最适合优先接到用户点得最多的地方。收藏按钮、点赞按钮、加购按钮、播放控制按钮,这些都是典型场景。状态变化一旦频繁,图标切换够不够顺,用户会立刻感知到。

下面这个例子就是一个比较稳的写法。点击图标时,用 CROSS_FADE 触发快速替换,同时保留状态变量,后面如果要接业务接口也方便扩展。

@Entry
@Component
struct FavoriteButtonDemo {
  @State isFavorite: boolean = false
  @State triggerValue: number = 0

  build() {
    Column({ space: 16 }) {
      SymbolGlyph(this.isFavorite ? $r('sys.symbol.heart_fill') : $r('sys.symbol.heart'))
        .fontSize(32)
        .fontColor(this.isFavorite ? Color.Red : Color.Black)
        .symbolEffect(
          new ReplaceSymbolEffect(EffectScope.WHOLE, ReplaceEffectType.CROSS_FADE),
          this.triggerValue
        )
        .onClick(() => {
          this.isFavorite = !this.isFavorite
          this.triggerValue++
        })
    }
    .width('100%')
    .padding(24)
  }
}

阴影这项能力更适合做层次感和触感。比如一个加号按钮,普通状态下影子稍微重一点,按下时影子收缩,视觉上就会更像一个真实可按压的控件。

@Entry
@Component
struct ShadowButtonDemo {
  @State isPressed: boolean = false

  private normalShadow: ShadowOptions = {
    radius: 8,
    color: '#33000000',
    offsetX: 2,
    offsetY: 2
  }

  private pressedShadow: ShadowOptions = {
    radius: 4,
    color: '#22000000',
    offsetX: 1,
    offsetY: 1
  }

  build() {
    Column() {
      SymbolGlyph($r('sys.symbol.plus_circle'))
        .fontSize(48)
        .symbolShadow(this.isPressed ? this.pressedShadow : this.normalShadow)
        .gesture(
          LongPressGesture({ repeat: false })
            .onAction(() => {
              this.isPressed = true
            })
            .onActionEnd(() => {
              this.isPressed = false
            })
        )
    }
    .width('100%')
    .padding(24)
  }
}

这类写法很适合放到首页主操作入口和工具栏核心按钮上。层次出来之后,界面会显得更稳,也更容易形成视觉焦点。symbolShadow 既然已经进了组件本身,项目里就没有必要再为图标影子单独搭复杂布局。

三、禁用态和渐变填充,重点在状态表达和品牌化

禁用态最怕两件事。第一件事是表达不清,用户看不出为什么点不了。第二件事是处理太粗糙,整个图标一灰到底,信息量很弱。SLASH_OVERLAY 这类斜杠遮罩就比较适合做这件事,状态语义很强,也更贴近系统图标的表达方式。

下面这个例子适合用在权限受限、会员未解锁、功能暂不可用这类场景里:

@Entry
@Component
struct DisabledSymbolDemo {
  @State hasPermission: boolean = false
  @State triggerValue: number = 0

  build() {
    Column({ space: 12 }) {
      SymbolGlyph($r('sys.symbol.lock_shield'))
        .fontSize(40)
        .symbolEffect(
          new ReplaceSymbolEffect(EffectScope.WHOLE, ReplaceEffectType.SLASH_OVERLAY),
          this.triggerValue
        )
        .fontColor(this.hasPermission ? '#007DFF' : '#666666')

      Button('切换权限状态')
        .onClick(() => {
          this.hasPermission = !this.hasPermission
          this.triggerValue++
        })
    }
    .padding(24)
  }
}

渐变填充更适合品牌化和视觉强调。普通工具页和设置页未必需要它,活动页、会员页、奖励页、品牌图标入口就很适合。shaderStyle 这项能力加进来之后,图标终于可以直接承载渐变色,而不是继续依赖外层容器或贴图方案。

@Entry
@Component
struct GradientSymbolDemo {
  private brandGradient: LinearGradientOptions = {
    angle: 135,
    colors: [
      ['#FF4D4F', 0.0],
      ['#FF7A45', 0.5],
      ['#FFC53D', 1.0]
    ]
  }

  build() {
    Column() {
      SymbolGlyph($r('sys.symbol.star'))
        .fontSize(64)
        .shaderStyle([new LinearGradientStyle(this.brandGradient)])
    }
    .width('100%')
    .padding(24)
  }
}

这里有一个实际开发时要记住的点,渐变一旦接进图标本身,品牌图标和普通功能图标就要分开管理。品牌入口适合用渐变做强调,普通图标大量上渐变会把界面做花,信息层级也会乱掉。

四、项目里怎么组合

这四项能力真正有价值的地方,不在单独使用,而在组合后的控制力。收藏按钮适合快速替换动效。主操作按钮适合阴影。禁用态图标适合斜杠遮罩。品牌入口适合渐变。把它们分到合适的场景里,图标系统会很快从能用变成好用。

一个比较实用的策略,是先做三个分层。

第一层是高频交互图标。优先接 CROSS_FADE,把状态切换做顺。
第二层是核心入口图标。优先接 symbolShadow,把重点操作做出来。
第三层是品牌和活动图标。优先接 shaderStyle,把视觉识别拉开。

禁用态这层单独做规则,不和普通态混写。哪些图标用斜杠遮罩,哪些继续保留颜色变化,这件事最好在组件层统一封一下。这样做之后,业务页面直接拿现成组件,视觉风格会统一很多。

比如可以把常见收藏按钮封成一个业务组件,后面项目里所有收藏交互都走同一套写法:

@Component
export struct AppFavoriteSymbol {
  @Prop active: boolean
  @Prop triggerValue: number

  build() {
    SymbolGlyph(this.active ? $r('sys.symbol.heart_fill') : $r('sys.symbol.heart'))
      .fontSize(28)
      .fontColor(this.active ? '#FF4D4F' : '#222222')
      .symbolEffect(
        new ReplaceSymbolEffect(EffectScope.WHOLE, ReplaceEffectType.CROSS_FADE),
        this.triggerValue
      )
  }
}

这种封法对项目很重要。图标的动态效果一旦散在各个页面里,后面统一改风格会非常痛苦。提前把通用交互组件收掉,后面维护成本会低很多。

总结

鸿蒙 6 API 20 给 SymbolGlyph 补进来的这四项能力,真正解决的是图标动态效果和状态表达的落地问题。CROSS_FADE 让状态切换更顺,symbolShadow 让图标层次更清楚,SLASH_OVERLAY 让禁用态更容易识别,shaderStyle 则把图标的品牌化表达能力拉高了一个层级。

项目里更稳的接法也很清楚。高频交互先接快速替换,核心入口优先接阴影,禁用态单独做规范,品牌图标再用渐变拉开层级。把这些规则收进组件层,后面的页面开发会顺很多,整体观感也会更统一。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐