Flutter 像素级还原:设计稿到代码的视觉无损转换技巧
一、1px 的偏差,用户看得出来
做过 UI 还原的开发者都有这种经历:对着设计稿调了一下午,感觉"差不多了",结果设计师走过来扫一眼就说,"这里间距是 12px 不是 14px"、"这个阴影扩散半径不对"、"文字行高跟设计稿不一样"。
这种对话在 Flutter 开发中尤其常见,因为 Flutter 的渲染机制和 Web 完全不同。Web 里的 1px 就是 1 个 CSS 像素,但 Flutter 里的逻辑像素(logical pixel)和设备像素(device pixel)之间有 devicePixelRatio 的转换。再加上 Flutter 自带的 Material 主题会隐式覆盖你的样式------不搞清楚这些机制,你永远在做"差不多"的还原。
这篇文章,我会分享一套我用了两年的 Flutter 像素级还原工作流------从 Figma 参数提取到 Flutter 代码的精确映射,覆盖间距、圆角、阴影、字体、渐变五个最容易出偏差的维度。
二、Figma 参数到 Flutter 参数的精确映射表
间距:Figma 的 px 和 Flutter 的 px 等价吗?
答案:在逻辑像素层面等价,但前提是你设置对了。
Figma 默认使用 1x 设计尺寸(通常 375 或 390 宽度的 iPhone 设计稿)。Flutter 的逻辑像素在 iPhone 14 Pro(390×844 logical pixels)上天然就是 1:1 对应。但这里有一个陷阱:
dart
// 错误:在 MaterialApp 里开启 useMaterial3 可能导致默认有 padding
MaterialApp(
useMaterial3: true, // M3 的某些控件有隐式内边距
home: MyPage(),
)
// 正确:显式关闭不需要的默认样式
MaterialApp(
useMaterial3: true,
theme: ThemeData(
useMaterial3: true,
// 重置所有默认内边距
visualDensity: VisualDensity.standard,
// 关键:关闭 AppBar 的默认 elevation
appBarTheme: const AppBarTheme(
scrolledUnderElevation: 0,
elevation: 0,
),
),
)
阴影:Figma 的 4 个参数如何映射到 Flutter
Figma 的阴影参数是:X 偏移、Y 偏移、模糊(Blur)、扩散(Spread)、颜色+透明度。
Flutter 的 BoxShadow 接受:offset(对应 X/Y)、blurRadius(对应 Blur)、spreadRadius(对应 Spread)、color。
95% 的情况直接映射即可:
dart
// Figma: shadow X=0, Y=4, Blur=16, Spread=0, Color=#000 10%
BoxShadow(
offset: const Offset(0, 4),
blurRadius: 16,
spreadRadius: 0,
color: Colors.black.withOpacity(0.10),
)
但有一个卡住很多人的细节:Figma 的阴影色是用 rgba(0,0,0,0.1) 表示的,而 Flutter 的 withOpacity 会乘以当前 color 的 alpha。所以以下两种写法是不同的:
dart
// 写法 A:Colors.black.withOpacity(0.1) → alpha = 0.1 ✓
Colors.black.withOpacity(0.1)
// 写法 B:Color(0x1A000000) → alpha = 0x1A/0xFF ≈ 0.102 ✓
Color(0x1A000000)
// 写法 C:Colors.black.withOpacity(0.5).withOpacity(0.1) → alpha = 0.05 ✗ 错误!
规则:永远只调用一次 withOpacity,或者在构造函数中直接指定 alpha 通道。
三、五个最容易出偏差的细节及解决方案
1. 字体行高(Line Height)的 Flutter 换算
Figma 的行高有两种模式:固定值(如 20px)和百分比(如 150%)。Flutter 的 TextStyle.height 是一个乘数(multiplier),不是固定值:
dart
// Figma: fontSize=14, lineHeight=20 (固定)
// Flutter 换算: height = 20/14 ≈ 1.4286
TextStyle(
fontSize: 14,
height: 20 / 14, // = 1.4286
)
// Figma: fontSize=16, lineHeight=150% (百分比)
// Flutter 换算: height = 1.5
TextStyle(
fontSize: 16,
height: 1.5,
)
关键细节 :Flutter 的 height 是相对于 fontSize 的,但它的计算基准是字体的 em-square,不是实际字形高度。中文和英文混排时,行高会有视觉偏差。我的做法是:先算理论值,然后在设备上对比截图,通常需要 ±0.05 的微调。
2. 渐变角度的坐标系差异
Figma 的渐变角度:0° = 从左到右,顺时针旋转。
Flutter 的 LinearGradient.begin 和 end:基于 Alignment 坐标系,Alignment(-1, -1) 是左上角。
dart
// Figma: 渐变角度 135°(从左上到右下)
// Flutter 换算:
LinearGradient(
begin: Alignment.topLeft, // 对应 135° 的起点
end: Alignment.bottomRight, // 对应 135° 的终点
colors: [Color(0xFF667EEA), Color(0xFF764BA2)],
)
// 如果需要精确的任意角度:
import 'dart:math';
Alignment angleToAlignment(double degrees) {
final rad = (degrees - 90) * pi / 180; // 减 90° 因为 Flutter 坐标系差异
final dx = cos(rad);
final dy = sin(rad);
return Alignment(dx, dy);
}
// Figma: 角度 73°
LinearGradient(
begin: angleToAlignment(73 + 180), // 终点是对面
end: angleToAlignment(73),
colors: [...],
)
3. MediaQuery 的 padding 陷阱
dart
// 错误:直接使用 MediaQuery.of(context).size 做布局计算
// 这个 size 包含了状态栏、底部安全区等系统 UI
final screenHeight = MediaQuery.of(context).size.height;
// 正确:使用 padding 减去系统 UI 区域
final padding = MediaQuery.of(context).padding;
final safeHeight = MediaQuery.of(context).size.height - padding.top - padding.bottom;
4. 圆角的连续曲线 vs 标准曲线
Figma 支持"连续曲线"圆角(即 corner smoothing,iOS 的"超椭圆"圆角)。Flutter 不直接支持,但可以用 ContinuousRectangleBorder 近似:
dart
// Figma 的"圆角平滑度"(Corner Smoothing)设为 60%
// 使用 ContinuousRectangleBorder 作为近似
ClipRRect(
borderRadius: BorderRadius.circular(16),
// 或使用:
clipper: ShapeBorderClipper(
shape: ContinuousRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: ...,
)
5. 图片的缩放模式与 Figma 的 Fill/Fit 对应
| Figma 填充模式 | Flutter BoxFit | 说明 |
|---|---|---|
| Fill | BoxFit.fill |
拉伸填满(会变形) |
| Fit | BoxFit.contain |
完整显示(可能有留白) |
| Crop | BoxFit.cover |
最常用:裁剪填满 |
| None | BoxFit.none |
原始尺寸 |
四、我的像素级还原检查清单
每次做完 UI 还原,我会对照这份清单逐项自查:
- 间距检查 :在 Flutter Inspector 中开启"Show Layout Guidelines",逐一对照 Figma 的间距标注。特别注意
ListView的paddingvsitemExtent导致的额外间距。 - 阴影对比:截屏后和 Figma 导出图叠图对比(50% 透明度),重点关注阴影的扩散范围和颜色深度。
- 字体检查 :使用
flutter_screenutil或直接利用MediaQuery.textScaleFactor验证字体缩放后的表现。 - 圆角一致性:卡片、按钮、输入框的圆角值是否与设计系统一致------同一层级的所有容器圆角应该相同。
- 安全区验证:在 iPhone X 及以上机型的模拟器中检查底部安全区和灵动岛的适配。
- 暗黑模式:切换到暗黑模式后,所有颜色是否正确映射(参考上一篇文章的暗黑模式适配算法)。
五、总结
像素级还原的本质不是"看得像就行",而是"参数一一对应"。Figma 的每个设计参数(间距、圆角、阴影参数、字体配置、渐变角度)都有它在 Flutter 中的精确映射方式。知道这些映射规则,你能在半小时内完成以前需要两天的 UI 还原工作。更重要的------当设计师再来说"这里差 2px"的时候,你有底气告诉 TA,到底是谁的稿子标错了。
作者:李慕杰(Leo / 8limujie)
一个被设计师追着改了三年间距、终于写完这套映射表的前端匠人