Expo Symbols(expo-symbols)跨平台符号图标方案:SF Symbols 与 Material Symbols 的集成指南
【免费下载链接】expoAn open-source framework for making universal native apps with React. Expo runs on Android, iOS, and the web.项目地址: https://gitcode.com/GitHub_Trending/ex/expo
Expo Symbols 是 Expo SDK 中负责跨平台符号图标的官方模块,它在 iOS/tvOS 上渲染 Apple 的 SF Symbols,在 Android 与 Web 上渲染 Material Symbols,让开发者用同一套声明式SymbolView组件即可在多个平台展示高质量的系统图标。本文将围绕packages/expo-symbols的版本演进与源码实现,完整讲解其安装配置、组件 API、跨平台名称映射、动画效果与已知问题,帮助你直接上手并在项目中规避踩坑点。
一、模块定位与平台覆盖
从 README.md 的定位描述与 package.json 的说明来看,expo-symbols 的职责非常清晰:
Provides access to native symbol libraries across platforms for React Native and Expo apps. Uses SF Symbols on iOS/tvOS and Material Symbols on Android and web.
具体到平台支持,以当前仓库(版本 57.0.1)为准:
| 平台 | 图标来源 | 渲染实现 |
|---|---|---|
| iOS | SF Symbols) | |
| tvOS | SF Symbols | 同 iOS,使用固定pointSize: 18.0(见 SymbolView.swift) |
| macOS | SF Symbols | macOS 14+ 支持符号动画,默认 scale 为.medium(见 SymbolView.swift) |
| Android | Material Symbols | 通过expo-font加载 Material Symbols 字体渲染(见 src/SymbolView.tsx) |
| Web | Material Symbols | 与 Android 共用同一套字体渲染路径 |
需要说明的是,从版本演进看,平台能力是逐步补齐的:Android/Web 的 Material Symbols 支持在 55.0.0 中加入(CHANGELOG.md 中 #39516),macOS 支持在 56.0.6 中加入(#46471)。
二、安装与工程配置
托管(managed)Expo 项目
直接使用 Expo CLI 安装即可,命令会自动选取与当前 SDK 匹配的版本:
npx expo install expo-symbols裸(bare)React Native 工程
根据 README.md 的指引,裸工程需要先完成expo包本身的安装与配置,再执行:
npx expo install expo-symbols npx pod-install其中npx pod-install用于同步 iOS 的原生依赖。
依赖关系
从 package.json 可以看到模块自身的依赖设计:
- 运行时依赖:
@expo-google-fonts/material-symbols(Android/Web 的 Material Symbols 字体包)、sf-symbols-typescript(提供 iOS SF Symbols 的类型名称,保证name属性有完整的类型提示); - peer 依赖:
expo、expo-font、react、react-native——其中expo-font是 Android/Web 渲染路径的关键(源码中用loadAsync加载字体); - 子路径导出:
./androidWeights/*子路径专门导出各字重对应的字体资源,供 Android/Web 端按需加载。
三、SymbolView 组件核心 API
SymbolView是模块对外的主要组件,从 src/index.ts 可以看出模块整体导出结构:
export type { SFSymbol } from 'sf-symbols-typescript'; export type { AndroidSymbol } from './android'; export { unstable_getMaterialSymbolSourceAsync } from './materialImageSource'; export type * from './SymbolModule.types'; export { SymbolView } from './SymbolView';组件的全部 Props 定义在 src/SymbolModule.types.ts,下面按用途分组说明。
基础用法
import { SymbolView } from 'expo-symbols'; export default function App() { return ( <SymbolView name="house.fill" tintColor="#007AFF" size={32} /> ); }Props 详解
| Prop | 类型 | 默认值 | 平台 | 说明 |
|---|---|---|---|---|
name | SFSymbol \| { ios?, android?, web? } | 必填 | 全部 | 符号名称;传对象可做跨平台映射(见下文) |
fallback | ReactNode | — | 全部 | 当某平台未定义符号时渲染的兜底内容 |
type | 'monochrome' \| 'hierarchical' \| 'palette' \| 'multicolor' | 'monochrome' | iOS | 符号变体(渲染模式) |
scale | 'default' \| 'unspecified' \| 'small' \| 'medium' \| 'large' | 'unspecified' | iOS | 符号缩放级别 |
weight | SymbolWeight \| { ios, android } | 'unspecified' | 全部 | 字重;Android/Web 端需从androidWeights子路径导入对应字重 |
colors | ColorValue \| ColorValue[] | — | iOS | palette模式下的调色板颜色数组 |
size | number | 24 | 全部 | 符号尺寸 |
tintColor | ColorValue | 见下文 | 全部 | 着色颜色 |
resizeMode | ContentMode | 'scaleAspectFit' | iOS | 图片在容器内的缩放模式 |
animationSpec | AnimationSpec | — | iOS | 动画配置(见下文) |
SymbolWeight的取值集合为:'unspecified' | 'ultraLight' | 'thin' | 'light' | 'regular' | 'medium' | 'semibold' | 'bold' | 'heavy' | 'black'。
ContentMode的取值集合为:'scaleToFill' | 'scaleAspectFit' | 'scaleAspectFill' | 'redraw' | 'center' | 'top' | 'bottom' | 'left' | 'right' | 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight'。
平台映射与默认兜底
当传入name为字符串时,iOS/Android/Web 会直接使用该名称查找对应平台符号库中的符号;若希望为不同平台显式指定不同符号,可传入对象:
<SymbolView name={{ ios: 'house.fill', android: 'home', web: 'home', }} />Android 端的符号名类型AndroidSymbol来自 src/android/index.ts 中由symbols.json推导出的联合类型,androidSymbolToString会将符号名映射为字体中对应的 Unicode 码点(String.fromCharCode(symbols[symbol]))。
当平台未定义符号时(例如只提供了android未提供ios),会渲染fallback属性;iOS 实现里还会在原生 View 不存在时兜底渲染 fallback(见 src/SymbolView.ios.tsx)。
Android/Web 的颜色默认值
Android 端默认着色使用了系统平台色(源码 src/SymbolView.tsx):
const DEFAULT_SYMBOL_COLOR = Platform.OS === 'android' ? PlatformColor('@android:color/system_primary_dark') : '#7d9bd4';即 Android 使用系统 primary dark 色,Web 端默认回退为#7d9bd4。同时该文件也确认:Android/Web 端SymbolView实际是「View + Text」组合,通过expo-font的loadAsync异步加载对应字重的 Material Symbols 字体,再用androidSymbolToString渲染字形,lineHeight与fontSize都取size以保证符号占据正确的方形空间(对应 CHANGELOG 中 #41091 的修复)。
四、动画支持(iOS 17+ / macOS 14+)
animationSpec是 iOS 平台专属能力,底层映射到 Apple 的SymbolEffect(见 ios/SymbolView.swift 中addSymbolEffects的实现)。
AnimationSpec 结构
type AnimationSpec = { effect?: AnimationEffect; repeating?: boolean; repeatCount?: number; speed?: number; variableAnimationSpec?: VariableAnimationSpec; }; type AnimationEffect = { type: 'bounce' | 'pulse' | 'scale'; wholeSymbol?: boolean; // 默认 false,true 表示整个符号一起动画 direction?: 'up' | 'down'; };原生侧的行为(对应 SymbolView.swift):
repeating默认为false,映射为SymbolEffectOptions.repeating / .nonRepeating;repeatCount通过options.repeat(abs(repeatCount))设置重复次数;speed通过options.speed(speed)设置动画速度;- 若提供了
variableAnimationSpec,优先调用addSymbolEffect(variableAnimationSpec.toVariableEffect())走可变色动画分支。
VariableAnimationSpec 可变色动画
type VariableAnimationSpec = { reversing?: boolean; // 每次重复时反向 nonReversing?: boolean; // 每次重复时不反向 cumulative?: boolean; // 各层依次点亮并保持到动画结束,会取消 iterative iterative?: boolean; // 各层短暂点亮后恢复 hideInactiveLayers?: boolean; // 完全隐藏非激活层 dimInactiveLayers?: boolean; // 以降低的不透明度绘制非激活层 };这些效果是叠加的——每个置为true的字段都会额外叠加一个效果。使用示例:
<SymbolView name="wifi" animationSpec={{ effect: { type: 'bounce', direction: 'up' }, repeating: true, }} />注意:原生实现中符号动画依赖if #available(iOS 17.0, tvOS 17.0, macOS 14.0, *)的运行时判断,即 iOS 17 / tvOS 17 / macOS 14 以下系统会自动跳过动画部分,因此动画能力与系统版本强相关。
五、Android/Web 字重加载
Android/Web 端由于采用字体渲染,不同字重对应不同字体文件。按 SymbolModule.types.ts 中weight的说明,Android/Web 端应通过子路径导入:
// 先导入字重字体资源 import 'expo-symbols/androidWeights/regular'; import 'expo-symbols/androidWeights/bold'; // 再渲染 <SymbolView name="star" weight={{ ios: 'bold', android: 'regular' }} tintColor="#FFD700" size={28} />仓库中 src/android/weights/ 目录下提供了regular、bold、light、medium、semiBold、thin、extraLight等字重入口,对应 Material Symbols 字体的细分字重。weight也支持传对象分别指定 iOS 与 Android 的字重。
六、实用辅助 API:unstable_getMaterialSymbolSourceAsync
对于需要ImageSourcePropType而非组件的场景(例如 tab bar 图标),模块从 55.0.0 开始提供unstable_getMaterialSymbolSourceAsync(#41064)。其实现位于 src/materialImageSource.ts:
export async function unstable_getMaterialSymbolSourceAsync( symbol: AndroidSymbol | null, size: number, color: string ): Promise<ImageSourcePropType | null>工作流程:
- 将 Material 符号名转换为字体字形码点(
androidSymbolToString); - 用
expo-font的loadAsync确保regular字重字体已加载; - 调用
expo-font的renderToImageAsync(fontChar, { fontFamily, size, color, lineHeight: size })将字形渲染成图片源。
若expo-font未提供renderToImageAsync(版本过旧),会输出警告并返回null;符号为空时也返回null。调用示例:
import { unstable_getMaterialSymbolSourceAsync } from 'expo-symbols'; const icon = await unstable_getMaterialSymbolSourceAsync('home', 24, '#007AFF');七、iOS 原生实现要点
iOS 端的原生视图位于 ios/SymbolView.swift,核心流程在reloadSymbolIOS()中:
- 通过
UIImage(systemName: name)创建符号图片; - 用
getSymbolConfig()构建UIImage.SymbolConfiguration:按symbolType分别应用preferringMonochrome()(iOS 16+)、hierarchicalColor、paletteColors(调色板颜色数 > 1 时)或preferringMulticolor(); - 非 hierarchical 模式下,通过
withTintColor(tint, renderingMode: .alwaysOriginal)应用着色; - 最后(iOS 17+)先
removeAllSymbolEffects()再按需添加动画效果。
JS 侧 src/SymbolView.ios.tsx 会做 Props 归一化:将colors统一为数组、用processColor处理颜色、计算animated布尔值、提取平台名称与字重,最后通过requireNativeView('SymbolModule')渲染原生视图。
macOS 端(#if os(macOS)分支)由于NSImage没有withTintColor,实现上改用contentTintColor进行着色,并把配置烘焙进新的NSImage(见 SymbolView.swift)。
八、版本演进与踩坑记录
以下是从 CHANGELOG.md 提炼的关键节点,能帮助你判断升级风险:
破坏性变更(Breaking changes)
- 56.0.0(2026-05-05):最低 iOS/tvOS 版本提升到16.4,macOS 提升到13.4(#43296)。升级到 56.x 前需确认工程的最低系统版本满足要求;
- 0.2.0(2024-10-22):iOS/tvOS 部署目标提升到15.1(#30840、#30865)。
新功能与稳定性
- Unpublished(即将发布):Expo Symbols 从 beta 转为stable(#48537),同时修复了非原生回退(Android/Web 字体渲染)中
styleprop 被忽略的问题(#48553); - 56.0.6:新增macOS 支持(#46471);
- 55.0.0:Android/Web 支持 Material Symbols(#39516),并加入
unstable_getMaterialSymbolSourceAsync(#41064); - 1.0.0(2025-08-13):修复 Android 上「native view manager isn't exported」警告(#38504);
- 0.4.5:改用
processColor,从而接受所有合法的颜色字符串(#36914); - 0.4.0:添加
PlatformColor到类型定义(#34890); - 0.1.4:新增
sizeprop 以对齐同类包 API(#28497)。
依赖相关
- 56.0.5:修复
@expo-google-fonts/material-symbols的useFonts对expo-font和react的未声明依赖问题(#45471),升级时建议同步更新该字体包; - 0.3.0:修复 Android 端
expo-modules.config的平台配置错误(#35849),并统一了expo-module.config.json的平台语法(#34445)。
版本节奏说明
本模块多数小版本(如 57.0.1、57.0.0、56.0.4 等)明确标注「不包含面向用户的功能变更」,属于与 SDK 主版本对齐的例行发布;需要关注的核心节奏是:55.x 补齐 Android/Web、56.x 补齐 macOS 与系统版本下限、当前主线将模块转正为 stable。
九、实践建议小结
- 跨平台应用优先使用对象形式的
name映射,并为缺失平台提供fallback,避免某些平台空白; - Android/Web 端记得导入字重子路径(如
expo-symbols/androidWeights/bold),否则字重可能不符合预期; - 需要图标作为图片源(如 tab bar)时,使用
unstable_getMaterialSymbolSourceAsync,同时注意该 API 依赖较新的expo-font; - 动画仅限 iOS 17+ / tvOS 17+ / macOS 14+,低版本会自动降级为静态符号,设计动画前先确认目标系统版本;
- 升级 56.x 前核对最低系统版本(iOS/tvOS 16.4、macOS 13.4),避免 CI 或真机不满足要求;
- 需要查阅最新 API 细节时,可继续阅读模块内 README.md、类型定义 src/SymbolModule.types.ts 以及原生实现 ios/SymbolView.swift,并结合官方 SDK 文档确认稳定版行为。
【免费下载链接】expoAn open-source framework for making universal native apps with React. Expo runs on Android, iOS, and the web.项目地址: https://gitcode.com/GitHub_Trending/ex/expo
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考