29、WindowManager 在 Jetpack Compose 中的适配:Compose 如何与 Window 交互?
说实话,Compose 出来那会儿,我第一反应是:这玩意儿怎么跟 WindowManager 打交道?
毕竟我们做 Android 开发的都知道,WindowManager 是系统级的窗口管理服务。而 Compose 呢,它是个声明式 UI 框架,跑在 View 系统之上。这两者之间,到底是怎么个配合法?
今天我们就来聊聊这个。我会结合我自己的踩坑经历,把 Compose 和 Window 的交互机制讲清楚。
Compose 的窗口本质:它还是个 View
很多人以为 Compose 完全抛弃了 View 系统。其实不是的。
你想想看,Compose 最终还是要渲染到屏幕上。而屏幕上的内容,在 Android 里必须通过 Window 来承载。所以 Compose 再怎么声明式,底层还是得跟 WindowManager 打交道。
具体来说,Compose 的 setContent { } 背后,实际上创建了一个 ComposeView。这个 ComposeView 继承自 ViewGroup,它就是一个普通的 View。然后通过 Activity 或 Dialog 的 setContentView() 方法,把这个 View 挂到 Window 上。
核心结论: Compose 并没有绕过 WindowManager。它只是把 View 层的内容替换成了 Compose 的渲染逻辑。Window 的创建、管理、属性设置,依然走的是 WindowManager 那套流程。
Compose 中获取 Window 的几种方式
我在项目中经常需要获取 Window 对象,比如设置屏幕亮度、调整窗口 flag。在 Compose 里怎么做?
方式一:通过 LocalContext
最直接的方式,就是通过 LocalContext.current 拿到 Activity,然后获取 Window。
@Composable
fun MyScreen() {
val context = LocalContext.current
val window = remember {
(context as? Activity)?.window
}
// 使用 window 设置 flag
LaunchedEffect(Unit) {
window?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
}
嗯,这里要注意:LocalContext 拿到的可能是 Application 上下文,不一定能强转为 Activity。我建议加个安全判断,或者用 LocalActivity 这个 CompositionLocal。
方式二:使用 LocalWindowInfo
Compose 1.2 之后,官方提供了 LocalWindowInfo。这个可以拿到窗口的尺寸、安全区域等信息。
@Composable
fun WindowInfoExample() {
val windowInfo = LocalWindowInfo.current
val density = LocalDensity.current
// 获取窗口尺寸(单位:dp)
val windowSize = with(density) {
windowInfo.containerSize.toDpSize()
}
Text("窗口尺寸:${windowSize.width} x ${windowSize.height}")
}
我个人习惯用这个方式,因为它不依赖 Activity,在 Dialog 或 Popup 里也能正常工作。
Compose 中的 Dialog 与 Window
说到 Dialog,这里有个坑。我记得有一次,我在 Compose 里用 Dialog 组件弹出一个全屏对话框,结果发现状态栏被盖住了。
为什么会这样?因为 Compose 的 Dialog 本质上还是通过 DialogWindow 创建的,它默认的 Window 类型是 TYPE_APPLICATION。如果你想要全屏效果,得手动设置 Window 参数。
@Composable
fun FullScreenDialog(
onDismiss: () -> Unit
) {
val context = LocalContext.current
Dialog(
onDismissRequest = onDismiss,
properties = DialogProperties(
usePlatformDefaultWidth = false // 允许全屏
)
) {
// 这里需要手动设置 Window 参数
val dialog = LocalDialog.current
LaunchedEffect(Unit) {
dialog?.window?.apply {
setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN
)
// 设置布局参数
setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
}
}
Box(
modifier = Modifier.fillMaxSize().background(Color.Black)
) {
// 你的全屏内容
}
}
}
注意: LocalDialog 是 Compose 内部提供的 CompositionLocal,需要在 Dialog 的 content 内部使用。如果你在 Dialog 外部调用,会拿到 null。
Compose 与 WindowInsets:安全区域的适配
这个我必须要讲。很多 Compose 新手都会遇到一个问题:内容被状态栏或导航栏遮挡。
在传统 View 系统里,我们通过 fitsSystemWindows 来处理。在 Compose 里,用的是 WindowInsets 系统。
@Composable
fun SafeAreaScreen() {
Box(
modifier = Modifier
.fillMaxSize()
.windowInsetsPadding(
WindowInsets.systemBars // 自动避开状态栏和导航栏
)
) {
// 你的内容
}
}
但这里有个细节:windowInsetsPadding 只对当前组件生效。如果你有嵌套滚动,或者需要处理 IME 键盘弹出,情况会更复杂。
我曾经在做一个聊天界面时,键盘弹起后输入框被挡住了。后来发现是 imePadding() 没加对位置。正确的做法是:
@Composable
fun ChatScreen() {
Column(
modifier = Modifier
.fillMaxSize()
.imePadding() // 给输入框留出键盘空间
.windowInsetsPadding(WindowInsets.systemBars)
) {
// 消息列表
LazyColumn(
modifier = Modifier.weight(1f)
) { /* ... */ }
// 输入框
TextField(
value = "",
onValueChange = { },
modifier = Modifier.fillMaxWidth()
)
}
}
Compose 中的 Popup 与 WindowManager
Compose 的 Popup 组件,底层也是通过 WindowManager 添加一个悬浮窗口。不过它做了很多封装,比如对齐、偏移等。
但如果你需要更精细的控制,比如自定义弹出位置、动画,那还是得直接操作 WindowManager。
@Composable
fun CustomPopup(
anchorBounds: Rect,
content: @Composable () -> Unit
) {
val context = LocalContext.current
val windowManager = remember {
context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
}
// 这里需要创建一个 ComposeView 作为弹出内容
// 然后通过 WindowManager.addView() 添加到窗口
// 具体代码略长,核心思路就是:
// 1. 创建 ComposeView
// 2. 设置 LayoutParams(类型、位置、宽高)
// 3. 调用 windowManager.addView()
// 4. 在 dispose 时调用 windowManager.removeView()
}
小技巧: 如果你只是需要简单的弹出菜单,直接用 Compose 的 DropdownMenu 就好。它内部已经处理好了 WindowManager 的交互。只有当你需要完全自定义的弹出窗口时,才考虑手动操作。
知识体系总览
下面这张图,是我梳理的 Compose 与 Window 交互的核心脉络:
避坑指南:我踩过的几个坑
最后,分享几个我实际项目中遇到的坑,希望能帮你少走弯路。
- 坑一:Dialog 的 Window 类型 - 我曾经在 Compose Dialog 里弹出另一个 Dialog,结果第二个 Dialog 无法获取焦点。原因是 Dialog 默认的 Window 类型是
TYPE_APPLICATION,嵌套使用时需要调整 flag。 - 坑二:WindowInsets 的时机 - 在
LaunchedEffect里读取WindowInsets可能拿到 0。因为 Compose 的布局还没完成。我建议用BoxWithConstraints或者onSizeChanged来获取真实尺寸。 - 坑三:Popup 的触摸事件 - Compose 的 Popup 默认会拦截外部触摸事件。如果你想让点击外部区域关闭 Popup,需要设置
popupPositionProvider和onDismissRequest。这个我折腾了半天才搞清楚。
嗯,关于 Compose 与 Window 的交互,核心就是理解 ComposeView 这个桥梁。它把声明式的 Compose 代码,翻译成了 WindowManager 能理解的 View 操作。只要抓住这个本质,很多问题就迎刃而解了。
一句话总结: Compose 没有重新发明窗口管理。它只是换了一种更优雅的方式,来操作 WindowManager 这个老朋友。
公众号:蓝海资料掘金营,微信deep3321