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)

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

相关推荐
泯泷11 小时前
那段文字是谁删的?Yjs 14 正式版之前,一套删除归属方案的实现与边界
前端·javascript·算法
野槐14 小时前
前端项目优化(有了我,会发生...)
前端
aa小小15 小时前
【无标题】
前端
计算机魔术师16 小时前
还在单线程跟 AI 聊天?Claude Code 并行多会话让你一个人干三个人的活
前端
志尊宝16 小时前
Vue3 零基础每日笔记(038):亲手封装 useDebounceFn 与 useThrottleFn——高频事件的流量阀
前端·javascript·vue·html·vue3
JavaGuide16 小时前
对标 MinIO!全新一代分布式文件系统正式发布
前端·后端
风骏时光牛马16 小时前
AI驱动自动化业务工作流搭建实践
前端
码云之上16 小时前
Skill 里的脚本终于能跑了,星悟接 CubeSandbox 的纪实
前端·人工智能·前端框架
IT_陈寒16 小时前
Vue computed属性这个坑,我居然踩了三次才爬出来
前端·人工智能·后端
烈风逍遥17 小时前
第三篇:组件化实践,SpTable 通用表格组件设计
前端·架构