Skip to content

当前窗口、断点与响应式布局:从 375 小屏补偿到多栏结构 ​

回到总览:屏幕适配:从清晰显示到动态窗口可用相关模块:01-dp/dpi 与图片资源放置(dp 换算 / 密度桶)· 03-字体 / 安全区 / 折叠屏 / 验证矩阵(系统边界)· 11-image-loading(图片加载链路)

一句话定义 ​

用统一断点(compact < 600dp / medium 600~840 / expanded > 840)在手机、平板、横屏之间切换布局,而不是纯比例放大。三端断点对齐 Material 标准,保证心智一致、设计稿可复用。

屏幕适配三层模型:密度地基 / 断点响应式 / 系统边界,只做一层都不完整

这张图把本文档在整条适配链里的位置标出来:本文档(02)只解决中间那层“断点响应式”。它的前提是 01 已经把密度单位(dp / sp / 矢量 / 密度桶)做对,它的落点是 03 要兜住的系统边界(字体 / 安全区 / 折叠分屏)。读者先在脑里建立“三层叠加”的心智,再看下面的断点细节,就不容易把 375 放大误当成完整适配。

开篇案例:把 375 手机稿放到 1024dp 平板会怎样 ​

设计师按 375 宽出稿,开发用 flutter_screenutil 把整页尺寸按 当前宽度 / 375 等比放大到平板:

text
s = 1024 / 375 ≈ 2.73

结果:48dp 图标变成约 131dp 视觉量级、列表行高和卡片圆角一起被放大近 3 倍,信息密度几乎没变——用户看到的是“大号手机”,不是“平板应用”。正确做法是到 600/840dp 断点后换两栏/三栏结构,增长栏位和导航形态,而不是继续放大手机单列布局。

代码索引 ​

主题Lab 说明源码
Flutter 断点适配对照adaptive-layout-demoadaptive_layout_demo_page.dart
Android Compose 断点对照screen-adaptation (Compose)WindowSizeClassDemoActivity.kt
Android View/XML 资源分桶screen-adaptation (View/XML)ViewXmlAdaptiveDemoActivity.kt

为什么需要 ​

  • 为什么 375 基准在平板上会出事故?
    • 一句话答:缩放系数 s = 屏幕逻辑宽 / 375,iPad 768~1024dp → s≈2~2.7,整个 UI 被放大两倍多:图标巨大、列表稀疏、横屏更糟(详见 §375 设计稿缩放)。
  • 为什么三端断点要统一 600 / 840?
    • 一句话答:Material 规范定义的标准断点,保证三端心智一致、同一个设计稿能映射到同一套布局决策。
  • 为什么“大屏适配”本质上是信息密度问题?
    • 一句话答:因为大屏真正多出来的是可承载内容的空间,所以应该增加栏位、列表详情并排、导航形态,而不是把同一套手机 UI 等比拉大。

前置边界:dp 解决密度,不解决占屏比例和信息密度 ​

01 已说明:dp 是逻辑密度锚点,让同一 100dp 按钮在 320dpi 和 480dpi 屏上视觉尺度近似一致(真机因厂商逻辑密度 override 会有偏差,不能承诺绝对物理尺)。

但 dp 不归一化占屏比例:360dp 逻辑宽手机上 100dp 占 27.8%,411dp 手机上只占 24.3%——大屏显空、小屏显挤。这是布局层问题,不是再调几个 dp 能解决的,需要断点换结构或(仅 compact 小屏)** 375 比例补偿**。

px / dpi / dp / 英寸 换算关系:两台手机上 100dp 按钮像素数不同但物理尺寸一致

图下备注:dp 归一化了“密度”,但没归一化“占屏比例”。占屏比例随当前窗口逻辑宽变化——这正是 02 要解决的输入变量。

375 设计稿缩放:解决什么、不能解决什么 ​

本节承接 01 迁出的完整论证:DP 是物理锚定,375 是比例锚定。

DP 的核心特性:在正常(未虚报)设备上,100dp 渲染出的视觉尺度在不同密度屏上近似一致。这保证了“不同密度屏显示一致”——同一套 dp 布局,在 320dpi 和 480dpi 上看起来一样大。

但 DP 不归一化“占屏比例”:两台手机逻辑宽不同(360dp vs 411dp),100dp 占屏比例就不同(27.8% vs 24.3%)——大屏上显得“小、空”,小屏上显得“大、挤”。

所以业界用“375 分份”(比例法) :设计师画 375pt 宽的设计稿(iPhone 基准稿),App 按“设备逻辑宽 ÷ 375”整体等比缩放整稿:

text
实际值 = 设计值 × (设备逻辑宽 / 375)
  • 设计稿上“占 26.7% 宽的按钮”,在 360 宽手机占 26.7%、在 411 宽手机也占 26.7%——比例永远一致,所以“看着像同一套 UI”。
  • 巧用:375.dp 即全屏宽、375.dp/4 即 25% 宽,百分比体系自洽(Flutter 工程里 .dp = ScreenUtil().setWidth 正是这套)。
  • 375 是业界最常用基准:源于 iPhone 设计稿宽 375pt;Flutter 的 flutter_screenutil 默认 / 常用 designSize: Size(375, 812)。
  • iOS 与 Android dp 一样“直接写设计值”,但“不满屏”问题照旧:pt 就是设计单位,375pt 稿在 iOS 直接写 width: 375,无需 375 分份的比例系数——这点 Android dp、Flutter 逻辑像素也一样。但 375pt 在 410/430pt 的 iPhone 上同样不满一屏、在 iPad(768pt+)更空——iOS 靠 Auto Layout 约束自适应 + Size Class 断点换布局(见下文 iOS 对照)兜底,而不是把整稿等比放大。375 分份只是 Flutter(flutter_screenutil)这类“整稿等比缩放”路线的补偿手段,不是三端通吃的免适配方案。

适用边界(手机 compact) :

逻辑宽s = width/375评价
360dp0.96略缩,可接受
390dp1.04微调,合理
430dp1.15建议 clamp(1.0, 1.2) 上限
768dp2.05失控,必须换断点布局
1024dp2.73失控,必须换断点布局
dart
final s = (MediaQuery.sizeOf(context).width / 375).clamp(1.0, 1.2);
// 所有尺寸乘 s;超出 1.2 的平板/横屏必须走断点布局,而不是继续放大
// 正文文字不要跟 s 绑死——字号跟 sp / TextScaler(见 03)

dp 换算与 375 基准 vs 断点响应式

读法:手机宽度附近 s≈1.0~1.15 是小幅补偿;768/1024dp 时 s 冲到 2 倍以上,应优先变化栏位和导航,不是整页倍率。

一句话收口:375 法建在 dp 之上,只解决 compact 小屏的比例锚定;跨尺寸(平板 / 折叠 / 横屏)必须叠加断点响应式——否则 768 宽平板上整稿被放大到系数 ≈2.05,按钮吹成两倍大、留白巨大。

真正的输入是当前窗口,不是设备型号 ​

布局判断的输入是当前可用窗口,不是“这是一台平板”:

状态同一台平板的可能窗口宽应采用的布局
全屏竖屏768~840dpmedium,两栏
全屏横屏1024dp+expanded,多栏 / Rail
分屏 50%约 500dpcompact,退回单栏
折叠半折可能 < 600dpcompact
折叠展开可能 > 840dpexpanded

静态 vs 动态:

  • values-sw600dp / layout-sw600dp:资源解析阶段的静态分桶,启动时按最小宽度选 XML/dimen,适合基础平板布局差异。
  • WindowMetrics / WindowSizeClass / LayoutBuilder:运行时读当前窗口,分屏、自由窗口、折叠态变化时必须用它重新分类。

配置变化(旋转、分屏、折叠)后应重新计算窗口宽度并刷新布局结构,不要缓存“设备是平板所以永远双栏”。

统一断点模型:compact / medium / expanded ​

宽度档位宽度范围常见设备 / 窗口状态布局结构导航形态375 基准是否还能主导
compact< 600dp手机竖屏;平板分屏后被压窄的窗口单列主内容;列表和详情通常分步进入BottomBar / 顶栏返回可以,只做小范围补偿,建议 clamp(1.0, 1.2)
medium600~840dp平板竖屏;大手机横屏;部分折叠屏展开态两栏最常见:列表 + 详情、筛选 + 结果BottomBar → NavigationRail不应继续主导,应该切到两栏结构
expanded> 840dp平板横屏;桌面窗口;更宽的展开态多栏 / 三列 / 固定侧边栏 / 更高信息密度NavigationRail / PermanentDrawer不应该,继续放大会把手机 UI 拉成“大号手机”

读表时先看“当前窗口宽度”落在哪一档,再看这一档要求的是“结构切换”还是“尺寸微调”。最关键的记忆点:600 / 840dp 是结构边界;375 只在 compact 里做小屏补偿。

断点响应式三设备形态:compact 单列、medium 两栏、expanded 三栏,大屏增长栏位不放大

从缩放到结构:大屏增加信息密度 ​

375 缩放与断点换布局:大屏应该增长栏位和信息密度

红色路径:增长按钮、标题、卡片的物理尺寸,信息密度几乎不变 → “看起来更大”。绿色路径:增长栏位、导航形态和同屏承载量 → “同一屏能处理更多任务”。

断点到了 medium / expanded,最该做的是主从(master-detail)结构同屏摊开:

  • 窄屏(< 600dp) :单列列表,点按 push 进详情页。
  • 宽屏(≥ 600dp) :左导航 / 菜单 + 列表 + 详情同屏并排。

列表在宽屏变菜单 + 列表 + 详情:master-detail 主从布局

Flutter 可直接用 adaptive_scaffold / TwoPane,或 Row(children: [if (w >= 600) NavRail(), Expanded(child: ListPane()), if (w >= 600) Expanded(child: DetailPane())])。

导航形态随宽度演进:BottomBar → NavigationRail → PermanentDrawer。

Android View / XML 实现 ​

对应 Lab:screen-adaptation (View/XML sw600dp) —— ViewXmlAdaptiveDemoActivity 演示 values-sw600dp / layout-sw600dp 静态分桶 + WindowMetricsCalculator 运行时窗口判断协同。

  • dp 自动适配:px = dp × density,小屏缩放由系统完成。
  • 资源限定符:values-sw600dp/(平板布局)、values-land/(横屏)、layout-sw600dp/ 等——系统按限定符自动选资源。
xml
<!-- res/values/dimens.xml(手机) -->
<dimen name="item_padding">8dp</dimen>
<!-- res/values-sw600dp/dimens.xml(平板,宽度 ≥ 600dp 时覆盖) -->
<dimen name="item_padding">24dp</dimen>
  • 运行时窗口宽度分类:先用 WindowMetricsCalculator 拿当前窗口宽度,再按 600 / 840 自己分类,决定是单列、双栏还是多栏。
kotlin
val metrics = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(this)
val widthDp = metrics.bounds.width() / resources.displayMetrics.density
val windowSizeClass = when {
    widthDp < 600f -> "compact"
    widthDp < 840f -> "medium"
    else -> "expanded"
}
  • ConstraintLayout / RecyclerView 弹性:Guideline 百分比 / Chain 分配权重 / GridLayoutManager 动态列数,小屏不放大、大屏不空旷。

Jetpack Compose 实现 ​

对应 Lab:screen-adaptation (Compose WindowSizeClass) —— WindowSizeClassDemoActivity 演示 calculateWindowSizeClass + ListDetailPaneScaffold + NavigationSuiteScaffold,并模拟 375/430/600/768/1024dp 对照纯缩放。

kotlin
// 1) 断点:material3-window-size-class
val wsc = calculateWindowSizeClass(activity)
val layout = when (wsc.widthSizeClass) {
    WindowWidthSizeClass.Compact -> CompactLayout()
    WindowWidthSizeClass.Medium  -> MediumTwoPane()
    WindowWidthSizeClass.Expanded -> ExpandedMultiPane()
}

// 2) 官方两栏:material3-adaptive 1.4+
ListDetailPaneScaffold(
    listPane = { ListPane() },
    detailPane = { DetailPane() },
) // 手机=单栏(列表/详情切换),平板=并排两栏

// 3) 网格列数自适应
LazyVerticalGrid(GridCells.Adaptive(minSize = 120.dp)) { items(...) }
  • 约束驱动:BoxWithConstraints 适合局部小分支,WindowSizeClass 适合页面级结构切换。
  • 导航形态切换:大屏下 BottomBar → NavigationRail → PermanentDrawer。
  • 系统边界:Compose 页面上线时还要配合 WindowInsets;详见 03。

Flutter 实现 ​

对应 Lab:adaptive-layout-demo

dart
LayoutBuilder(builder: (context, constraints) {
  final w = constraints.maxWidth;
  if (w < 600) return const CompactList();
  if (w < 840) return const TwoPaneLayout();
  return const ExpandedMultiPane();
});

当前仓库里的 Flutter demo 通过页面内预设宽度按钮模拟 375 / 430 / 600 / 768 / 1024dp:

  1. 375 基准缩放在手机宽度下是可接受的轻微补偿。
  2. 到了 768 / 1024dp,应该切两栏 / 三列,而不是把整套手机 UI 一起放大。
dart
final scale = (_simulatedWidth / 375).clamp(1.0, 1.2);

switch (breakpoint) {
  case _Breakpoint.compact:
    return _PhoneStyleDashboard(...);
  case _Breakpoint.medium:
    return _TwoPaneDashboard(title: 'Medium 600~840dp', columns: 2);
  case _Breakpoint.expanded:
    return _TwoPaneDashboard(title: 'Expanded > 840dp', columns: 3);
}
  • 可能执行顺序
    1. 选择一个模拟宽度。
    2. 先算断点落在哪一档。
    3. compact 走单列,medium 走两栏,expanded 走三列。
    4. 若打开“只看 375 基准缩放结果”,则故意不换布局,只放大手机面板。
  • 可能输出
    text
    模拟宽度:768dp
    断点:medium
    375 缩放系数:1.20x
    模式:断点换布局
  • 预期现象:768/1024dp 下,断点模式增长的是栏位和信息密度;纯缩放模式增长的是整体 UI 比例。
  • 观察重点:这正是“响应式适配”与“等比放大”的本质差异。

iOS 对照:Auto Layout、Size Class 与实际容器宽度 ​

对 Android / Flutter 工程师的映射:

Android / Flutter 概念iOS 对照
WindowMetrics / LayoutBuilder 当前宽容器实际 bounds,不是 UIDevice 型号
WindowSizeClass compact/medium/expandedUITraitCollection horizontalSizeClass(.compact / .regular)
values-sw600dp 静态 XMLSize Class + Storyboard / XIB 变体
断点换双栏Auto Layout 约束 + UISplitViewController / 自定义双栏
375 screenutil 小屏补偿一般不整页缩放;用约束与 Size Class

关键边界:iPad Split View、Slide Over、Stage Manager 下,horizontalSizeClass 可能仍是 .regular,但容器宽度已被压到 phone 级——必须读实际容器宽度决定单栏还是双栏,不能只看 Size Class 标签。Dynamic Type、Safe Area 见 03。

场景推演 ​

窗口宽档位结构导航不要做
375dp 手机竖屏compact单列 + BottomBar375 微调 s≈1.0~1.15 可接受不要为 375 单独写死 px
430dp 大手机compact单列clamp(1.0, 1.2)不要开始双栏
768dp 平板竖屏medium列表 + 详情两栏NavigationRail不要整页 ×2 放大
1024dp 横屏expanded三栏 / 固定侧栏PermanentDrawer / Rail不要保留手机 BottomBar 占满宽
500dp 平板分屏compact退回单栏BottomBar不要因“设备是平板”强留双栏

社区成熟方案对照 ​

端方案原理典型用法适用边界
Android Viewvalues-sw600dp / layout-sw600dp / dimen 分桶资源解析阶段按最小宽度切不同布局和尺寸资源手机/平板两套 XML;平板更大边距、双栏容器适合静态资源切换;分屏实时变化仍要配合运行时窗口判断
Android / ComposeWindowMetrics / WindowSizeClass运行时读取当前窗口宽度并映射到 600/840dp 档位单栏→双栏→多栏;BottomBar→Rail适合结构切换;不适合拿来做整页等比放大
Composematerial3-adaptive / pane scaffold官方把主从式、多 pane 页面抽成骨架ListDetailPaneScaffold、导航/详情并排很适合列表详情页;复杂营销页、瀑布流仍需自定义布局
FlutterLayoutBuilder / MediaQuery在 widget 树内按当前约束切布局if (width < 600)、动态列数、Rail/Drawer 切换适合跨尺寸结构变化;不要只按机型或平台名分类
Flutterresponsive_framework / responsive_builder把断点、设备分组、包装器进一步封装项目统一断点、统一 builder 入口适合中大型项目收口;简单页面手写即可
Flutterflutter_screenutil用设计稿宽度推导尺寸缩放系数小屏手机下微调圆角、边距、图标尺寸只适合 compact 小屏补偿;不该统治平板布局,更不该缩放正文文字

Android / Compose / Flutter / Web 对照 ​

维度AndroidComposeFlutterWeb
断点来源sw600dp 限定符 / 运行时宽度判断calculateWindowSizeClassLayoutBuilder / MediaQueryCSS media queries
两栏布局Fragment + 资源切换ListDetailPaneScaffold自定义 TwoPaneLayout / adaptive_scaffoldgrid + media query
大屏治理values-wXXXdp / 动态列数GridCells.Adaptive 列数自适应SliverGridDelegateWithMaxCrossAxisExtentauto-fill minmax
导航切换BottomNav → Rail / DrawerBottomBar → Rail / DrawerBottomNav → Rail / Drawer顶栏 / 侧栏切换

常见场景 ​

  1. 手机竖屏(compact) :单列表 + 底部导航;375 基准 s≈1.0~1.15 微调可用。
  2. 平板竖屏(medium) :列表 + 详情两栏,不放大。
  3. 平板横屏 / 折叠展开(expanded) :固定侧边栏 + 主内容多栏;列表网格列数随宽度增加。
  4. 分屏 / 多窗口:窗口宽度可能随时掉回 compact,所以不要把“设备是平板”误当成“当前一定是两栏”。

常见误配、事故后果与排障 ​

  1. 纯比例放大做适配(375 法全端套用):平板 UI 放大 2~3 倍,图标巨大、布局稀疏。修法:clamp 上限 + 断点换布局。
  2. GridView 固定列数:平板中间大片留白。修法:GridCells.Adaptive(minSize) / MaxCrossAxisExtent。
  3. 只用竖屏设计:横屏/平板未测,线上布局溢出。修法:values-land / OrientationBuilder / 真机矩阵测试。
  4. 把设备类型当断点:平板分屏后实际宽度可能只剩 500dp。修法:以当前窗口宽度为准,不要只看机型。
  5. 导航形态不切:大屏还保留手机底部导航,内容区浪费。修法:大屏切 NavigationRail / drawer。

排障路径:先拿“屏幕逻辑宽(dp)”→ 判断落在哪个断点 → 再决定是布局切换还是尺寸缩放 → 看导航形态和列数是否跟着变 → 最后用模拟器/真机宽度矩阵验证。

与相近概念对比 ​

方案原理适用
375 基准缩放所有尺寸 × (逻辑宽/375),建议 clamp手机竖屏、UI 组件轻微补偿
断点响应式按宽度切换布局结构手机 / 平板 / 横屏全覆盖
自适应布局(Compose adaptive / adaptive_scaffold)官方可组合两栏/多栏骨架列表+详情类页面
约束驱动局部适配子树按当前约束做小范围变化卡片、模块块级自适应

对应实验 ​

Lab说明源码
adaptive-layout-demo模拟 375/430/600/768/1024dp 宽度,对比 375 缩放与断点换布局(与文首「代码索引」一致)adaptive_layout_demo_page.dart
screen-adaptation (Compose)Android Compose:WindowSizeClass + ListDetailPaneScaffold + 导航形态切换,同设备模拟多档宽度对照纯缩放WindowSizeClassDemoActivity.kt
screen-adaptation (View/XML)Android View/XML:values-sw600dp / layout-sw600dp 资源分桶 + WindowMetricsCalculator 运行时窗口判断协同ViewXmlAdaptiveDemoActivity.kt

复习检查题 ​

  1. 三端统一断点是多少?各自对应什么场景?

    答:compact < 600dp(手机竖屏,单列);medium 600~840(平板竖屏,两栏);expanded > 840(横屏 / 平板,多栏)。

  2. 为什么 375 基准不能用于平板?

    答:s = 逻辑宽 / 375,iPad 768~1024dp 时 s≈2~2.7,整个 UI 放大两倍多导致图标巨大、布局稀疏;正确做法是断点换布局。

  3. Flutter 里 flutter_screenutil 与 LayoutBuilder 断点的分工是什么?

    答:前者解决“手机竖屏下按设计稿等比缩放”(375 基准 + clamp 上限);后者解决“跨尺寸换布局”(600/840 断点)。小屏用缩放、大屏换布局,两者配合。

  4. 为什么 values-sw600dp 还不够,还要看 WindowMetrics / WindowSizeClass?

    答:因为资源限定符更像“启动和资源选择时的静态分桶”,而分屏、自由窗口、折叠态变化是运行时事件。真实页面结构切换必须看当前窗口宽度。

  5. 为什么平板分屏后还可能退回单栏?

    答:因为断点看的是当前窗口宽度,不是设备名。平板一旦进入分屏,窗口可能掉回 500dp 左右,这时仍应回到 compact 单栏布局。

  6. 平板全屏时你是双栏,用户一切分屏窗口变窄,你该怎么判断?

    答:重新读 WindowMetrics / LayoutBuilder 的当前宽度;若掉到 < 600dp,立即退回 compact 单栏,不要缓存“这是平板所以永远双栏”。铰链与安全区变化见 03。

速记 ​

  • 断点:600 / 840dp,三端统一;compact / medium / expanded。
  • 375 只配小屏:clamp(1.0, 1.2),大屏换布局不放大;正文不跟 s 绑死。
  • 输入:当前窗口宽,不是设备型号。
  • Compose:calculateWindowSizeClass + ListDetailPaneScaffold + GridCells.Adaptive。
  • Flutter:LayoutBuilder 断点 + SliverGridDelegateWithMaxCrossAxisExtent。
  • Android View:values-sw600dp + 运行时窗口宽度分类。
  • 大屏核心:增长信息密度、导航形态和栏位结构,不是增长整个 UI 比例。

站点构建时间:2026/8/24 23:43:17