Flutter 像素级还原:设计稿到代码的视觉无损转换技巧

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.beginend:基于 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 还原,我会对照这份清单逐项自查:

  1. 间距检查 :在 Flutter Inspector 中开启"Show Layout Guidelines",逐一对照 Figma 的间距标注。特别注意 ListViewpadding vs itemExtent 导致的额外间距。
  2. 阴影对比:截屏后和 Figma 导出图叠图对比(50% 透明度),重点关注阴影的扩散范围和颜色深度。
  3. 字体检查 :使用 flutter_screenutil 或直接利用 MediaQuery.textScaleFactor 验证字体缩放后的表现。
  4. 圆角一致性:卡片、按钮、输入框的圆角值是否与设计系统一致------同一层级的所有容器圆角应该相同。
  5. 安全区验证:在 iPhone X 及以上机型的模拟器中检查底部安全区和灵动岛的适配。
  6. 暗黑模式:切换到暗黑模式后,所有颜色是否正确映射(参考上一篇文章的暗黑模式适配算法)。

五、总结

像素级还原的本质不是"看得像就行",而是"参数一一对应"。Figma 的每个设计参数(间距、圆角、阴影参数、字体配置、渐变角度)都有它在 Flutter 中的精确映射方式。知道这些映射规则,你能在半小时内完成以前需要两天的 UI 还原工作。更重要的------当设计师再来说"这里差 2px"的时候,你有底气告诉 TA,到底是谁的稿子标错了。


作者:李慕杰(Leo / 8limujie)

一个被设计师追着改了三年间距、终于写完这套映射表的前端匠人

相关推荐
里欧跑得慢30 分钟前
Flutter 三方库 function_tree — 鸿蒙应用开发中的动态数学公式解析与计算神器,实现鸿蒙深度适配下的复杂函数逻辑运行时求值实战(适配鸿蒙 HarmonyOS Next ohos)
android·前端·安全·flutter·华为·harmonyos
苏灿烤鱼1 小时前
没有机密文件,也没有付费数据,在浏览器里造一颗"间谍卫星"
前端·javascript·github
泯泷1 小时前
手搓JSVM第 5 篇:写第一个编译器:从 AST 生成 IR
前端·javascript·算法
泯泷1 小时前
手搓JSVM第 7 篇:控制流:if、while 与 jump
前端·javascript·算法
泯泷2 小时前
手搓JSVM第 6 篇:把 IR 编成字节码:emit 与 label fixup
前端·javascript·算法
泯泷2 小时前
手搓JSVM第 3 篇:从栈式 VM 到寄存器式 VM:为什么我们选择寄存器
前端·javascript·算法
泯泷2 小时前
第 4 篇:让 VM 支持变量:Slot、Environment 与 TDZ
前端·javascript·算法
粥里有勺糖4 小时前
视野修炼-技术周刊第131期 | Bun 与 pnpm Rust 化
前端·github·agent
Coodor4 小时前
如何在web浏览器使用js操作CPU卡
开发语言·前端·javascript·cpu卡