在折叠屏展开和收起状态下,如何动态调整 UI 布局?

解读

国内折叠屏机型(华为 Mate X 系列、荣耀 Magic V、OPPO Find N、vivo X Fold、小米 MIX Fold 等)已占高端机 30% 以上份额,用户对“大屏体验”极度敏感。面试时,面试官想确认三点:

  1. 你是否真的在国产折叠屏真机上跑过代码,而不是只看过官方文档;
  2. 能否在“配置变化”这一高频场景下,保证业务不中断、状态不丢失、动画不卡顿;
  3. 是否能把 Google 官方方案与国内厂商差异化特性(如华为“平行视界”、荣耀“分屏拖拽”、OPPO“悬停模式”)无缝结合。

一句话:既要“保活”,又要“保体验”,还要“保性能”。

知识点

  1. Jetpack WindowManager 1.1+(国内厂商已内置 Google 提供的 WindowMetrics API,无需额外集成)
  2. WindowLayoutInfo/ FoldingFeature 的 isSeparating、orientation、state(FLAT / HALF_OPENED)
  3. Activity 生命周期与 android:configChanges 的取舍;国内 ROM 对“重启 Activity”策略的魔改差异
  4. SavedStateHandle + ViewModel + rememberSaveable 三重状态恢复链
  5. Compose 的 WindowSizeClass(Dp 断点)与折叠屏“物理折痕”的映射关系
  6. 国产系统“平行视界”Activity 双实例机制:同一应用两个 Activity 分屏,需单独处理 Intent 复用
  7. 性能:折叠动画 400 ms 内完成,避免在 onResume 做重 IO;GPU 渲染层必须关闭不必要的离屏缓存
  8. 测试:adb shell wm size/overlay 模拟折痕,华为云测、荣耀云测、OPPO 云测真机 30 分钟免费额度

答案

“我去年在 OPPO Find N3 上做商旅 App 折叠适配,核心思路分三步:

第一步,统一数据源。
在 Application 级通过 WindowManager 注册 WindowLayoutInfo 回调,把 FoldingFeature 的 isSeparating、orientation、state 映射成自定义的 ScreenPosture(FULL_INNER、DUAL_INNER、HALF_OPEN、TABLETOP)。用 SharedFlow 分发,确保单 Activity 与多 Fragment 都能零耦合监听。

第二步,UI 分层决策。
Compose 侧直接使用 androidx.compose.material3.windowsizeclass,将宽度断点与 ScreenPosture 做联合判断:

  • 当 widthDp ≥ 600 且 isSeparating = false → 展开大屏,采用 List-Detail 双栏;
  • 当 isSeparating = true → 折痕居中,自动切换成双窗口模式,左侧放主列表,右侧放详情 Fragment,并通过 Jetpack Navigation 的 nested graph 隔离回退栈;
  • 当 state == HALF_OPENED && orientation == VERTICAL → 悬停笔记本模式,下半屏固定输入框,上半屏渲染预览,利用 ConstraintLayout 的 flowVertically 链快速重排。

第三步,生命周期兜底。
国内 ROM 对 configChanges 的“重启 Activity”策略并不完全遵守 AOSP,我在 AndroidManifest 中只声明了 orientation|screenSize|smallestScreenSize|screenLayout,把其余配置交给系统,同时在 ViewModel 里用 SavedStateHandle 缓存列表滚动位置与搜索关键词;Compose 侧用 rememberSaveable 保存 LazyList 的 firstVisibleItemIndex,实测折叠动画 380 ms 内完成,无闪屏无重启。”

拓展思考

  1. 如果业务要求“折叠过程中视频不中断”,如何避开 Activity 重启?
    答:在 manifest 追加 uiMode|density,但把播放器放到 Service + SurfaceView,通过 WindowManager 的 displayId 切换 Surface 的附属 Display,即可实现“无缝迁移”。

  2. 国内厂商“平行视界”会把同一个 App 的两个 Activity 放进左右屏,如何防止单例数据库被重复初始化?
    答:在 Application 的 onCreate 中加 ProcessName 判断,只有主进程才初始化 Room,双实例进程通过 ContentProvider 暴露 URI,实现单进程单数据库。

  3. 折叠动画期间,GPU 负载骤升,如何量化?
    答:用 perfetto 抓取 SurfaceFlinger 的 GPU frequency 与 frame drops,目标:动画 400 ms 内掉帧 ≤ 2 帧,GPU 频率提升 ≤ 20 %;若超标,则把 Compose 的 LazyColumn 缓存策略从 Recycling 改为 LazyList 的 beyondBoundsItemCount = 0,减少离屏渲染。