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)。

系统通过 WindowManagerWindowLayoutInfo 来通知应用当前的折叠状态。但更直接的方式是监听 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 知识体系总览

下面这张图是我自己梳理的多屏适配核心逻辑。你看一眼就能明白整个流程:

多屏与折叠屏适配核心逻辑 物理屏 物理分辨率、刷新率 逻辑屏 逻辑尺寸、density 折叠状态 展开/折叠/半折叠 DisplayManager getDisplays() / DisplayListener / createDisplayContext() Presentation 副屏显示独立内容 Configuration变化 onConfigurationChanged() WindowLayoutInfo 折叠角度、铰链位置 自适应布局 + 流畅渲染

从这张图你可以看到,整个适配链路是自底向上的。硬件层提供原始数据,系统层通过 DisplayManager 统一管理,应用层再根据这些信息做布局和渲染决策。任何一个环节出问题,最终表现就是「卡顿」或「错乱」。

19.6 避坑指南

最后分享几个我踩过的坑:

  • 不要假设屏幕数量固定。 用户可能随时插拔外接显示器。用 DisplayListener 动态监听。
  • Presentation 的 Context 必须用 createDisplayContext() 创建。 否则资源加载会使用默认屏幕的 density,导致布局错位。
  • 折叠状态变化时,Activity 可能会重建。 如果你在 AndroidManifest 里设置了 configChanges,记得把 screenSizesmallestScreenSize 加上,否则系统会销毁重建。
  • 半折叠状态(tent mode)下,屏幕尺寸不变但折叠角度变化。 这时候要用 WindowLayoutInfo 获取铰链位置和角度,而不是单纯依赖尺寸变化。

我曾经在一个项目中,因为没处理好折叠状态下的 density 变化,导致内屏上的按钮全部偏小。用户反馈说「点都点不到」。排查了两天才发现是 density 从 2.5 跳到了 2.75,而布局文件里用的 dp 值没做适配。从那以后,我每次做折叠屏适配都会在 onConfigurationChanged() 里打印一份完整的 Display 信息日志。

好了,多屏适配的核心逻辑就这些。记住一句话:别把物理屏当逻辑屏用,也别把逻辑屏当物理屏算。 搞清楚映射关系,剩下的就是细心了。


公众号:蓝海资料掘金营,微信deep3321