Administrator
发布于 2026-08-26 / 33 阅读
0
0

系列文章《Android 工程师的 100 篇进阶笔记》第 93 篇:可折叠设备适配实战:从 API 到工程化

前言

2026 年的可折叠手机出货量预计破 5000 万台。但你打开国内某 Top 5 媒体 App 看看——横屏、大屏、折叠屏状态全是"撑大到顶"模式,跟普通竖屏 App 没差。

工程师的难处:市面上至少三种折叠态(书、笔记本、平板)、两种铰链姿态(垂直、水平)、三种屏幕比例(21:9、4:3、1:1),光配置资源就够你喝一壶。

这篇文章不讲概念,直接上能落地的工程方案。

一、Jetpack WindowManager 1.4:从感知到响应

dependencies {
  implementation("androidx.window:window:1.4.0")
  // 测试
  testImplementation("androidx.window:window-testing:1.4.0")
}

val tracker = WindowInfoTracker.getOrCreate(activity)
lifecycleScope.launch {
  tracker.windowLayoutInfo(activity).collect { info ->
    val feature = info.displayFeatures
      .filterIsInstance<FoldingFeature>()
      .firstOrNull()

    val foldingState = feature?.state ?: FoldingFeature.State.FLAT
    val orientation = feature?.orientation ?: FoldingFeature.Orientation.VERTICAL

    when {
      foldingState == FoldingFeature.State.HALF_OPENED -> showBookLayout(orientation)
      foldingState == FoldingFeature.State.FLAT && feature != null -> showFoldedLayout()
      else -> showStandardLayout()
    }
  }
}

要在仿真器调试:Android Studio Koala 起内置了 Foldable、Flip、Tablet 几种虚拟设备。

二、Hinge-aware 布局:别让按钮落在铰链上

工程上最容易翻车的是按钮或文字被铰链"截两半"。

@Composable
fun HingeAwareLayout(
  content: @Composable ColumnScope.() -> Unit
) {
  val feature = LocalFoldingFeature.current
  if (feature?.state == FoldingFeature.State.HALF_OPENED) {
    val hingeRect = feature.bounds
    Column {
      Box(modifier = Modifier.weight(1f).fillMaxWidth()) { content() }
      Spacer(modifier = Modifier.height(hingeRect.height.dp))
      Box(modifier = Modifier.weight(1f).fillMaxWidth()) { content() }
    }
  } else {
    Column { content() }
  }
}

原理是给铰链让出明确的 Spacer,把内容拆到两块独立区域。微软 Surface Duo 的官方 Sample 里也是这个套路。

三、Tabletop / Book Mode:交互模型根本不同

模式

屏幕关系

主要交互

UI 建议

Book Mode

上下两屏

用户像看书一样操作

上下分区,顶部放控件、底部放内容

Tabletop

笔记本形态(铰链水平 90°)

一手托机器、一手触屏

上半屏显示内容,下半屏变触控板

Laptop

笔记本形态(铰链垂直 90°)

双手键盘+触屏

上半屏作屏幕、下半屏化作键盘

Tabletop 模式比较特殊——下半屏可以变成 Touchpad:

// 监测 FoldingFeature.Orientation.HORIZONTAL + State.HALF_OPENED
val tableTopMode = foldingFeature?.let {
  it.orientation == FoldingFeature.Orientation.HORIZONTAL &&
    it.state == FoldingFeature.State.HALF_OPENED
} ?: false

if (tableTopMode) {
  BottomPanelTouchpad(onMove = { dx, dy -> scrollBy(dx, dy) })
}

四、资源目录:按屏幕宽度而不是"sw600dp"

这是常被忽略的点。折叠屏展开后宽度普遍 ≥ 600dp,但铰链一关瞬间回到 320dp。

正确的资源配置:

res/
├── layout/                  # 默认(折叠态)
├── layout-sw600dp/          # 平板与展开态
├── layout-sw720dp/          # 折叠屏展开 + 桌面模式
├── layout-sw840dp/          # 大折叠屏双屏合盖
└── values-h600dp/           # 仅用于高度型折叠屏

不要用 drawable-sw600dp 这种"按唯一维度"的策略——折叠态的 sw 跟展开态能差到 600dp。

五、相机预览与多任务:相机别独占屏幕

折叠屏展开后,用户的"视频预览+操控"如果还像普通手机那样全屏,会出现两种错位:

  1. 用户两个手握在上半屏,下半屏的按钮够不着;

  2. 横屏布局被迫拉成竖向挤压。

val hinge = windowLayoutInfo.displayFeatures
  .filterIsInstance<FoldingFeature>().firstOrNull()
if (hinge?.state == FoldingFeature.State.HALF_OPENED) {
  // 把相机预览控件推到上半屏,按钮推到下半屏
  CameraDualPanelLayout(
    topPanel = { PreviewView() },
    bottomPanel = { CameraControls() }
  )
} else {
  CameraFullScreenLayout() // 折叠态走原方案
}

六、性能:折叠屏 120Hz 不是默认

折叠屏屏幕大、刷新率高,渲染压力翻倍。优化建议:

  • 避免全屏渐变——会强制重绘整个 surface;

  • 用 LazyColumn/LazyRow——别再用 ScrollView 在 7 英寸屏上铺一堆子 View;

  • Material 3 Expressive 的 spring 动画——在折叠屏上 eST 会放大 1.5 倍,要用 Modifier.animateContentSize 而不是 Animatable;

  • 折叠时回收资源——监听到 State.FLAT 时主动 onTrimMemory 把不用的纹理释放掉。

七、测试:仿真器 + 真机缺一不可

仿真器只能跑 WindowInfoTracker,跑不了铰链物理动画。要测手感必须真机:

  • 三星 Galaxy Z Fold / Z Flip:铰链角度传感器最准;

  • 华为 Mate X:铰链垂直 90° 时触发"笔记本模式"更稳定;

  • Pixel Fold:用 adb shell wm size 2200x1840 模拟展开。

每种机型都要走一遍:

  1. 启动 App → 系统级折叠监听是否触发;

  2. 旋转 → 重新订阅 layoutInfo;

  3. 锁屏 → onStop 后 state 缓存是否正确还原。

八、上线门槛检查清单

  • 折叠态资源都能找到对应 dpi;

  • 横屏 / 折叠态没有依赖单一竖屏布局;

  • 大屏下没有显示"建议使用手机访问"提示;

  • 平板模式支持 orientation lock 控制;

  • 关键交互(购买、表单提交)在折叠态/展开态都可用。

小结

折叠屏的"挑战"不在 API,而在测试矩阵。下一篇文章我们换到大屏的另一头:Android TV 与智能投影应用适配,工程难度更高——焦点逻辑跟触控根本不是一套。

参考阅读

  • Jetpack WindowManager 1.4 API

  • 大屏应用质量指南(developer.android.com)

  • 「Foldables by Google」官方 Sample


评论