19、多屏与折叠屏适配:DisplayManager、逻辑屏与物理屏映射、折叠状态变化处理
说实话,多屏适配这个话题,在几年前还是个「高级特性」。但现在呢?折叠屏手机都出好几代了,车机、平板、桌面模式也遍地开花。你想想看,一个App如果连多屏都搞不定,用户一折叠、一展开就闪退或者布局错乱,那体验得多糟糕。
我个人习惯把多屏适配分成三个层次来理解:硬件层(物理屏长什么样)、系统层(DisplayManager怎么管)、应用层(我们开发者怎么接)。今天我们就一层层剥开来看。
19.1 物理屏与逻辑屏:别再傻傻分不清
先问一个问题:你的手机屏幕分辨率是 1080×2400,那你的逻辑分辨率是多少?
很多人会脱口而出「就是 1080×2400 啊」。其实不对。物理屏是硬件像素,逻辑屏是系统用来布局的虚拟尺寸。两者之间隔着一个东西——密度因子(density)。
举个例子。我手头有一台折叠屏设备,展开后物理分辨率是 2208×1840,但逻辑分辨率只有 1104×920。为什么?因为它的 density 是 2.0。说白了,系统用逻辑坐标来布局,最终渲染时再乘以 density 映射到物理像素。
在多屏场景下,这个映射关系会更复杂。每个屏幕可能有不同的 density、不同的旋转方向、甚至不同的刷新率。
核心概念:
- 物理屏(Physical Display):硬件层面的显示设备,有固定的像素尺寸和刷新率。
- 逻辑屏(Logical Display):系统为应用提供的虚拟画布,基于 density 和 dpi 计算。
- 映射关系:逻辑像素 × density = 物理像素。
我在项目中遇到过一个问题:某款折叠屏手机在内屏和外屏切换时,density 居然不一样。内屏 density 是 2.75,外屏是 2.5。结果呢?用 dp 写的布局在外屏上显示正常,一到内屏就偏小。排查了半天才发现是 density 变了。嗯,这个坑我替你们踩过了。
19.2 DisplayManager:系统层面的屏幕管家
Android 里谁在管这些屏幕?DisplayManager。它是系统服务,负责枚举所有可用的显示设备、监听屏幕插拔、管理显示模式。
获取方式很简单:
DisplayManager dm = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
Display[] displays = dm.getDisplays();
for (Display display : displays) {
Log.d("MultiDisplay", "Display ID: " + display.getDisplayId());
Log.d("MultiDisplay", "Display Name: " + display.getName());
}
这段代码会列出当前系统里所有的屏幕。注意,不只是物理屏,虚拟屏(比如投屏、录屏用的)也会被列出来。
每个 Display 对象都携带了关键信息:
| 属性 | 说明 | 获取方式 |
|---|---|---|
| DisplayId | 屏幕唯一标识 | display.getDisplayId() |
| Size | 逻辑尺寸(像素) | display.getSize() |
| RealSize | 物理尺寸(像素) | display.getRealSize() |
| Rotation | 旋转角度(0/90/180/270) | display.getRotation() |
| RefreshRate | 刷新率(Hz) | display.getRefreshRate() |
小技巧:如果你需要监听屏幕插拔,可以用 DisplayManager.DisplayListener。回调里有 onDisplayAdded()、onDisplayRemoved()、onDisplayChanged()。我在做车机投屏时就用这个接口来感知副屏的接入和断开。
19.3 折叠状态变化:从展开到折叠,你的App扛得住吗?
折叠屏最核心的体验就是「形态变化」。展开是平板,折叠是手机。中间还有一个半折叠状态(tent mode)。
系统通过 WindowManager 的 WindowLayoutInfo 来通知应用当前的折叠状态。但更直接的方式是监听 Configuration 变化。
当屏幕折叠或展开时,系统会触发 onConfigurationChanged()。这时候你需要重新检查屏幕尺寸、density、以及折叠角度。
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// 检查屏幕尺寸是否变化
if (newConfig.screenWidthDp != oldConfig.screenWidthDp ||
newConfig.screenHeightDp != oldConfig.screenHeightDp) {
// 重新布局
reLayoutForNewScreenSize();
}
// 检查是否是折叠状态变化
if (newConfig.isScreenRound != oldConfig.isScreenRound) {
// 处理折叠/展开
handleFoldStateChange();
}
}
这里有个细节:isScreenRound 这个字段在折叠屏上其实表示「屏幕是否处于折叠状态」。嗯,名字有点误导,但 Google 就是这么设计的。
注意:千万不要在 onConfigurationChanged() 里做耗时操作。这个回调是在 UI 线程执行的。我曾经见过有人在里面做网络请求,结果界面卡顿到飞起。正确的做法是:只做标记,然后在下一帧或者通过 Handler 异步处理。
19.4 实战:多屏窗口的创建与渲染
如果你想让 App 的内容显示在副屏上(比如折叠屏的内屏和外屏同时显示不同内容),你需要创建一个 Presentation。
Presentation 是一个特殊的 Dialog,它绑定到指定的 Display 上。用法如下:
public class MyPresentation extends Presentation {
public MyPresentation(Context context, Display display) {
super(context, display);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.presentation_layout);
}
}
// 在 Activity 中启动
DisplayManager dm = (DisplayManager) getSystemService(Context.DISPLAY_SERVICE);
Display[] displays = dm.getDisplays();
if (displays.length > 1) {
MyPresentation presentation = new MyPresentation(this, displays[1]);
presentation.show();
}
这里要注意:Presentation 的 Context 必须是 DisplayContext,否则资源加载会出错。我建议用 createDisplayContext() 来创建:
Context displayContext = createDisplayContext(display);
MyPresentation presentation = new MyPresentation(displayContext, display);
19.5 知识体系总览
下面这张图是我自己梳理的多屏适配核心逻辑。你看一眼就能明白整个流程:
从这张图你可以看到,整个适配链路是自底向上的。硬件层提供原始数据,系统层通过 DisplayManager 统一管理,应用层再根据这些信息做布局和渲染决策。任何一个环节出问题,最终表现就是「卡顿」或「错乱」。
19.6 避坑指南
最后分享几个我踩过的坑:
- 不要假设屏幕数量固定。 用户可能随时插拔外接显示器。用
DisplayListener动态监听。 - Presentation 的 Context 必须用
createDisplayContext()创建。 否则资源加载会使用默认屏幕的 density,导致布局错位。 - 折叠状态变化时,Activity 可能会重建。 如果你在
AndroidManifest里设置了configChanges,记得把screenSize和smallestScreenSize加上,否则系统会销毁重建。 - 半折叠状态(tent mode)下,屏幕尺寸不变但折叠角度变化。 这时候要用
WindowLayoutInfo获取铰链位置和角度,而不是单纯依赖尺寸变化。
我曾经在一个项目中,因为没处理好折叠状态下的 density 变化,导致内屏上的按钮全部偏小。用户反馈说「点都点不到」。排查了两天才发现是 density 从 2.5 跳到了 2.75,而布局文件里用的 dp 值没做适配。从那以后,我每次做折叠屏适配都会在 onConfigurationChanged() 里打印一份完整的 Display 信息日志。
好了,多屏适配的核心逻辑就这些。记住一句话:别把物理屏当逻辑屏用,也别把逻辑屏当物理屏算。 搞清楚映射关系,剩下的就是细心了。