UI 与交互篇(6/6):大屏、横屏、异形屏适配实践

4 阅读5分钟

大屏、横屏、异形屏适配实践:一套代码跑稳手机、平板和车机场景

系列:UI 与交互篇(6/6)

Flutter 响应式布局 横屏适配 SafeArea 工程实践

很多 Flutter 项目在竖屏手机里看着没问题,一上平板、横屏、折叠屏、刘海屏就开始“露馅”:按钮被遮住、弹窗贴边、列表一行太长、视频页切横后布局错乱。
这篇讲一套我在业务里反复验证过的适配方案:先定规则,再做分层,最后用指标回归,而不是每个页面写 if-else 硬撑。


1. 问题背景:业务场景 + 现象

典型业务场景:

  • 直播/房间页:竖屏是主路径,横屏是高频副路径。
  • 排行榜/大厅页:在平板上要利用额外空间。
  • 弹窗与底部面板:在异形屏(刘海、挖孔、圆角)容易贴边或被系统手势区吞掉。

常见现象:

  • 只按 MediaQuery.of(context).size 写死比例,横屏时组件变形。
  • 顶部状态栏、底部手势条没有统一处理,出现“看得见点不到”。
  • iPad/安卓平板仍沿用单栏列表,视觉浪费、操作路径变长。
  • showModalBottomSheet 在大屏看起来像“窄条”,交互不自然。

2. 原因分析:核心原理 + 排查过程

核心原理

适配问题本质是三件事没拆开:

  1. 可用空间:屏幕尺寸、方向、窗口大小(多窗口/分屏)。
  2. 安全区域padding / viewPadding / viewInsets(刘海、系统栏、键盘)。
  3. 布局策略:不同断点下是单栏、双栏,还是主从布局。

如果把这三件事混在每个页面里临时判断,项目越大越难维护。

排查过程(建议按这个顺序)

  1. 在问题页打印:sizeorientationpaddingviewInsets
  2. 检查是否把系统安全区和键盘安全区混用了。
  3. 统计“硬编码尺寸”(例如固定 width: 375)出现频率。
  4. 抽查横屏场景:是否只是把竖屏布局横着放大,而非重新分配信息层级。

3. 解决方案:方案对比 + 最终选择

方案对比

  • 方案 A:页面内 if-else 判断

    • 优点:改得快。
    • 缺点:重复高、风格不一致、后续维护成本陡增。
  • 方案 B:全局缩放(按设计稿统一缩放)

    • 优点:视觉统一。
    • 缺点:文字可读性与触控面积容易失真,不适合复杂交互页面。
  • 方案 C:断点 + 安全区 + 布局组件化(推荐)

    • 优点:规则统一,可扩展到平板/车机/桌面窗口。
    • 缺点:前期需要搭一层基础设施。

最终选择(落地口径)

我推荐用 “三层适配”

  1. 适配配置层:统一断点、边距、最大内容宽度。
  2. 页面骨架层:根据断点切单栏/双栏/主从。
  3. 组件约束层:弹窗、底部面板、按钮、卡片遵循统一尺寸与安全区规则。

4. 关键代码:最小必要代码片段

4.1 统一断点与布局类型

enum DeviceClass { phone, tablet, desktopLike }

DeviceClass resolveDeviceClass(double width) {
  if (width < 600) return DeviceClass.phone;
  if (width < 1024) return DeviceClass.tablet;
  return DeviceClass.desktopLike;
}

4.2 页面骨架:单栏 / 双栏切换

class AdaptiveScaffold extends StatelessWidget {
  const AdaptiveScaffold({
    super.key,
    required this.primary,
    this.secondary,
  });

  final Widget primary;
  final Widget? secondary;

  @override
  Widget build(BuildContext context) {
    final size = MediaQuery.sizeOf(context);
    final device = resolveDeviceClass(size.width);

    if (device == DeviceClass.phone || secondary == null) {
      return SafeArea(child: primary);
    }

    // 平板及以上:主从布局
    return SafeArea(
      child: Row(
        children: [
          Expanded(flex: 5, child: primary),
          const VerticalDivider(width: 1),
          Expanded(flex: 4, child: secondary!),
        ],
      ),
    );
  }
}

4.3 横屏策略:不是“拉伸”,而是“重排”

bool isLandscape(BuildContext context) =>
    MediaQuery.orientationOf(context) == Orientation.landscape;

// 示例:横屏时把信息区移到右侧,操作区固定底部(或侧边)

4.4 异形屏与键盘:区分三类 inset

final media = MediaQuery.of(context);
final safeTopBottom = media.padding;      // 刘海/系统栏
final keyboardInset = media.viewInsets;   // 键盘遮挡
final stableSystem = media.viewPadding;   // 稳定系统区域(不受键盘影响)

使用建议:

  • 页面主体:优先 SafeArea(处理异形屏)。
  • 输入页:底部跟随 viewInsets.bottom 做动画抬升。
  • 全局底部按钮:避免仅看 padding.bottom,要考虑键盘态。

4.5 统一 BottomSheet 宽度与圆角(大屏关键)

Future<T?> showAdaptiveSheet<T>({
  required BuildContext context,
  required WidgetBuilder builder,
}) {
  final width = MediaQuery.sizeOf(context).width;
  final maxSheetWidth = width >= 600 ? 560.0 : width;

  return showModalBottomSheet<T>(
    context: context,
    isScrollControlled: true,
    useSafeArea: true,
    shape: const RoundedRectangleBorder(
      borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
    ),
    builder: (ctx) => Align(
      alignment: Alignment.bottomCenter,
      child: ConstrainedBox(
        constraints: BoxConstraints(maxWidth: maxSheetWidth),
        child: builder(ctx),
      ),
    ),
  );
}

5. 效果验证:数据/截图/日志

建议把适配结果做成可回归指标,而不是“看起来差不多”:

  • 设备覆盖矩阵
    • 手机竖屏(小屏)
    • 手机横屏
    • 平板竖屏
    • 平板横屏
    • 异形屏(刘海/挖孔)
  • 交互可达性:关键按钮点击区域 >= 44dp。
  • 安全区正确性:无内容被状态栏/手势区遮挡。
  • 性能稳定性:横竖屏切换后首帧恢复时间、掉帧率可接受。
  • 自动化截图对比:同页面多尺寸 Golden Test,减少回归漏网。

6. 可复用结论:通用经验 + 避坑清单

通用经验

  1. 先定断点,再写页面,不要每个页面自定义“平板逻辑”。
  2. 横屏是重排,不是缩放:信息层级和操作路径都要重设计。
  3. SafeArea 不是万能:输入法场景必须关注 viewInsets
  4. 弹窗/底部面板必须统一抽象:宽度、圆角、动画、可滚动策略一次定好。
  5. 适配要“平台化”:基础组件做对,业务页面自动收益。

避坑清单

  • 在组件内部直接用全屏 MediaQuery.size 做尺寸计算。
  • 把字体、间距按比例缩放到所有设备。
  • 横屏仅旋转布局,不重构信息结构。
  • 底部按钮未考虑键盘抬升与手势区。
  • 大屏弹窗仍使用手机全宽样式,导致视觉重心失衡。
  • 适配只靠人工点点点,没有截图回归机制。

结语

大屏、横屏、异形屏适配不是“多写几段判断”,而是把 UI 规则沉淀成工程能力:
断点统一、骨架稳定、组件收敛、验证自动化
当你的项目能在新增页面时“天然适配”而不是“临时补丁”,这套实践就真正生效了。