Flutter 折叠屏 iPad / 宽屏适配实践

Flutter 折叠屏 iPad / 宽屏适配实践

最近业内关于 iPhone 推出折叠设备的传闻愈演愈烈。作为开发者,与其等风来,不如先修路。 如果你正在做 折叠屏 iPad / 宽屏适配,而且项目已经跑了一段时间,这篇文章大概率就是写给你的。

你可能正处在这样的背景里:

  • 项目长期以一套手机设计稿为主,当前基准是 375 x 667
  • 页面数量不少,不现实为折叠展开态和平板单独维护一套 UI
  • 工程里已经大量使用 flutter_screenutil,不想为了宽屏适配推翻整套尺寸体系
  • 现在遇到的问题很具体:展开后控件变大、留白变多、Grid 比例失真、横向卡片太宽、某些标题或容器开始裁切

如果你和上面的情况接近,那么这篇文章想解决的就不是"如何从零认识 flutter_screenutil",而是另一个更实际的问题:

在保留原有手机设计稿和尺寸体系的前提下,怎么把折叠屏 / 宽屏适配做得可控、可维护,而且不用把页面重写一遍。

下面总结的是一套已经在真实业务工程中落地的方案。它不是单讲 flutter_screenutil,而是把 fork 过的 flutter_screenutil、全局初始化策略,以及页面里的实际写法串成一套可复用的工程方法。整体思路也参考了 SwiftyFitsize 的做法。

一句话概括这套方案:

保留 375 设计稿作为统一基准,在全局缩放层抑制大屏放大,在页面结构层按大屏分支,在局部尺寸层通过 .fwLayoutBuilder、真实列宽来修正宽屏失真。

1. 先说结论:这套方案是"三层适配"

折叠屏 / 宽屏适配,不是靠单一技巧完成的,而是三层一起工作:

  1. 全局缩放层 :fork flutter_screenutil,引入大屏宽度系数,避免展开后所有控件一起变胖。
  2. 结构分支层 :用 ScreenUtil().isLargeScreen 切换列数、横滑 / 铺平、单双栏等布局结构。
  3. 局部修正层 :针对 banner、地图卡片、Grid、AppBar、底部占位等高频问题,用 .fwLayoutBuilder、getter 重算等方式定点修复。

这三层的关系可以理解为:

  • 第一层解决"整体太大"
  • 第二层解决"结构不合理"
  • 第三层解决"具体组件看起来还是不对"

2. 为什么选择 fork flutter_screenutil

这类工程通常不会放弃 screenutil,反而会继续把它当作全局尺寸基准,只是在宽屏场景下补齐原库不够用的能力。

常见做法是:

yaml 复制代码
dependency_overrides:
  flutter_screenutil: 
    git: 
      url: https://github.com/zeqinjie/flutter_screenutil.git 
      ref: master

再由基础库统一对外导出:

dart 复制代码
export 'package:flutter_screenutil/flutter_screenutil.dart';

这意味着:

  • 业务代码仍沿用熟悉的 .w / .h / .sp
  • 适配能力可以通过基础库统一下发
  • 不需要每个业务模块各自发明一套宽屏规则

这其实很重要。宽屏适配最怕的不是某个页面不好修,而是不同人修出不同口径,最后工程里同时存在三四套断点与尺寸语义。

3. fork 版 flutter_screenutil 到底加了什么

这次 fork 最关键的不是新增几个 API,而是把"手机尺寸语义"和"大屏尺寸语义"拆开了。

3.1 大屏判定

常见的新增能力是:

  • ScreenUtil().isLargeScreen
  • largeScreenShortestSideBreakpoint

通常按 最短边 >= 600 判定为大屏,这和业务里的布局分支保持一致。

这带来的直接收益是:

  • 页面做大屏分支时统一写 ScreenUtil().isLargeScreen
  • 避免手写 MediaQuery.shortestSide >= 600
  • 以后如果调整断点,业务代码和尺寸缩放仍能保持同一口径

3.2 大屏宽度系数

ScreenUtilInitScreenUtil 中新增:

  • largeScreenFitMultiple
  • largeScreenWidthFactor

一个典型初始化如下:

dart 复制代码
return ScreenUtilInit(
  designSize: const Size(375, 667),
  minTextAdapt: true,
  splitScreenMode: true,
  useInheritedMediaQuery: true,
  largeScreenFitMultiple: 0.5,
  largeScreenShortestSideBreakpoint: 600,
  builder: ...
);

这里最核心的是 largeScreenFitMultiple: 0.5

它的含义不是"把整个页面缩小到一半",而是:

  • 仍然按设计稿宽度比缩放
  • 但在大屏场景下,再给宽度链路乘一个额外系数
  • 从而抑制折叠展开态 / 平板下控件、留白、字号随宽度线性放大的问题

换句话说,这套方案并没有改掉 375 设计基准,而是在大屏下让"宽度放大曲线"变得更克制。

3.3 强制宽度链:.fw / .fsp

fork 版还会补两组很关键的扩展:

  • .fw:按原始宽度比例缩放,不乘大屏系数
  • .fsp:字体按原始宽度比例缩放,不乘大屏系数

以及对应的:

  • .fh
  • .fr
  • .fdg
  • .fdm

但从真实落地经验来看,最常用、最值得记住的还是:

  • 默认用 .w / .sp
  • 少数横向尺寸用 .fw

4. 这套方案里的真正约定

这套方案并不是"看到大屏就全部改成 .fw",而是已经形成了一套比较稳定的约定。

4.1 结构变化用 ScreenUtil().isLargeScreen

凡是下面这类场景,都优先用 ScreenUtil().isLargeScreen 处理:

  • 手机 2 列,大屏 3 列
  • 手机横滑,大屏铺平
  • 手机单列,大屏双列
  • 地图 / 侧栏等场景切换容器宽度

例如:

dart 复制代码
final int crossAxisCount = ScreenUtil().isLargeScreen ? 3 : 2;

这类写法表达的是"布局结构变了",而不是"同一个宽度单位换了算法"。

4.2 默认继续使用 .w / .sp

主路径仍然是:

  • 高度用 .w
  • 竖向间距用 .w
  • 大多数字号继续用 .sp
  • 普通圆角、边框、纵向节奏继续沿用原有写法

这是因为全局已经有 largeScreenFitMultiple 帮我们兜住"大屏整体过大"的问题,绝大部分组件不需要再做额外动作。

4.3 只有横向尺寸需要"保留设计宽感"时,才改成 .fw

.fw 更像一把手术刀,而不是全量替换方案。

典型场景包括:

  • 地图卡片固定设计宽
  • 横向弹层固定设计宽
  • 横向 banner 高度不变,但宽度不要被大屏再放大
  • 某些需要保持手机设计观感的列表卡片宽度

例如:

dart 复制代码
SizedBox(width: 375.fw);
dart 复制代码
final double topViewWidth =
    ScreenUtil().isLargeScreen ? 364.fw : 355.w;
dart 复制代码
Container(width: 343.fw);
dart 复制代码
SizedBox(height: 48.fw);

这背后的经验可以总结成一句话:

当你想抵消大屏额外宽度系数时,只改横向相关尺寸,不要顺手把整块组件的高度、竖向间距也一起改掉。

4.4 不要写 isLargeScreen ? n.fw : n.w

这是这套方案里一个很容易反复出现的伪分支。

在这类 fork 实现里,非大屏时 .fw.w 数值本来就是一样的。所以如果某个尺寸的设计意图就是"横向不要乘大屏系数",那直接写:

dart 复制代码
343.fw

就够了。

不要再额外写:

dart 复制代码
ScreenUtil().isLargeScreen ? 343.fw : 343.w

这会让代码显得更复杂,但没有带来额外语义。

4.5 状态对象里,和屏幕相关的尺寸尽量写成 getter

这个点很细,但很关键。

在折叠态、展开态、分屏态之间切换时,如果尺寸是在对象构造期就算死的,就会出现:

  • 底部留白不对
  • 避让高度不对
  • 弹层卡住
  • 列表内容与真实底栏高度不一致

因此更稳妥的做法是:随屏幕变化的尺寸用 getter,每次读取时重算。

例如:

dart 复制代码
double get bottomContactHeight => 60.w;

这类写法比 final double bottomContactHeight = 60.w; 更适合折叠屏场景。

4.6 AppBar 高度要和 title 区域一起适配

自定义 AppBar 在大屏里很容易出现一个经典问题:

  • title 根容器已经按 .w 放大
  • toolbarHeight 还停留在默认值
  • 结果就是标题被裁切,或者与返回按钮垂直不对齐

比较稳的写法是:

  • toolbarHeight 跟随适配
  • PreferredSizeWidget.preferredSize 也同步适配

例如:

dart 复制代码
@override
Size get preferredSize => Size.fromHeight(kToolbarHeight.w);

5. 页面级适配里,最常见的四种修法

如果把宽屏问题归类,真正高频的其实就四种。

5.1 Grid / 瀑布流:列数分支 + 真实列宽重算比例

这是折叠屏适配里最常见的一类问题。

表面看是:

  • Grid 裁切
  • 某一行被挤爆
  • 大屏下格子变得又高又空

本质原因通常是:

  • 列数变了
  • 列宽变了
  • childAspectRatio 却还是旧值
  • 或者外层高度仍然是过去按手机宽度拍脑袋写死的

比较成熟的写法,是把 LayoutBuilder 和真实列宽结合起来。

这里不是写死 childAspectRatio: 1,而是:

  1. 通过 LayoutBuilder 拿到实际可用宽度
  2. 计算 cellWidth
  3. 用设计稿下的参考行高推导 childAspectRatio

例如:

dart 复制代码
typedef GridAspectMetrics = ({
  double cellWidth,
  double referenceRowHeight,
  double childAspectRatio,
});

GridAspectMetrics computeGridAspectMetrics({
  required double gridInnerMaxWidth,
  required int crossAxisCount,
  double designWidth = 375,
  double designHorizontalPadding = 16 * 2,
}) {
  final double cellWidth = gridInnerMaxWidth / crossAxisCount;
  final double referenceRowHeight =
      ((designWidth - designHorizontalPadding) / crossAxisCount).w;
  final double childAspectRatio = cellWidth / referenceRowHeight;
  return (
    cellWidth: cellWidth,
    referenceRowHeight: referenceRowHeight,
    childAspectRatio: childAspectRatio,
  );
}

也就是:

列宽跟着真实空间走,行高保持设计稿节奏,最终比例动态算出来

这样在手机、折叠展开、平板上,宫格都会更稳定。

5.2 横滑列表 / 地图卡片:固定设计宽走 .fw

地图、横滑卡片、底部拖拽卡通常都很依赖"视觉宽度感"。

如果这些组件继续走默认 .w,在大屏下会被额外放大,看起来容易出现:

  • 卡片太胖
  • 文字太散
  • 与地图、遮罩、操作区比例失衡

所以这类区域里常见写法会是:

  • 375.fw
  • 343.fw
  • 364.fw

这类写法的本质不是"适配大屏",而是"在大屏下保留手机设计稿里的横向控制感"。

banner 是最典型的"只改宽,不动高"的区域。

例如:

dart 复制代码
CarouselOptions(
  viewportFraction: 1.0,
  height: 48.fw,
)

虽然这里用了 fw,但它表达的仍然是"横向观感按设计稿控制",并不是鼓励把整块组件所有尺寸都改成 force 版本。

在实际经验里,banner 这类区域通常遵循:

  • 横向尺寸按需 .fw
  • 纵向视觉节奏尽量保留原设计
  • 如果背景图需要铺满,再配合 fitWidthcoverfill 做局部修正

5.4 大屏结构改造优先于魔法数微调

这类问题经常说明一件事:

当问题来自结构本身时,最有效的办法不是继续调尺寸,而是直接换结构。

例如:

  • 手机横滑,大屏改成多列铺平
  • 手机 2 列,大屏 3 列
  • 单行横向入口,大屏改成更稳定的 Row / Expanded 结构

所以遇到折叠屏问题时,排查顺序建议是:

  1. 先看是不是应该换结构
  2. 再看是不是要改 .fw
  3. 最后才是微调某几个 magic number

6. 这套方案为什么比"改 designSize"更稳

很多工程遇到折叠屏,第一反应是:

  • 大屏时把 designSize 改大
  • 或者单独给平板一套设计稿

但这通常不是主路径,原因也很现实:

  • 页面存量很多
  • .w / .sp 的心智已经深度绑定 375 设计稿
  • 动态切 designSize 会放大排查成本
  • 组件库、业务库、宿主工程之间容易出现缩放语义不一致

相较之下,这套方案的优势是:

  • 延续旧心智 :大家依然围绕 375 写页面
  • 全局改动小:集中在 fork 包和宿主初始化
  • 业务接入渐进:有问题的页面按需治理
  • 语义明确.w 是默认链路,.fw 是大屏横向逃生口

7. 一份可以直接带走的团队约定

如果要把这套方案浓缩成团队规范,可以这样写:

全局层

  • 统一使用 fork 版 flutter_screenutil
  • 保持 designSize: Size(375, 667)
  • 通过 largeScreenFitMultiple 控制大屏宽度缩放曲线
  • 结构断点统一走 ScreenUtil().isLargeScreen

页面层

  • 布局结构变化优先用大屏分支处理
  • Grid / 瀑布流优先用 LayoutBuilder + 真实列宽重算比例
  • 横向固定设计宽的卡片、弹层、入口视图按需使用 .fw
  • 不要滥用 .fw,尤其不要把竖向节奏一起 force 掉

编码层

  • 不写 isLargeScreen ? n.fw : n.w
  • 屏幕相关占位高度优先写 getter
  • 自定义 AppBar 的 toolbarHeight、title 高度、preferredSize 保持一致

8. 最后的经验总结

这套折叠屏 / 宽屏适配方案,本质上不是在追求"所有页面在所有设备上一模一样",而是在做一件更现实的事:

用尽量小的全局改动,换取尽量稳定的宽屏体验,并给业务页面留下足够明确、足够统一的修正手段。

它最有价值的地方,不是新增了 .fwisLargeScreen,而是把适配思路收敛成了统一语言:

  • 全局靠 largeScreenFitMultiple
  • 结构靠 isLargeScreen
  • 局部横向靠 .fw
  • 比例问题靠 LayoutBuilder
  • 动态占位靠 getter 重算

当团队都按这套语言协作时,折叠屏适配就不再是"某个页面的特殊修补",而会变成一套可维护、可推广、可持续复用的工程能力。

9. 使用

适配前

适配后

use

yaml 复制代码
flutter_screenutil: 
  git: 
    url: https://github.com/zeqinjie/flutter_screenutil.git 
    ref: master

10. 感谢

相关推荐
ab_dg_dp1 小时前
Android 17+ 提取 AIDL 生成 Java 文件的实用脚本
android·java·python
小村儿1 小时前
连载13- 内部Tools,Claude Code 怎么真正"动"你的代码
前端·后端·ai编程
IT_陈寒1 小时前
Python的线程池把我坑惨了,原来异步不是万能的
前端·人工智能·后端
Arrom2 小时前
DLNA 渲染端排障实战:从 20s 卡顿到 stale subscriber 的两周追凶之旅
android·java
初一初十2 小时前
vue3茶叶商城网站vue网页vuejs前端
前端·javascript·vue.js·vscode·前端框架
kyriewen2 小时前
前端性能优化:LCP 从 4s 到 0.9s 的 5 个核心手段(附配置代码)
前端·javascript·性能优化
xiaofeichaichai2 小时前
Proxy与Reflect
前端·javascript
_李小白3 小时前
【android opencv学习笔记】Day 32:直线检测之霍夫变换
android·opencv·学习
小蜜蜂dry3 小时前
nestjs实战-权限二:角色模块
前端·后端·nestjs