Appearance
屏幕适配:从清晰显示到动态窗口可用
模块范围:主线实现栈为 Android View / Jetpack Compose / Flutter;iOS 在资源倍率、Auto Layout / Size Class、Dynamic Type、Safe Area 等处作必要对照,不扩成第四条等篇幅主线。
开篇案例:手机正常,为什么仍不算适配完成
同一套页面在 QA 的 390dp 手机上可能“看起来没问题”,但上线后仍可能在以下场景翻车:
| 场景 | 典型现象 | 多半属于哪一层 |
|---|---|---|
| 1024dp 平板全屏 | 图标巨大、列表稀疏,像“大号手机” | 布局 / 断点 → 02 |
| 系统字体 2.0x | 标题裁切、按钮点不到、Row 挤爆 | 系统边界 / 字体 → 03 |
| 键盘弹起 | 底部 CTA 被挡、输入框不可达 | 系统边界 / IME → 03 |
| 平板分屏(窗口约 500dp) | 仍强行双栏,内容挤爆 | 布局 / 当前窗口 → 02 |
| xxhdpi 机放 mdpi 桶图 | 图标发糊、内存暴涨 | 资源 / 解码 → 01 |
完整适配的目标:在不同密度、窗口尺寸、无障碍字体和系统占用区域下,页面仍清晰、可读、可点、可达,且窗口变化后结构仍合理——不是把设计稿按比例放大到所有设备。
一句话定位
这一模块讲「图片与 UI 如何在不同屏幕密度、不同屏幕尺寸、不同系统安全区下正确、清晰、可点击、可阅读地显示」,主线是:
dp 与密度(基础换算)→ 图片资源放置(密度桶 / 放图策略)→ 断点响应式布局(三端实现)→ 字体 / 安全区 / 折叠屏 / 分屏治理。
与 11-image-loading(图片加载链路)互补:11 管“怎么加载”,12 管“显示多大、放哪个桶、怎么布局、怎么避开系统栏与大屏坑”。11 的全链路索引会在此处回链。
本模块要回答的问题
逻辑单位与换算机制
- dp、pt、逻辑像素如何在 Android View、Compose、Flutter、iOS 中换算?三端机制有何不同?
- 一句话答:三端同构为
px = 逻辑单位 × 倍率,但倍率从哪来、资源怎么解码各不相同——Android(含 Compose)走连续dpi/160+ 密度桶解码;Flutter 走devicePixelRatio+ 变体目录,不按 Android 桶机制扩大解码;iOS 走离散@2x/@3x1:1 匹配(详见 01 第一层)。
- 一句话答:三端同构为
| 栈 | 逻辑单位 | 换算公式 | 原理要点 |
|---|---|---|---|
| Android View | dp | px = dp × (设备dpi / 160) | 以 160dpi(mdpi)为基准定义 1dp;布局按 dp 上屏,位图按 drawable-{桶} 放置,BitmapFactory 用 inDensity / inTargetDensity 解码期缩放,让图在不同密度屏显示成相同逻辑尺寸 |
| Jetpack Compose | Dp / sp | 同 dp:LocalDensity.current.density = dpi/160 | 无独立密度体系,完全继承 Android 资源桶与解码公式;48.dp 与 View 的 48dp 等价,painterResource(R.drawable.*) 仍走 drawable-{桶} / VectorDrawable / nodpi |
| Flutter | 逻辑像素 | px = 逻辑像素 × devicePixelRatio | devicePixelRatio 来自宿主(Android 上 ≈ density,iOS 上 ≈ scale);资源用 1.0x/2.0x/3.0x 变体目录,AssetImage 按 dpr 选最近变体原像素解码——不会在解码期按 Android 密度桶再放大;缺高倍变体时尺寸按固有逻辑尺寸、绘制可能拉伸发糊 |
| iOS(对照) | pt | px = pt × scale(通常 2 或 3) | 设备密度离散三档(@1x/@2x/@3x),UIImage(named:) 1:1 匹配切图,不按 dpi 桶做二次解码缩放;仍须按倍率出多套切图,大图可用 Single Scale 单源 |
dp/ 逻辑像素解决了什么、没解决什么?- 一句话答:它们是逻辑密度锚点——让同一
100dp在不同 dpi 屏上视觉尺寸通常接近;但不归一化占屏比例(360dp 宽与 411dp 宽上同一100dp占屏不同)。占屏比例、375 补偿、断点换栏位归 02。另:1080px宽在dpr 2.0与2.625上逻辑宽分别是 540dp / 411dp——适配看当前窗口逻辑宽,不看机型名。
- 一句话答:它们是逻辑密度锚点——让同一
- 布局和字号为什么不能用同一套单位?
图片资源放置策略
图片放哪个密度桶 / 变体?小图标和背景大图策略一样吗?
- 先分两类再选策略——
- ① UI 小图标:Android/Compose 矢量 XML 或 xxhdpi 单桶(只缩不放),Flutter 全量
1.0x~3.0x变体或flutter_svg,iOS@2x/@3x精确切图; - ② 背景 / 运营大图:Android
drawable-nodpi+inSampleSize,Flutter 单源 +cacheWidth降采样,iOS Single Scale +contentMode。
- ① UI 小图标:Android/Compose 矢量 XML 或 xxhdpi 单桶(只缩不放),Flutter 全量
Android 低桶放高 dpi 机:内存按
(设备dpi/桶dpi)²涨且插值变糊(详见 01 第二层)。- 先分两类再选策略——
UI 上把图缩小了,为什么内存还不降?
- 一句话答:内存由解码像素决定,不由最终显示尺寸决定——只改
width/height不解码降采样,仍会按桶 / 原图全量解码进内存(详见 01 第三层 ·11-image-loading)。
- 一句话答:内存由解码像素决定,不由最终显示尺寸决定——只改
| 栈 | UI 小图标 | 背景 / 大图 | 解码原理(你要记住的) |
|---|---|---|---|
| Android View | VectorDrawable 或 drawable-xxhdpi 单桶 | drawable-nodpi + inSampleSize | 桶 dpi → inDensity,设备 dpi → inTargetDensity;解码倍率 r = 设备dpi/桶dpi,内存 ∝ r² |
| Compose | 同上(painterResource) | 同上 | 与 View 同一套资源与解码,无 Flutter 式变体目录 |
| Flutter | flutter_svg 或配齐 1.0x/2.0x/3.0x | 单张 asset + cacheWidth/cacheHeight | AssetImage 按 dpr 选变体;缺失高倍变体时不按 density 放大解码,绘制阶段可能拉伸低清源图 |
| iOS(对照) | Asset Catalog @2x/@3x | Single Scale 单源高清图 | 离散 scale 1:1 点阵匹配,不做 Android 式桶缩放;缺 @3x 会在高分屏发糊 |
口诀:小图标靠倍率档(或矢量),大图靠单源 + 运行时自适应。完整决策流程见 01「资源问题的工程决策流程」。
布局与断点(详见 02)
- 平板 / 横屏为什么不能用纯比例放大?
- 一句话答:375 基准在 768~1024dp 平板会把整个 UI 放大 2~2.7 倍,图标巨大、布局稀疏;应改用断点(600 / 840dp)** 换布局而不是放大**(详见 02)。
- 三端断点分别怎么实现?
- 一句话答:Android View 用
values-sw600dp+ 运行时窗口宽度分类;Compose 用material3-window-size-class+material3-adaptive;Flutter 用LayoutBuilder自写断点或responsive_builder包(详见 02)。
- 一句话答:Android View 用
系统边界(详见 03)
- 字体放大、刘海屏、手势区、折叠屏为什么也属于“屏幕适配”?
- 一句话答:因为真实适配不只是“宽高对不对”,还包括字能不能读、按钮能不能点、内容会不会被系统栏挡住、窗口变化时布局会不会崩(详见 03)。
先做归因:适配问题属于哪一层
先问症状归 01 / 02 / 03 哪一层,再按「先检查什么」排查:
| 归属层 | 常见症状 | 先检查什么 | 专题入口 |
|---|---|---|---|
| 01 密度与资源 | 图标糊、大图 OOM、内存异常 | 矢量 / 倍率档、nodpi、解码 inSampleSize / cacheWidth;桶放反则内存按 (设备dpi/桶dpi)² 涨 | 01 |
| 02 窗口与断点 | 平板像放大版手机、横屏挤、分屏后双栏挤爆 | 是否全端套 375 缩放;是否缺 600/840 结构切换;是否按设备型号而非当前窗口宽度判断 | 02 |
| 03 系统边界 | 大字体裁切、底部按钮被挡、刘海 / 键盘 / 折叠态异常 | sp / TextScaler、容器是否写死高度、Expanded / weight;SafeArea / WindowInsets / viewInsets、键盘 IME | 03 |
屏幕适配三层模型
| 层次 | 解决什么 | 典型故障 | 不解决什么 |
|---|---|---|---|
| 第一层:密度与资源 | 逻辑单位、倍率、图片清晰度、解码内存 | 图标糊、大图 OOM、低桶内存 ×9 | 平板双栏、键盘遮挡 |
| 第二层:窗口与断点 | 当前窗口宽度、600/840 断点、栏位与导航 | 平板空、横屏挤、分屏仍双栏 | 字体截断、刘海挡内容 |
| 第三层:系统边界 | 字体、Insets、键盘、折叠/分屏、验证矩阵 | 大字体炸行、CTA 不可达 | 图片放错桶 |
读图顺序:01 → 02 → 03。01 偏资源与清晰度,02 偏断点与布局结构,03 偏字体、安全区、键盘、分屏这类上线边界。只做其中一层,都不算完整适配。
跨端统一原则
- 布局用逻辑单位(
dp/Dp/ 逻辑像素 /pt),避免布局尺寸直接写px。 - 小屏可微调,大屏换结构:375 比例法仅适合 compact 小屏补偿(建议
clamp(1.0, 1.2)),平板应增长栏位与信息密度,不整体放大。 - 文本服从可读性与无障碍:字号用
sp/TextScaler语义,不要全局禁用系统字体缩放。 - 按当前窗口与 Insets 判断,不按设备型号:分屏、折叠、多窗口会让同一台设备瞬间拥有不同可用宽度与安全区。
Android View / Compose / Flutter 实现速查
这张表按适配维度横向对照四端(主线三栈 + iOS 对照):同一类问题在各端「用什么 API / 单位 / 机制」——不是完整教程,而是复习时快速定位「我这端该查什么」。密度与资源归 01,断点与结构归 02,字体与安全区归 03。
| 维度 | Android View | Compose | Flutter | iOS(对照) |
|---|---|---|---|---|
| 密度 / 资源 | drawable-{桶} / nodpi / VectorDrawable | 继承 Android 资源体系 | 1.0x/2.0x/3.0x / flutter_svg | Asset Catalog @1x/@2x/@3x |
| 断点与结构 | values-sw600dp + WindowMetrics | WindowSizeClass / PaneScaffold | LayoutBuilder / MediaQuery | Size Class + 容器实际宽度 |
| 字体缩放 | sp(跟随系统 fontScale) | TextUnit.Sp + MaterialTheme.typography | MediaQuery.textScaler(勿全局禁用) | Dynamic Type / UIFontMetrics |
| 最小可点击区 | 约 48×48 dp(视觉可更小,触控区撑满) | minimumInteractiveComponentSize() | 约 48×48 逻辑像素(Padding 扩区) | 约 44×44 pt(HIG 建议) |
| 系统栏 / 安全区 | WindowInsetsCompat / edge-to-edge | WindowInsets / Scaffold padding | SafeArea / viewPadding / viewInsets | safeAreaLayoutGuide |
| 动态窗口 | 运行时窗口宽度 / 分屏 | WindowSizeClass + 自适应脚手架 | 断点 + 可伸缩布局骨架 | Split View / Stage Manager 看容器宽 |
三篇专题分别解决什么
| 序号 | 主题 | 文档 | 你会在这里解决什么判断 |
|---|---|---|---|
| 01 | dp / dpi 与图片资源放置 | 逻辑单位、资源倍率与图片解码:dp / dpi / scale 到内存 | 图为什么糊、内存为什么涨、资源如何准备;px = dp × dpi/160;图标→矢量/xxhdpi,背景→nodpi+降采样 |
| 02 | 断点响应式:三端实现 | 当前窗口、断点与响应式布局:从 375 小屏补偿到多栏结构 | 窗口变宽/变窄后如何换结构;统一断点 600/840dp;375 仅小屏;大屏换布局不放大 |
| 03 | 字体缩放、安全区、折叠屏与验证矩阵 | 字体、系统边界与动态窗口:屏幕适配的上线验收层 | 默认状态正常但真实用户环境为何仍会失败;字体可读 + 内容不被挡 + 窗口变化不崩 |
业界通用适配方案总表
这张表让你先知道行业里常见手段分别解决什么,再决定该深入 01、02 还是 03。
| 方案 | 主流栈 | 背后原理 | 最常见用法 | 重点边界 | 深入阅读 |
|---|---|---|---|---|---|
密度桶 / nodpi / 资源变体 | Android / Flutter / iOS 对照 | 先解决“同样物理尺寸下,图清不清、内存炸不炸” | Android drawable-{桶}、Flutter 2.0x/3.0x 资源 | 它只解决图片资源,不解决字体和布局 | 01 |
sw600dp / dimen 分桶 | Android View | 用资源限定符按最小宽度切布局和尺寸资源 | values-sw600dp、layout-sw600dp、平板大间距/双栏 XML | 静态分桶好用,但分屏和动态窗口仍需运行时判断 | 02 |
WindowMetrics / WindowSizeClass | Android / Compose | 读取当前窗口宽度,按 600/840dp 做结构切换 | 单栏/双栏/多栏、BottomBar/Rail/Drawer 切换 | 不该退化成整页倍率放大 | 02 |
| Compose adaptive / pane scaffold | Compose | 官方把主从式和多 pane 页面封成自适应骨架 | 列表-详情两栏、导航与内容并排 | 适合主从页,不是所有复杂页面都能直接套 | 02 |
LayoutBuilder / MediaQuery 断点 | Flutter | 在 widget 树内按约束/窗口宽度切布局 | 600/840 断点、动态列数、NavigationRail | 不该只按机型判断,也不该混成 375 全端缩放 | 02 |
| 375 设计稿缩放方案 | Flutter 为主 | 用 当前宽度 / 设计稿宽度 做小屏尺寸补偿 | flutter_screenutil、设计稿边距/圆角/图标微调 | 只适合 compact 小屏,不适合平板,更不该缩放正文文字 | 02 |
| 安全区 / Insets / SafeArea | Android / Compose / Flutter / iOS 对照 | 把系统栏、刘海、手势区、键盘占用空间让出来 | edge-to-edge、顶部标题避状态栏、底部 CTA 避 home indicator | 它解决系统边界,不解决断点结构 | 03 |
| 字体缩放跟随系统 | Android / Compose / Flutter | 让正文和交互信息随用户无障碍字体设置变化 | Android sp、Compose sp、Flutter TextScaler | 不能靠全局禁用缩放来“修适配” | 03 |
最小验证路径
上线前至少覆盖以下维度(完整矩阵与检查顺序见 03):
| 维度 | 最小矩阵 |
|---|---|
| 宽度 | 375 / 600 / 840+(可补 430、768、1024dp) |
| 方向 | 竖屏 / 横屏 |
| 字体 | 默认 / 放大 1.3x / 2.0x |
| 系统状态 | 无键盘 / 键盘弹起;全屏 / 分屏 |
| 文案 | 短中文 / 长中文 / 英文长词 |
对应 Labs 与推荐阅读顺序
- 先读 01(dp / dpi / 密度桶 / 放图策略)——“显示多大、内存多少”的地基;
- 再读 02(断点响应式三端实现)——“手机 / 平板 / 横屏怎么布局”;
- 最后读 03(字体 / 安全区 / 折叠屏 / 验证矩阵)——“为什么线上适配经常不是宽度问题,而是系统边界问题”;
- 需要“怎么把图片加载出来”时,跳转
11-image-loading。
可交叉验证的 Lab 入口:
- adaptive-layout-demo —— 375 基准缩放 vs 600/840 断点布局,对照手机 / 平板模拟宽度(对应 02)
- screen-adaptation (Android Compose + View/XML) —— Compose
WindowSizeClass+ListDetailPaneScaffold主从/导航切换,View/XMLvalues-sw600dp资源分桶 +WindowMetricsCalculator运行时判断(对应 02) - font-scale-safe-area-demo —— 大字体、Row 挤压、按钮高度、SafeArea 与键盘遮挡(对应 03)
11-image-loading的 Android / Flutter 示例 —— 观察“约束驱动采样”“解码尺寸”和“显示尺寸”的关系(对应 01)
01 更像“资源怎么准备”;02/03 更像“布局和系统边界怎么兜住”。
常见考察与工程表达
按「归因 → 方案 → 验证」组织,避免只背 API 名词:
| 类型 | 考察什么 | 应回到 |
|---|---|---|
| 基础换算 | dp / px / sp 区别;1080px 为何常 ≈ 360dp 逻辑宽 | 01 |
| 资源策略 | 图标为何矢量;背景为何 nodpi + 降采样;Flutter 只放 1x 为何发糊 | 01 |
| 布局策略 | 375 为何不能平推平板;为何增长栏位而非整体放大 | 02 |
| 系统边界 | 状态栏/刘海/手势区/键盘如何拿 insets | 03 |
| 验证 | 横屏、分屏、折叠、大字体、多语言如何测 | 03 |
| 选型 | 何时 sw600dp、何时 WindowSizeClass、何时只是 SafeArea/字体弹性 | 00 归因表 |
常见优化点
- 资源优化:图标矢量化;背景大图单高质量源 + 降采样;避免多桶重复大图导致包体膨胀。
- 布局优化:大屏增加栏位、信息密度、导航形态(bottom bar → rail → drawer),而不是整套放大。
- 可读性优化:文字用
sp/ text scaler;按钮保留最小点击区域;长文案 / 多语言别写死宽高。 - 系统边界优化:edge-to-edge + 安全区;键盘弹起时可滚动或抬升;顶部图文注意刘海遮挡。
- 验证优化:建设“375 / 430 / 600 / 768 / 1024dp + 横竖屏 + 字体放大 + 分屏”矩阵,而不是只靠肉眼抽测。
- 方案收口优化:资源问题先回 01,结构问题先回 02,字体 / 安全区 / 键盘 / 分屏问题先回 03,避免把所有适配都误判成“再调几个 dp”。
复习检查题
一张 480×480 的位图放
drawable-mdpi,在 xxhdpi(3x)手机上解码后内存是原图的几倍?答:9 倍。解码尺寸 = 480×(480/160) = 1440px,宽高各放大 3 倍,内存按面积放大 3² = 9 倍;且只有 480px 细节,放大显示会模糊。
为什么 iOS 不使用 Android 式 density 桶,而 Android 需要?
答:iOS 设备密度离散(@1x/@2x/@3x),用“点(point)”做逻辑单位并按 scale 精确匹配资源,不按 dpi 桶缩放位图;但仍需多倍率切图。Android 设备密度连续,必须用桶 +
inDensity/inTargetDensity解码缩放来保证 dp 物理尺寸一致。平板(1024dp)用 375 基准法适配会出什么问题?正确做法是什么?
答:缩放系数 s ≈ 1024/375 ≈ 2.73,整个 UI 被放大近 3 倍,图标巨大、布局稀疏。正确做法是按断点(compact <600 / medium 600~840 / expanded >840)切换布局(两栏 / 多列),而不是纯比例放大。
为什么字体缩放和安全区也属于屏幕适配?
答:因为真实用户看到的是“完整页面”,不是单纯尺寸换算结果。只要字体放大后炸行、内容被刘海/手势区/键盘遮住,用户就会认为“这个页面没适配好”。
你会怎么快速区分这是“资源问题”“布局问题”还是“字体/系统边界问题”?
答:先问它影响的是图像清晰度还是页面结构还是文本可读性。清晰度/内存先看 01,栏位/导航/断点先看 02,字体炸行/安全区/键盘/分屏先看 03。
一页速记
三层:资源与密度 → 窗口与断点 → 系统边界与验证。
四条原则:逻辑单位;大屏换结构;文本跟系统;看当前窗口与 Insets。
三个专题:01 资源 · 02 断点 · 03 边界。
- 换算:
px = dp × (dpi/160);1080p ≈ 360dp 逻辑宽。 - 放图:图标 → 矢量 / xxhdpi 单桶;背景大图 → nodpi + 降采样。
- 低桶是坑:内存按
(设备dpi/桶dpi)²放大,且模糊。 - 断点:600 / 840dp,三端统一;375 只配小屏。
- 真实适配:不仅看宽高,还看字体、点击区、安全区、键盘、折叠态和验证矩阵。