简介:面向Android 11及以上版本的应用开发者,这份分屏功能实现项目演示了如何通过系统API开启、关闭分屏并在多窗口间切换,适合有一定Android基础、想深入理解多任务机制的开发者学习。压缩包共514个文件,约13.91MB,以xml布局与资源配置、json数据、java源码及dex字节码为主,同时包含gradle构建脚本、jar依赖和可直接安装的apk,结构完整,便于从源码工程层面拆解实现思路。项目围绕startActivityInSplitScreenMode()启动分屏、ActivityManager检查设备兼容性与任务切换、onMultiWindowModeChanged()监听窗口变化等关键点展开,覆盖分屏生命周期的主要环节。目前已有896人学习这份资源。通过研究其布局文件、事件监听器和辅助工具类,可快速搭建自己的分屏Demo,也能在实际开发中迁移相关写法,解决多窗口适配和切换问题。 分屏这个功能,平时开发里很少会被当成一个独立模块来对待,但只要你的应用真的被用户拖进分屏模式,各种奇怪问题就会接二连三地冒出来。我最近基于 Android 11(API 30)整理了一套分屏适配 demo,把多窗口下的状态感知、布局切换、尺寸适配和常见坑都过了一遍,这篇博文就是这次实践的完整记录。如果你正打算给应用补上分屏支持,或者想搞清楚 Android 11 的多窗口 API 到底怎么用,可以重点看看后面的实操部分。
1. Android 11 分屏 demo 到底该实现什么
1.1 分屏的使用场景与系统行为
分屏(Split Screen)是 Android 多窗口体系里最常用的一种形态,核心价值就是让两个应用在同一个屏幕内同时可见、同时可操作。最常见的场景是边看视频边回消息、边查资料边记笔记、视频会议的同时打开文档。Android 11 在系统层面已经非常成熟,手势导航下的分屏体验也做了不少优化:从底部上滑进入最近任务,点应用图标,就能看到“分屏”入口。
demo 需要覆盖的并不是“让系统强制进入分屏”这种系统级能力,而是应用进入分屏之后,如何让自己活得体面。也就是说,这个 demo 要解决的是三个问题:第一,应用能否被系统允许进入分屏,这由 Manifest 和 targetSdk 决定;第二,应用能否感知自己已经处于分屏状态,这依赖isInMultiWindowMode这类 API;第三,应用能否在窗口尺寸剧烈变化后重新布局,这需要配合onMultiWindowModeChanged和WindowMetrics来做。
1.2 普通应用的权限边界:能响应,难强启
在动手写代码之前,先把权限边界说清楚,否则你可能会搜索到一堆看着很牛但实际上跑不起来的代码。
Android 系统确实有触发分屏的系统级 API,比如ActivityTaskManager里的某些方法,但这些接口基本都有平台签名或系统权限限制。普通应用通过公开 SDK 编译,根本没有办法直接调用并弹出一个系统分屏。就算用反射强行调用,大多数设备上也会因为没有权限而直接抛异常。真正靠谱的做法是:把应用自身的适配做好,然后由用户通过系统交互(最近任务长按图标选“分屏”)来触发。也有测试场景可以通过adb指令辅助进入分屏,但不同厂商 ROM 的支持程度差异很大,我在后面会单独讲。
所以这个 demo 的定位非常明确:它是一个“分屏适配 demo”,不是“系统分屏启动器”。
2. 分屏适配四个关键点:从声明到布局切换
2.1 声明 resizeableActivity:别让应用“拒绝”分屏
分屏适配的第一步,是在 Manifest 中明确告诉系统你的 Activity 允许调整窗口大小。属性就是android:resizeableActivity,它有两个值:true表示允许进入多窗口模式,false表示禁止。
需要注意,这个属性的默认值跟应用 targetSdk 有关。如果你的 targetSdk 大于等于 24,默认值是true;如果你的 targetSdk 低于 24,系统会认为应用没有做过多窗口适配,默认值是false。Android 11 上做 demo,我建议直接把 targetSdk 设到 30 或 31,然后显式声明:
<activity android:name=".MainActivity" android:resizeableActivity="true" android:exported="true" />如果你把resizeableActivity设成false,系统会强制把该 Activity 放到全屏窗口里运行,并自动调整尺寸以避免用户把应用拉进分屏后出现崩溃。这是兜底方案,不建议常规使用,因为它会让应用在多任务场景下直接“缺席”。Android 11 还允许通过<meta-data>或android:maxAspectRatio做一些更细的尺寸限制,但在分屏 demo 里没有太大必要。
2.2 用 isInMultiWindowMode 检测当前是否在分屏
分屏状态下,应用窗口的宽高比可能是 1:1、16:9,甚至会被拉成一条细长的竖条。布局要想做出正确的响应,第一步是准确判断自己是否处于多窗口模式。
Activity.isInMultiWindowMode()是 API 24 加入的,Android 11 上依然可用。它返回true时,表示当前 Activity 正与其他 Activity 共享屏幕。
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(binding.root) binding.tvMode.text = if (isInMultiWindowMode) "当前:分屏模式" else "当前:全屏模式" }这个 API 在很多场景下非常好用。比如全屏播放视频的页面,进入分屏后自动从横屏播放器切换成居中小窗;聊天列表进入分屏后,可以把列表的 item 间距压缩,让一屏能展示更多内容。需要注意的是,isInMultiWindowMode()只反映“当前是否有多个窗口”,它不能告诉你“我是在上边还是下边”,也无法告诉你分割线的具体位置。这些信息需要结合其他回调来获取。
2.3 在 onMultiWindowModeChanged 里做布局切换
当用户把应用拖入分屏、退出分屏,或者拖动分割线改变窗口大小时,系统会回调onMultiWindowModeChanged。Android 11 上有两个重载版本:一个只有布尔参数,另一个带Configuration参数。实际开发中建议重写带Configuration的版本,因为在新配置里可以直接拿到最新的屏幕宽高、密度等信息,避免再去手动查询。
override fun onMultiWindowModeChanged(isInMultiWindowMode: Boolean, newConfig: Configuration) { super.onMultiWindowModeChanged(isInMultiWindowMode, newConfig) binding.tvMode.text = if (isInMultiWindowMode) "当前:分屏模式" else "当前:全屏模式" swapLayoutForMode(isInMultiWindowMode) }这里有一个非常容易踩的坑:分屏变化不仅会回调onMultiWindowModeChanged,还可能会触发onConfigurationChanged,如果你的 Activity 声明了android:configChanges。不声明的话,甚至可能直接触发 Activity 重建。所以布局刷新逻辑一定要设计成幂等的,简单说就是同一个状态被回调两次,也不能产生重复绑定或闪烁的效果。我在后面的常见问题里会再展开。
2.4 用 WindowMetrics 拿到真实可用的窗口尺寸
Android 11 引入了一组新的窗口尺寸获取方式:WindowManager.getCurrentWindowMetrics()。它返回WindowMetrics对象,里面包含bounds和windowInsets,可以拿到当前窗口在考虑系统栏、刘海屏等遮挡之后的真实可用区域。
fun getAvailableBounds(): Rect { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { windowManager.currentWindowMetrics.bounds } else { val display = windowManager.defaultDisplay val point = Point() display.getRealSize(point) Rect(0, 0, point.x, point.y) } }在分屏场景下,窗口宽高和全屏时差距很大,用DisplayMetrics或getRealSize拿到的往往是整个屏幕的物理尺寸,容易导致布局计算出错。用WindowMetrics则是直接面向当前窗口,适配起来更准。官方也在逐步推动旧 API 的弃用,新项目建议从一开始就使用它。
3. 上手搭一个可分屏的 demo 工程
3.1 工程配置与目录
demo 不需要太复杂,我建了一个包含两个 Activity 的工程:MainActivity作为首页入口,SecondActivity作为第二个任务窗口。主界面放一个按钮和一个状态文本,SecondActivity 放一段占位内容。这样最直观地演示“两个窗口并存”时的交互和数据传递。
工程主要配置如下:
android { compileSdk 31 defaultConfig { applicationId "com.example.splitscreendemo" minSdk 26 targetSdk 30 versionCode 1 versionName "1.0" } }Manifest 中两个 Activity 都声明resizeableActivity="true",同时不设置固定的screenOrientation。这一步很关键,如果某个 Activity 锁定了横屏或竖屏,它在分屏模式下会被系统强制调整,甚至无法正常参与分屏。
src/main/java/com/example/splitscreendemo ├── MainActivity.kt └── SecondActivity.kt res/layout ├── activity_main.xml └── activity_second.xml3.2 MainActivity 状态感知核心代码
核心逻辑写在 MainActivity 里。这里我用了 Kotlin,和 Java 的逻辑完全一致,只看你习惯哪种。
class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) binding.tvMode.text = if (isInMultiWindowMode) "当前:分屏模式" else "当前:全屏模式" binding.btnOpenSecond.setOnClickListener { startActivity(Intent(this, SecondActivity::class.java)) } } override fun onMultiWindowModeChanged(isInMultiWindowMode: Boolean, newConfig: Configuration) { super.onMultiWindowModeChanged(isInMultiWindowMode, newConfig) binding.tvMode.text = if (isInMultiWindowMode) "当前:分屏模式" else "当前:全屏模式" // 这里根据模式切换布局约束,比如全屏用列表,分屏用网格 val targetLayout = if (isInMultiWindowMode) { R.layout.activity_main_split } else { R.layout.activity_main } // 简单方式:直接重新 inflate 或使用 ConstraintLayout 的约束变化 if (binding.root.tag != targetLayout) { Log.d("SplitDemo", "switch to ${if (isInMultiWindowMode) "split" else "full"} layout") binding.root.tag = targetLayout } } }这里我故意没有写完整的布局切换代码,因为不同项目布局差异太大。实际开发中建议用ConstraintLayout配合百分比约束,或者用两套layout目录,而不是直接换根布局,否则页面上已有的状态数据会比较难保留。
3.3 手动验证步骤:从最近任务进入分屏
工程跑起来之后,验证步骤如下。
- 打开
MainActivity,确认界面完整显示,点击按钮能跳转到SecondActivity,再回到 MainActivity。 - 从屏幕底部上滑进入最近任务列表。
- 找到 MainActivity 的卡片,点击右上角或顶部的应用图标,在弹出的菜单中选择“分屏”。
- 屏幕会自动分成上下两个区域,下方会出现最近任务列表,选择 SecondActivity。
- 此时 MainActivity 和 SecondActivity 会同时显示在屏幕上下两侧,
tvMode文本应从“全屏模式”变成“分屏模式”。
拖动中间分割线,观察应用是否出现明显的卡顿、退出、布局错乱。整个过程中,logcat 里SplitDemo标签下会输出布局切换日志,可以用来确认回调是否正常触发。
4. 触发分屏的几种姿势与边界
4.1 用户手势触发
普通用户触发分屏最标准的手势是:从屏幕底部上滑进入最近任务,点击应用图标,选择“分屏”。部分 Android 11 设备还支持在最近任务里长按卡片直接拖到屏幕顶部或底部来快速分屏。这套交互是系统提供的,应用侧无需任何处理,只需保证自己可调整大小。
如果你的应用没有声明resizeableActivity="true",那么在最近任务的菜单里可能不会出现“分屏”选项,或者即便选择了分屏,系统也会强制把它整屏显示。因此,验证 demo 之前,一定要先检查 Manifest 配置。
4.2 adb 与测试机上的辅助手段
做自动化测试时,靠手势点击效率太低。Android 官方在adb中提供了一些窗口管理命令,可以帮助开发者更快进入多窗口状态。最常见的做法是先在应用内启动两个 Activity,再用am task相关命令调整窗口模式。
比如下面这种形式:
adb shell am task split-screen但我要特别提醒:这类命令在不同版本、不同厂商 ROM 上的可用性差异极大,有的需要 shell 权限,有的甚至需要 root。它更适合作为测试工程师在调试版设备上使用的辅助工具,不能写在正式产品逻辑里。更稳妥的自动化方案是使用UiAutomator模拟用户点击最近任务中的分屏按钮,虽然慢一点,但更接近真实行为。
4.3 ActivityOptions.setLaunchBounds 的局限
很多人搜索“Android 分屏实现”时会看到ActivityOptions.setLaunchBounds(Rect),以为这是启动分屏的公开 API,其实它只是设置新 Activity 的初始边界,并不是系统分屏模式的开关。
它的作用范围是:在支持多窗口的设备上,让新启动的 Activity 按你给定的Rect来决定窗口大小。你可以在代码里这样写:
val bounds = Rect(0, 0, displayWidth / 2, displayHeight) val options = ActivityOptions.makeBasic() options.setLaunchBounds(bounds) startActivity(intent, options.toBundle())但这段代码通常只会在支持自由窗口(freeform)的系统或特定设备上产生类似小窗的效果。在普通手机上,系统是否真正采用你给的 bounds,取决于设备本身的窗口策略,而且它并不会触发基于系统任务的分屏模式。所以我的结论是:setLaunchBounds可以作为窗口自定义大小的参考 API,但不要指望它代替分屏功能。
5. 分屏适配中的常见问题与避坑记录
| 问题表现 | 可能原因 | 解决办法 |
|---|---|---|
| 进入分屏后 Activity 重新创建,页面状态丢失 | 未处理配置变更,系统重建 Activity | 使用 ViewModel 保存业务数据,或用savedInstanceState恢复轻量状态 |
| 分屏后布局挤压,控件重叠 | 使用固定宽高或依赖全屏尺寸计算 | 改用WindowMetrics读取当前窗口真实尺寸,配合ConstraintLayout的相对约束 |
| onMultiWindowModeChanged 触发多次,界面闪烁 | 布局刷新逻辑没有做幂等保护 | 在回调里增加状态判断,比如记录当前模式标签,只有状态变化时才刷新 |
| 软键盘弹出后遮挡输入框 | 窗口 mode 变化后软键盘策略未调整 | 在 Manifest 中设置windowSoftInputMode=adjustResize,并在 onConfigurationChanged 中重新计算面板高度 |
| 分屏状态退出后,页面没有恢复全屏布局 | 只处理了进入分屏回退,没处理退出回退 | 保证布局切换在两个方向都生效,isInMultiWindowMode=false时也要刷新 |
| 视频播放页在分屏下黑屏或闪退 | Surface 和 TextureView 在窗口重建时生命周期没有处理好 | 在 onStop 中释放播放器资源,在 onStart 中重新初始化,同时避免在分屏切换瞬间操作 Surface |
再补充几个我在这次 demo 中实际踩到的经验。
一是不要在onMultiWindowModeChanged里做重量级操作,比如重新setContentView或重建整个 Fragment。分屏变化过程中,系统对性能会比较敏感,重量级操作容易导致掉帧甚至 ANR。我的习惯是只做轻量换肤或局部 View 可见性切换,真正复杂的状态放到ViewModel里统一管理。
二是分屏状态下onResume的触发规则和全屏时不同。两个应用同时可见时,只有获得焦点的那个会收到onResume,另一个停留在onPause但不会onStop。很多人在分屏下写“离开界面就暂停播放”的逻辑,结果发现用户一边看视频一边聊天时,视频并没有暂停,这就是因为在分屏模式下另一个 Activity 仍处于可见状态。判断是否需要暂停,不能只看onPause,最好结合isInMultiWindowMode来决定。
三是如果应用里有全屏性质的功能,比如相机预览、沉浸式阅读器、游戏画面,进入分屏后一定要做功能降级,而不是强行保持原来的交互。分屏窗口高度只有全屏一半时,很多按钮的位置需要重新设计,否则影响日常使用。
最后再分享一个我个人的做法:分屏适配不要等到测试阶段再做,而是在开发每个新页面时就把isInMultiWindowMode的布局逻辑写进去。成本不算高,但能提前暴露很多尺寸适配问题。遇到拿不准的视觉细节,直接用分屏模式跑一遍,比任何纸上讨论都有效。
本文还有配套的精品资源,点击获取