figmascope 的默认导出目标是 Jetpack Compose。这并非随意——Compose 的布局模型(Column、Row、Box、Modifier)与 IR 节点类型(stack、overlay、absolute、leaf)紧密对应。Figma 中的垂直堆叠在 Compose 中就是 Column。这种转换是机械性的,使其非常适合 Agent 驱动的代码生成。
本演练详细介绍 IR 到 Compose 的映射,展示真实的 JSON 片段及其对应的 Composable,并解释令牌映射层。
为什么 Compose 是默认目标
三个结构性原因:
- 自动布局 ↔ Column/Row。Figma 的自动布局框架(现代 Figma 设计的绝大多数)导出为
kind: "stack"节点。Stack 节点有一个axis属性——vertical映射到Column,horizontal映射到Row。这是 1:1 映射,无需解读步骤。 - 间距令牌 ↔ dp 值。Compose 对所有布局尺寸使用
Dp。tokens.json中的令牌值是无单位整数(例如spacing.16 = 16),直接映射到16.dp。无需转换,无需缩放。 - 颜色令牌 ↔ Color composable。
tokens.json中的 Figma 十六进制值通过单次转换映射到Color(0xFFrrggbb)。令牌键在你的主题中成为语义变量名。
IR 节点类型及其 Compose 映射
| IR 类型 | 属性 | Compose 原语 |
|---|---|---|
stack | axis: "vertical" | Column |
stack | axis: "horizontal" | Row |
overlay | 分层子元素 | Box |
absolute | x、y、width、height | Box 加 Modifier.offset(x.dp, y.dp) |
leaf | type: "text" | Text 加 TextStyle |
leaf | 带填充的 type: "rectangle" | Box(Modifier.background(Color(...))) |
所有节点类型都可以带有 spacing 属性(子元素间距)和 padding 对象(上/右/下/左)。两者都引用令牌键。
令牌映射详解
tokens.json 文件如下所示:
{
"spacing": {
"4": 4, "8": 8, "12": 12, "16": 16,
"20": 20, "24": 24, "32": 32, "48": 48
},
"radius": {
"4": 4, "8": 8, "12": 12, "16": 16, "full": 9999
},
"color": {
"7f5cfe": "#7F5CFE",
"1a1a2e": "#1A1A2E",
"f6f2ea": "#F6F2EA",
"ffffff": "#FFFFFF",
"e53935": "#E53935"
},
"typography": {
"heading.24": { "size": 24, "weight": 700, "lineHeight": 1.2 },
"body.14": { "size": 14, "weight": 400, "lineHeight": 1.5 },
"label.12": { "size": 12, "weight": 500, "lineHeight": 1.4 }
}
}
映射规则:
spacing.16→16.dpradius.12→RoundedCornerShape(12.dp)color.7f5cfe→Color(0xFF7F5CFE)(前置0xFF表示完全不透明)typography.heading.24→TextStyle(fontSize = 24.sp, fontWeight = FontWeight.Bold, lineHeight = 28.8.sp)
令牌键故意不透明(颜色用十六进制字符串,间距用数字字符串),这样它们不会将模型偏向任何特定命名约定。你的 Compose 主题可以独立为它们起语义名称——colorPrimary、spacingMd——与 IR 无关。
真实示例:主屏幕 JSON 转 Composable
以下是一个简化的主屏幕 IR。一个垂直堆叠,包含一个标题叶节点和一个卡片列表:
{
"name": "home",
"kind": "stack",
"axis": "vertical",
"spacing": "spacing.24",
"padding": { "top": "spacing.16", "right": "spacing.16",
"bottom": "spacing.16", "left": "spacing.16" },
"fill": "color.f6f2ea",
"children": [
{
"kind": "leaf",
"type": "text",
"stringRef": "home.title",
"typography": "typography.heading.24",
"fill": "color.1a1a2e"
},
{
"kind": "stack",
"axis": "vertical",
"spacing": "spacing.12",
"children": [
{
"kind": "overlay",
"radius": "radius.12",
"fill": "color.ffffff",
"padding": { "top": "spacing.16", "right": "spacing.16",
"bottom": "spacing.16", "left": "spacing.16" },
"children": [
{
"kind": "leaf",
"type": "text",
"stringRef": "home.card.label",
"typography": "typography.label.12",
"fill": "color.7f5cfe"
}
]
}
]
}
]
}
对应的 Composable——Agent 应从此 IR 生成的内容:
@Composable
fun HomeScreen() {
Column(
modifier = Modifier
.fillMaxSize()
.background(Color(0xFFF6F2EA))
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(24.dp)
) {
Text(
text = stringResource(R.string.home_title),
style = TextStyle(
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
lineHeight = 28.8.sp,
color = Color(0xFF1A1A2E)
)
)
Column(
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Box(
modifier = Modifier
.clip(RoundedCornerShape(12.dp))
.background(Color(0xFFFFFFFF))
.padding(16.dp)
) {
Text(
text = stringResource(R.string.home_card_label),
style = TextStyle(
fontSize = 12.sp,
fontWeight = FontWeight.Medium,
lineHeight = 16.8.sp,
color = Color(0xFF7F5CFE)
)
)
}
}
}
}
Composable 中的每个值都可追溯到 IR 中的令牌键。没有硬编码值——16.dp 来自 spacing.16,24.dp 来自 spacing.24,Color(0xFF7F5CFE) 来自 color.7f5cfe。
字符串引用——stringResource 映射
IR 中的每个文本节点都带有点记法键的 stringRef。strings.json 文件将键映射到显示值和后备值:
{
"home.title": { "value": "Good morning", "fallback": "Good morning" },
"home.card.label": { "value": "Today's summary", "fallback": "Summary" }
}
点记法映射到 Android 字符串资源 ID,点替换为下划线:home.title → R.string.home_title。fallback 字段是在 strings.xml 中尚不存在资源时硬编码的字面字符串:
text = stringResource(R.string.home_title, "Good morning")
告诉 Agent 始终使用 fallback 字段——而非空字符串——这样在填充 strings.xml 之前,开发阶段屏幕是可读的。
绝对定位
kind: "absolute" 的节点直接使用 Figma 坐标。这些出现在具有重叠元素或锚定到特定位置的元素的设计中。Compose 映射使用 Box 作为父元素,在子元素上使用 Modifier.offset:
// IR: { "kind": "absolute", "x": 24, "y": 80, "width": 120, "height": 40 }
Box(modifier = Modifier.fillMaxSize()) {
Box(
modifier = Modifier
.offset(x = 24.dp, y = 80.dp)
.size(width = 120.dp, height = 40.dp)
) {
// 子元素
}
}
绝对定位在结构良好的 Figma 文件中不常见(大多数现代文件使用自动布局)。当你看到它时,检查设计意图是否真的是绝对定位,或者设计师只是没有应用自动布局约束——IR 无法推断意图。
诚实的局限
包很好地覆盖了常见情况。一些它不处理的内容:
- 渐变填充。IR 在遇到渐变时导出警告。节点的背景填充被省略。留下
// TODO: gradient并手动实现。 - 复杂效果。IR 中不表示投影和模糊。如果存在,它们会出现在
_meta.json警告中。 - 矢量图标。IR 存储图标节点的引用 ID,而非路径数据。你需要单独将图标解析为实际的 drawable 或 Compose 图标。
- 嵌套组件。IR 在实例节点上包含
componentId。components/inventory.json将 ID 映射到名称。单独实现组件,并在父 Composable 中按名称引用它。
这些局限是明确的——它们出现在 _meta.json 警告和 CONTEXT.md 中。Agent 不会静默地猜测通过它们。
从 figmascope 主应用导出上下文包,然后与 Claude Code 或 Cursor 一起使用,直接从 IR 实现 Composable。无截图猜测,无硬编码值。