Android 多窗口适配实战:从配置到生命周期管理的全流程解析
1. 多窗口模式基础配置第一次给应用做多窗口适配时我在清单文件里踩了个坑。当时随手把resizeableActivity设成false结果测试时发现分屏功能完全失效。后来才明白这个属性是控制多窗口的开关就像给应用装了个分屏许可证。在AndroidManifest.xml中配置多窗口支持有两种方式!-- 全局配置所有Activity生效 -- application android:resizeableActivitytrue !-- 单个Activity配置会覆盖全局设置 -- activity android:name.VideoPlayerActivity android:resizeableActivityfalse/ /application这里有个隐藏知识点从Android 12API 31开始大屏设备会强制启用多窗口模式。我去年适配折叠屏时就遇到过这种情况即使设置了resizeableActivityfalse应用依然会被系统强制分屏。这时候就需要配合configChanges属性来处理activity android:name.MainActivity android:configChangesscreenSize|smallestScreenSize|screenLayout|orientation实测发现当用户拖动分屏边界时系统会连续触发多次配置变更。有次我在onConfigurationChanged()里直接更新布局结果导致界面闪烁。后来改用延迟100ms的防抖处理才解决private var configChangeJob: Job? null override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) configChangeJob?.cancel() configChangeJob lifecycleScope.launch { delay(100) updateLayout(newConfig) } }2. 布局动态适配技巧多窗口模式下最头疼的就是布局适配。我做过一个电商应用在分屏时商品列表直接挤成一团。后来总结出三种适配方案方案一多套布局文件在res目录下创建不同尺寸的布局res/ layout/ # 默认布局 layout-sw600dp/ # 最小宽度600dp的布局 layout-w800dp/ # 宽度800dp的布局方案二ConstraintLayout动态约束用代码动态调整约束条件val constraints binding.root.layoutParams as ConstraintLayout.LayoutParams if (isInMultiWindowMode) { constraints.matchConstraintPercentWidth 0.6f } else { constraints.matchConstraintPercentWidth 1f }方案三Jetpack WindowManager这是我最推荐的方式通过回调监听窗口变化val windowManager WindowManager(context) windowManager.registerLayoutChangeCallback { windowMetrics - val width windowMetrics.bounds.width() val height windowMetrics.bounds.height() updateLayout(width, height) }特别提醒处理键盘弹出时记得在清单里加上activity android:name.ChatActivity android:windowSoftInputModeadjustResize3. 生命周期管理实战多窗口下的生命周期比想象中复杂。有次用户反馈视频播放器在分屏时突然暂停排查发现是误用了onPause()。正确的处理姿势应该是override fun onPause() { // 错误做法在这里暂停视频 super.onPause() } override fun onStop() { // 正确做法在onStop暂停 player.pause() super.onStop() } override fun onStart() { player.play() super.onStart() }从Android 10开始系统支持多Activity同时Resumed状态。需要在清单中添加meta-data android:nameandroid.allow_multiple_resumed_activities android:valuetrue /重点监控这三个回调// 窗口焦点变化 override fun onWindowFocusChanged(hasFocus: Boolean) { if (hasFocus) { // 获取焦点时的操作 } } // 多窗口模式切换 override fun onMultiWindowModeChanged(isInMultiWindowMode: Boolean) { if (isInMultiWindowMode) { // 进入分屏 } else { // 退出分屏 } } // 配置变更 override fun onConfigurationChanged(newConfig: Configuration) { // 处理旋转、分屏大小变化等 }4. 画中画(PIP)模式专项适配视频类应用必须掌握的画中画模式核心代码其实很简单// 进入画中画 private fun enterPipMode() { val params PictureInPictureParams.Builder() .setAspectRatio(Rational(16, 9)) .build() enterPictureInPictureMode(params) } // 处理画中画事件 override fun onPictureInPictureModeChanged( isInPipMode: Boolean, newConfig: Configuration? ) { if (isInPipMode) { binding.controlPanel.visibility View.GONE } else { binding.controlPanel.visibility View.VISIBLE } }但实际开发中会遇到几个典型问题触摸事件穿透画中画窗口默认不响应触摸需要重写dispatchTouchEvent播放控制通过RemoteAction添加控制按钮状态同步使用ViewModel保持数据一致性我遇到最坑的问题是华为EMUI系统对PIP的特殊处理需要额外适配if (Build.MANUFACTURER.equals(HUAWEI, ignoreCase true)) { // 华为设备特殊处理 }5. 多窗口下的特殊场景处理场景一拖放交互分屏模式下跨应用拖拽是个亮点功能。适配时要注意// 设置拖拽阴影 val shadowBuilder View.DragShadowBuilder(view) view.startDragAndDrop( ClipData.newPlainText(label, data), shadowBuilder, null, View.DRAG_FLAG_GLOBAL ) // 接收拖拽数据 view.setOnDragListener { v, event - when (event.action) { DragEvent.ACTION_DROP - { val clipData event.clipData // 处理数据 } } true }场景二输入焦点管理多窗口共用输入法时容易出问题。建议在onWindowFocusChanged里更新输入状态使用ViewTreeObserver.OnWindowFocusChangeListener监听焦点变化场景三资源释放当应用变为非焦点窗口时override fun onTopResumedActivityChanged(topResumed: Boolean) { if (!topResumed) { releaseCamera() } else { reacquireCamera() } }6. 测试与调试技巧多窗口适配离不开充分测试我总结的测试矩阵包括基础测试分屏模式切换自由窗口大小调整画中画模式切换组合测试# 通过ADB命令测试 adb shell settings put global enable_freeform_support 1 adb shell am start -n com.example/.MainActivity --windowingMode 6自动化测试RunWith(AndroidJUnit4::class) class MultiWindowTest { Test fun testSplitScreen() { val activityScenario launchActivityMainActivity() activityScenario.onActivity { activity - activity.enterMultiWindowMode() assertTrue(activity.isInMultiWindowMode()) } } }调试时推荐使用adb shell dumpsys window windows | grep -E mCurrentFocus|mBounds7. 厂商定制化问题处理各厂商对多窗口的实现有差异需要特殊处理小米自由窗口if (Build.MANUFACTURER.equals(Xiaomi, ignoreCase true)) { window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) }华为分屏// 检测华为EMUI try { Class? hwBuildEx Class.forName(com.huawei.android.os.BuildEx); Method getUDID hwBuildEx.getMethod(getUDID); String udid (String) getUDID.invoke(hwBuildEx); } catch (Exception e) { // 非华为设备 }三星DeX模式meta-data android:namecom.samsung.android.keepalive.density android:valuetrue/8. 性能优化建议布局优化使用ViewStub延迟加载避免在onConfigurationChanged里inflate布局内存管理override fun onTrimMemory(level: Int) { when (level) { TRIM_MEMORY_UI_HIDDEN - { // 释放UI资源 } } }线程调度val handlerThread HandlerThread(ResizeThread).apply { start() } val resizeHandler Handler(handlerThread.looper) fun handleResize() { resizeHandler.post { // 处理耗时调整逻辑 } }最后给个实用技巧在onCreate里加这段代码可以打印当前窗口模式window.decorView.viewTreeObserver.addOnDrawListener { Log.d(WindowMode, 当前模式${ when(window.attributes.windowingMode) { WINDOWING_MODE_FULLSCREEN - 全屏 WINDOWING_MODE_PINNED - 画中画 else - 其他模式 } }) }