客户端的图标全部由本项目自行绘制,不使用 Material Icons、SF Symbols 或任何第三方图标字体。 这样做有两个原因:版权——图标集通常带有自己的授权条款,SF Symbols 更是禁止在非 Apple 平台再分发;一致性——过去界面上混用了 Material 的 outlined、填充和 rounded 三种变体, 线宽、圆角和视觉重量互不相同,同一屏里就能看出来不是一套。

位置

路径职责
lib/shared/icons/raku_glyph.dart绘制基元与 24×24 网格规则,含一个精简的 SVG 路径解析器
lib/shared/icons/glyphs/字形本体,按导航/操作、内容/语言、身份/场景分三个文件
lib/shared/icons/raku_icons.dart图标目录:每个字形一个 static const,外加用于遍历的 all
lib/shared/icons/raku_icon.dartRakuIcon 组件、RakuIconSizeRakuIconStyle
lib/shared/icons/icons.dart对外统一导出
图标是代码而非资产,因此不进 pubspec.yaml,也不参与字体子集;未被引用的字形会被 tree shaking 直接移除。

使用

import 'package:rakullapp_core/shared/icons/icons.dart';

const RakuIcon(RakuIcons.search)                       // 取主题的尺寸与颜色
const RakuIcon(RakuIcons.trash, size: RakuIconSize.md) // 显式尺寸
RakuIcon(RakuIcons.heart, color: AppColors.danger)     // 显式颜色
RakuIcon 的行为与 Flutter 的 Icon 一致:不传参数时从 IconTheme 取尺寸和颜色。 主题在 lib/main.dartappTheme 中统一设定(RakuIconSize.lg = 24, colorScheme.onSurface),这是全应用唯一决定图标默认尺寸与颜色的地方 尺寸只允许取 RakuIconSize 上的档位:16 / 20 / 24 / 32 / 40 / 48 / 64。不要写 18、22、 28 这类中间值——图标尺寸不成体系,正是原先视觉不齐的另一半原因。 设计系统组件(GlassButtonGlassChipGlassDialogGlassIconButtonGlassSegmented)的 icon 参数接收的是 Widget 而不是 IconData,并在内部套一层 IconTheme 决定尺寸与前景色。UILibrary 因此不绑定任何一套图标词汇:
GlassButton(label: '上传', icon: const RakuIcon(RakuIcons.upload), onPressed: ...)

绘制规则

所有字形画在 24×24 网格上,四周留 3 单位内边距,即活动区为 3–21。规则写在 RakuGlyph 的文档注释里,要点:
  • 线宽、线帽和连接方式属于画笔而不是字形,由 RakuIconStyle 统一决定,字形函数 无权覆盖。这保证了不会出现”某个图标比旁边粗一点”。
  • 圆形容器一律 r = 8.5、圆心 (12, 12);圆内的加号/叉号伸到 ±4,独立的伸到 ±7
  • 端点落在四分之一网格值上。
  • 箭头用共享的 arrowHead / arcArrow 基元,不要手算坐标——箭头出现在六个字形上, 手摆坐标就会摆出六种不同的箭头。
  • 填充只表示”开启”状态(例如 heartFilled 之于 heart),不用来做风格变体。

新增字形

  1. glyphs/ 下合适的文件里写 void drawXxx(RakuGlyph g),只使用 RakuGlyph 的基元。
  2. raku_icons.dart 里加一个 static const,并加入 all
  3. 跑几何测试与联络表(见下)。
优先复用已有字形。Material 的 outlined / 填充 / rounded 变体在这套集合里合并成同一个 字形;把 121 个 Material 标识符合并到 100 个字形,本身就是这次改动的一半价值。

校验

cd rakullapp_core
# 几何规则:活动区、视觉尺寸下限、居中
flutter test test/shared/icons/raku_icons_test.dart
# 联络表:把全部字形连同名称渲染成一张 PNG
flutter test test/shared/icons/icon_contact_sheet.dart   # -> build/icon_contact_sheet.png
几何测试守住”整套图标视觉尺寸一致”这条底线:溢出活动区的字形在同样标称尺寸下会显得更大。 注意它测量的是路径实际经过的范围,不是 Path.getBounds()——后者是保守边界,会把贝塞尔 控制点算进去,一段 90° 圆弧的控制点在 r × √2 处,半径 8.5 的整圆会被误判成占满整个网格。 联络表是必要的一步:靠读源码判断一套线性图标是否协调是做不到的,两个字形在视觉尺寸或 线条节奏上不一致,只有并排看才看得出来。

在测试中查找图标

find.byIcon 匹配的是 IconData,只有字体图标才有。自绘图标用 test/helpers.dart 中的:
findRakuIcon(RakuIcons.trash)
findWidgetWithRakuIcon(IconButton, RakuIcons.checklist)
图标都是 const,因此按标识比较是精确的。

同步到 Figma

设计稿里的图标不是画出来的,是从这里编译过去的tool/glyphs_to_svg.pyglyphs/ 里的字形函数直接转成 SVG 路径数据,再由 Figma 侧建成 99 个 24×24 组件 (Components 页 → Icons 分区),命名为 Icon / <名字>
cd rakullapp_core && python3 tool/glyphs_to_svg.py > /tmp/glyphs.json
之所以能用”转译”而不是”解释”,是因为字形函数里没有任何控制流——每个函数就是一串基元调用。 改过 glyphs/*.dart 之后重跑一次并更新 Figma 组件,两边就不会漂移。设计稿里需要图标时 一律用组件实例,不要手绘矢量,也不要从截图上描——在建立这条管线之前,每个画板都各画各的, 同一个图标在不同页面上都不一样,而且没有一个和应用里的一致。 组件里的描边与填充矢量都带 constraints: SCALE,所以实例缩放到 20 或 28 时线宽会跟着缩放, 与 RakuIconsize.shortestSide / RakuGlyph.grid 缩放的行为一致。 转译器主要在处理 Figma 侧的三个限制:vectorPaths 只接受绝对坐标的 M/L/C/ZHVSQT 和相对坐标都会报 Invalid command,因此二次贝塞尔要升成三次); Dart 里共享的路径常量与相邻字符串字面量要先解析拼接;以及 Figma 会丢弃零长度路径,所以圆点 必须是真正的填充圆。完整说明见 .cursor/skills/figma-sync/mapping.md