# Text 普通文字垂直居中偏下问题

**问题标签**: `Text`, `居中`, `偏下`, `垂直对齐`, `StrutStyle`, `字体`, `行高`, `topOffset`
**问题类型**: `UI 对齐`
**严重程度**: 高
**节省时间**: 多轮对话 → 1 次修复

---

## 问题描述

在 Row/Column/Container 中放置 `Text` widget，设计稿上文字在容器内垂直居中，但 Flutter 实际渲染时文字位置**视觉偏下**。

典型场景：
- 邀请码卡片中的大号文字（fs=24）在 height=105 的 Row 里偏下
- 任何固定高度容器内的中文 Text 视觉上不居中

---

## 问题根因

### 两重偏差叠加

**① Sketch "适应布局" 帧高留白（topOffset）**

Sketch Text 图层在"适应布局（Fit Content）"模式下：

```
帧高（frameHeight） ≈ fontSize × 1.375
topOffset = (frameHeight - fontSize) / 2
```

示例：fontSize=24 → frameHeight=33 → topOffset=4.5px

设计稿测量的 Y 坐标是帧的起点，**实际文字视觉上沿 = Y + topOffset**。
如果用原始 Y 坐标计算间距，偏差最大可达 4.5px。

**② Flutter 字体 ascent/descent 不对称**

Flutter 渲染 CJK 字体时，字形的上行距（ascent）默认大于下行距（descent），
即使容器用 `crossAxisAlignment.center`，字形的视觉重心也会**偏下**。

系统主动做补偿的唯一方式：通过 `StrutStyle.forceStrutHeight` 强制等量分配 leading。

---

## 测量数据解读

使用 `artboard-measure.js` 运行后，Text 图层会输出：

```json
{
  "text": {
    "content": "邀请码 ：",
    "fontSize": 24,
    "topOffset": 4.5,
    "visualHeight": 24
  },
  "layoutIntent": {
    "visualSpacing": {
      "note": "Text 图层存在 topOffset=4.5px，视觉上沿比 frame.y 低 4.5px",
      "paddingTopVisual": 40.5,
      "paddingBottomVisual": 40.5
    }
  }
}
```

`topOffset` 即是两重偏差中第一重的来源，同时也是 Flutter 修正所需的行高比例基准：

```
StrutStyle.height = frameHeight / fontSize = 33 / 24 = 1.375
```

---

## 正确方案

### 从 Sketch 获取数值

运行 `artboard-measure.js` 后查看：
- `text.fontSize`：字号
- `text.topOffset`：帧留白（= (frameHeight - fontSize) / 2）
- `layoutIntent.visualSpacing`：视觉修正后的实际间距

计算 `StrutStyle.height`：

```javascript
// 方式一：从 topOffset 反推
const strutHeight = (fontSize + topOffset * 2) / fontSize;
// 等价于：strutHeight = frameHeight / fontSize

// 方式二：直接用 frameHeight
const strutHeight = frameHeight / fontSize;  // 通常约 1.375
```

### Flutter 代码

```dart
Row(
  crossAxisAlignment: CrossAxisAlignment.center,
  children: [
    Text(
      '邀请码 ：',
      // forceStrutHeight 强制 leading 均匀分配于字形上下
      // height = frameHeight / fontSize = 33 / 24 = 1.375
      strutStyle: const StrutStyle(
        fontSize: 24,
        height: 1.375,
        forceStrutHeight: true,
        leading: 0,
      ),
      style: TextStyle(
        fontSize: 24,
        fontWeight: FontWeight.w600,
        color: DesignColors.textPrimary,
      ),
    ),
  ],
)
```

### 参数说明

| 参数 | 值 | 说明 |
|------|----|------|
| `fontSize` | 与 TextStyle 相同 | StrutStyle 必须指定字号 |
| `height` | `frameHeight / fontSize` ≈ 1.375 | 与 Sketch 帧高比例一致 |
| `forceStrutHeight` | `true` | 强制启用，否则 height 对 CJK 字体可能无效 |
| `leading` | `0` | 禁用额外行间距，让 height 完全控制 |

> ⚠️ **注意**：`StrutStyle` 只需设置在含文字的 `Text` widget 上，不是 `TextStyle`。
> `TextStyle.height` 控制行高，**不等于** `StrutStyle`，两者机制不同，不可混用。

---

## 与 TextField 居中问题的区别

| 场景 | 使用方案 | 关键参数 |
|------|---------|---------|
| **Text** 在固定容器里偏下 | `StrutStyle` | `forceStrutHeight: true, leading: 0` |
| **TextField** placeholder/光标偏下 | `style.height` + `contentPadding` | `isDense: true, contentPadding: EdgeInsets.zero` |

两个问题表现相似，但修复路径完全不同，不可混用。

---

## 举一反三

以下场景均可使用本方案：

- 大号标题文字在卡片中视觉偏下
- Icon + Text 水平排列，文字视觉位置比图标低
- 数字（Helvetica Bold）与中文单位（PingFang SC）混排时竖向不对齐
  → 分别给数字文字和单位文字设置各自的 `StrutStyle`

---

**维护团队**: MTA工作室
**创建日期**: 2026-02-25
**来源**: welfare_page.dart 邀请码卡片深度还原实战
