Android 14自由窗口全攻略:任意APK窗口化启动与调节

发布时间:2026/10/8 9:11:35
Android 14自由窗口全攻略:任意APK窗口化启动与调节
我从一个很实际的场景说起。手里那台Android 14平板装了个老版本APKUI只写了竖屏全屏但我在工作流里必须把它当成一个“可随时拖走、可缩放”的窗口旁边还要放资料和浏览器。系统自带的自由窗口入口在Android 14的开发者选项里基本找不到网上搜到的资料又大多停留在Android 7、Android 10的年代命令抄过来直接失效。于是我把Android 14原生机制、全局开关、ActivityOptions、ADB启动参数翻了个底朝天最终达成了目标任意第三方APK都能以自由窗口模式启动位置、尺寸都能精确控制。这篇文章就是把这套折腾过程完整记录下来适合在Android 14平板上折腾自由窗口的朋友也适合做AOSP定制、桌面化改造的开发者。我会把命令、原理、排查链路全部展开不藏私。1. 需求起点为什么要去折腾“自由窗口化启动APk”1.1 自由窗口不是分屏更不是厂商悬浮窗“自由窗口”在Android里是一个特定的窗口管理模式。它的英文是Freeform属于多窗口体系里自由度最高的一种形态窗口可以随意移动、边缘缩放、层叠摆放窗口和窗口之间互相独立更像PC桌面上的窗口体验。它和分屏不同分屏是固定的左右或上下两块尺寸调节被限制在屏幕一半左右它和厂商自己做的“小窗”“悬浮窗”也不是一回事那些方案普遍基于WindowManager的Overlay机制输出的是固定大小的悬浮视图不具备系统级多窗口生命周期。在Android 14上Freeform由WindowManager的WindowingMode驱动是一种真正的Task形态。窗口模式下Activity有自己的Bounds、自己的尺寸、自己的堆叠层级。正因如此你要“自由窗口化启动一个APK”本质上是在启动那一刻告诉系统请把目标Activity放进WindowingMode为Freeform的容器里并给它一个Bounds矩形区域。1.2 Android 14让这个需求变复杂的几个版本差异Android 14的多窗口体系和老版本差别非常大。早先Android 7、8时代开发者选项里还能看到“自由窗口”这类隐藏入口不少教程就是让用户去开发者选项勾选一个开关。从Android 10之后Google逐步收紧桌面形态的窗口逻辑Android 12L开始对大屏设备做了专门的适配但普通手机ROM里自由窗口相关入口基本被隐藏只剩下“强制活动可调整大小”这一类半隐形开关。到了Android 14更大的坑在于就算你把设备连上电脑用老的资料里那几行settings put命令也未必能直接生效。有些命令在AOSP里有对应实现但厂商定制ROM可能裁剪了Freeform Display的初始化逻辑有些命令被系统监听但需要同时满足两个开关条件还有些命令需要在重启之后才被SystemServer读取。我自己踩过的坑就包括只开了enable_freeform_support完全没管force_resizable_activities结果am start --windowingMode 5启动出来的窗口依然是全屏连窗口装饰都没出现。所以这篇文章不只是给命令更重要的是把“这些命令为什么存在”“少了哪一环会失败”讲清楚。受益的人分三类普通用户想手动把某个APK拖成窗口开发者想在代码里用ActivityOptions启动其他应用以及做ROM定制的人想从源码层把自由窗口能力固化为系统默认行为。2. 先把系统后台“放行”两个全局开关是最容易漏的一步2.1 开发者选项里找不到入口直接用adb改设置Android 14上如果你用的是AOSP类系统比如Pixel或者接近原生的平板开发者选项里真的没有“自由窗口”这个选项。但底层代码是支持Freeform的只是默认不开放。这里需要的两个关键设置项如下adb shell settings put global enable_freeform_support 1 adb shell settings put global force_resizable_activities 1第一行里的enable_freeform_support对应WindowManagerService里的Freeform支持开关它决定系统允不允许创建Freeform模式的窗口。第二行里的force_resizable_activities对应开发者选项里的“Force activities to be resizable”它决定系统会不会无视应用Manifest里的android:resizeableActivityfalse强制把Activity改成可调整大小。这两个开关缺一不可。只开第一个系统允许Freeform但目标应用的Activity声明了不可resize窗口管理器在创建Task时会被Manifest限制拽回全屏只开第二个应用变得可resize但WindowManager层面不允许Freeform Display--windowingMode 5传进去也会被忽略。写入后建议顺手检查一遍adb shell settings get global enable_freeform_support adb shell settings get global force_resizable_activities返回结果都应该是1。如果设备ROM比较激进修改后可能需要重启一次才能让WindowManager重新读取配置。实测部分设备不重启也能生效但个别厂商系统里SystemServer在启动阶段缓存了设置不重启会出现“设置已经写进去但窗口模式始终不对”的诡异现象。2.2 全局开启强制resizable之后的副作用这里要提前打预防针force_resizable_activities是一个全局开关它会影响系统里所有应用不只是你想窗口化那个。一旦全局开启很多应用会暴露在原本不适配的窗口尺寸下导致布局错乱、元素重叠、甚至直接闪退。我遇到过最典型的例子是某相机应用。它在Manifest里写了resizeableActivityfalse并预设了竖屏全屏的UI计算逻辑。强制resizable之后Freeform窗口倒出来了但界面里的相机预览Surface尺寸计算炸了最终黑屏。反过来看有些视频播放器和阅读器倒是自适应得很好窗口化之后反而更方便。所以我的建议是日常只针对目标APK做临时改动用完之后把force_resizable_activities设回0。如果你确定某个应用本身是可resize的只是系统没开Freeform那只需要第一个开关就够。千万别图省事让这个全局开关长期挂着它可能让你手机上所有应用都出现奇怪问题。2.3 自己编译AOSP还要多改一层config如果你是自己编译Android 14固件或者手上有可修改的ROM源码上面两个全局设置之外还有两个更底层的配置项值得关注frameworks/base/core/res/res/values/config.xmlbool nameconfig_freeformWindowSupporttrue/bool bool nameconfig_resizeWindowsForFreeformtrue/boolconfig_freeformWindowSupport决定此设备是否支持Freeform窗口模式config_resizeWindowsForFreeform则允许系统把所有可resize的窗口都纳入Freeform容器。把这两个值改成true编译进去自由窗口会变成系统级默认能力之后用ADB命令时不需要再依赖全局设置项直接启动即可。但注意这个改动影响面很大。普通应用开发者在调试时用还好如果要做量产ROM必须额外考虑应用兼容性以及设备屏幕尺寸。手机上强行开启Freeform多数应用并不会因为支持了自由窗口就变得好用反而可能在型号适配、状态栏交互上翻车。3. 把APK真正拉进自由窗口核心命令与参数拆解3.1 先查包名和默认入口Activity要把一个APK“自由窗口化启动”首先得拿到它的包名和默认入口Activity。最稳妥的方法是使用系统自带的cmd package命令。USB连接电脑开ADB后执行adb shell cmd package resolve-activity --brief com.some.app | tail -n 1假设包名是com.some.app返回结果一般是com.some.app/.MainActivity这个结果可以直接用在后面的-n参数里。如果手头没有现成包名也可以先用pm list packages配合关键词过滤或者用dumpsys package翻看AndroidManifest里的launcher activity。还有一条简单粗暴的路径用Monkey启动默认入口adb shell monkey -p com.some.app -c android.intent.category.LAUNCHER 1不过Monkey的用途主要是做压力测试它会把事件发给目标Activity但不保证你能拿到准确的组件名。拿组件名的场景下resolve-activity明显更干净结果还能直接喂给am start。3.2 am start 一行命令完成自由窗口化启动拿到组件名之后真正的核心命令长这样adb shell am start \ --windowingMode 5 \ --bounds 200,200,1200,900 \ -n com.some.app/.MainActivity--windowingMode 5表示把目标Activity放入Freeform窗口模式。这个数字不是拍脑袋定的它来自Android源码里WindowConfiguration的常量定义WINDOWING_MODE_FREEFORM 5。网上有些老文章写的是2那是Pinned模式对应的是画中画或者悬停窗口不是Freeform别用混。--bounds参数控制窗口的初始矩形区域格式是left,top,right,bottom单位是物理像素。例如200,200,1200,900表示窗口左上角在屏幕坐标(200,200)右下角在(1200,900)实际宽度为1000px高度为700px。窗口管理器会优先按这个Bounds创建窗口。启动命令可以在已有应用正在运行时执行但为了干净复现建议先adb shell am force-stop com.some.app再执行启动。否则应用可能保留之前的Task状态导致windowingMode传递被忽略。3.3 在代码里用ActivityOptions实现同样的效果如果你不是用ADB而是想写一个小工具APK通过代码启动其他应用并指定自由窗口一样能做。Android提供了ActivityOptions代码如下Context context getApplicationContext(); Intent intent context.getPackageManager().getLaunchIntentForPackage(com.some.app); if (intent null) return; intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); Rect bounds new Rect(200, 200, 1200, 900); ActivityOptions options ActivityOptions.makeBasic(); options.setLaunchWindowingMode(ActivityOptions.WINDOWING_MODE_FREEFORM); options.setLaunchBounds(bounds); context.startActivity(intent, options.toBundle());Kotlin写法几乎一样val intent packageManager.getLaunchIntentForPackage(com.some.app) if (intent ! null) { intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) val options ActivityOptions.makeBasic().apply { setLaunchWindowingMode(ActivityOptions.WINDOWING_MODE_FREEFORM) setLaunchBounds(Rect(200, 200, 1200, 900)) } startActivity(intent, options.toBundle()) }注意这里用的是getLaunchIntentForPackage它返回的Intent已经带了正确的Category和Component。但有些APK在getLaunchIntentForPackage返回的Intent里会附带额外的Extras如果目标应用对Intent要求比较苛刻建议改用resolveActivity自己构造显式Intent避免启动到错误页面。还要强调一点代码方式同样受enable_freeform_support和force_resizable_activities影响。如果这两个开关没开setLaunchWindowingMode(5)传入Bundle后会被系统降级成全屏模式而且不会有任何报错提示。4. 常用启动脚本与参数组合让自由窗口真正好用4.1 写一个设备端sh脚本自动居中启动每次都用一长串adb shell am start命令挺烦的尤其要反复调节窗口位置时手敲坐标非常低效。我习惯把启动逻辑写成脚本直接在设备上的终端环境里执行。下面这个脚本可以在设备本地运行运行前把它放到/system/bin或者推送到内部存储后用sh调用。核心逻辑是读包名解析默认Activity读取屏幕物理尺寸把目标窗口居中放在屏幕偏上位置。#!/system/bin/sh # freeform launch script # usage: freeform.sh package [width] [height] PACKAGE$1 W${2:-900} H${3:-600} if [ -z $PACKAGE ]; then echo usage: freeform.sh package [width] [height] exit 1 fi COMPONENT$(cmd package resolve-activity --brief $PACKAGE | tail -n 1) if [ -z $COMPONENT ]; then echo 找不到可启动的Activity: $PACKAGE exit 1 fi SIZE$(wm size | sed s/.*Physical size: // | head -n 1) SCREEN_W${SIZE%x*} SCREEN_H${SIZE#*x} LEFT$(( (SCREEN_W - W) / 2 )) TOP$(( (SCREEN_H - H) / 3 )) [ $LEFT -lt 0 ] LEFT0 [ $TOP -lt 0 ] TOP0 BOUNDS$LEFT,$TOP,$((LEFT W)),$((TOP H)) echo start: $COMPONENT - $BOUNDS am start --windowingMode 5 --bounds $BOUNDS -n $COMPONENT脚本里的wm size | sed s/.*Physical size: //目的是把形如Physical size: 1600x2560的输出转换成1600x2560再做字符串切分拿到宽高。如果你设备的wm size输出还有Override size之类的额外行脚本可能取错可以直接写死宽高或者按实际输出稍作调整。4.2 横屏应用、竖屏应用的Bounds选择经验自由窗口的Bounds直接影响应用的界面布局这里有明显的经验成分。竖屏应用进入宽窗口时界面两侧通常会出现大块空白或者内容被强制拉伸。这时适合把窗口比例控制在接近应用原始屏幕比例。比如一个手机APP默认屏幕是1080x2340比例接近9:19.5那么放在平板上时窗口适合设置为720x1560左右而不是塞满整个平板宽度。否则布局权重会把按钮拉得奇形怪状。横屏应用相对友好一些。视频播放器、游戏模拟器、文档阅读器设成960x540或者1280x720这类常见横屏比例基本不会出大问题。还有一类应用会在Freeform窗口里用逻辑尺寸重新计算布局比如很多Compose写的应用缩放时反而比老式View系统更顺滑。一个实用技巧是先拿一个接近预期尺寸的窗口启动再拖动窗口边缘微调等找到舒服的尺寸后记住这个Bounds写进脚本固定下来。下次启动直接复现省去每次都要拖一遍。4.3 窗口尺寸使用的是物理像素不是dp--bounds里的数字单位是物理像素不是dp。多数人在这里会犯迷糊Windows上做开发习惯了逻辑像素到了Android里以为指定了900dp实际出来却是900px。在240dpi的屏幕上900px只相当于600dp左右在320dpi的平板上900px只有约450dp的可视宽度。这会导致窗口看起来比预期小一大截。想转dp的话公式是px dp * density。假设平板密度是2.0你想让窗口显示为900dp宽就需要传入1800px。具体密度可以用adb shell wm density查询。脚本和命令里的建议是直接用物理像素方便和wm size返回的屏幕宽高对应起来不容易算错。5. 自由窗口的翻车现场为什么有些APK依然全屏5.1 启动后仍然全屏的完整排查链路最烦人的问题不是命令报错而是命令成功执行了但窗口还是全屏。我遇到这种情况时一般按下面这条链路排查第一步确认两个全局设置还在adb shell settings get global enable_freeform_support adb shell settings get global force_resizable_activities第二步确认组件名正确。第三步看看当前窗口到底处于什么模式。用系统命令直接看窗口信息adb shell dumpsys window windows | grep -E mDisplayId|windowingMode|mBounds执行启动命令后看输出如果当前窗口的windowingMode显示为WINDOWING_MODE_FULLSCREEN而不是WINDOWING_MODE_FREEFORM说明Freeform容器没有被正确创建。这时优先怀疑目标Activity自身不可resize或者系统配置被厂商裁剪。还有一个常见原因是Activity已经在最近任务里存在启动命令只是把已有Task带到了前台。旧Task保持全屏模式新传入的--windowingMode 5被忽略。解决办法是先移除旧Taskadb shell am task remove taskId然后重新执行启动命令。5.2 强制resizable后应用闪退如何定位把force_resizable_activities打开之后原本声明resizeableActivityfalse的应用确实能被塞进自由窗口但代价是应用自己的代码可能没想过多尺寸支持。闪退是最常见的结局。这时候不要全局甩锅给自由窗口先通过日志确认崩溃原因adb logcat -d | grep -E FATAL EXCEPTION|AndroidRuntime常见崩溃集中在三类第一应用在onCreate里根据屏幕尺寸做图片解密或资源预加载尺寸变成了Freeform窗口小尺寸后计算逻辑除零或数组越界第二SurfaceView或TextureView在窗口尺寸变化时没有重新初始化第三应用内部有自绘弹窗或Toast依赖全屏坐标拿到的DecorView尺寸异常。定位到目标之后更好的做法是不要全局开着force_resizable_activities而是临时开启启动完目标应用后再关掉。因为已经创建的Freeform Task一般不会因为全局开关关闭而立刻消失这样可以最大限度减少对其他应用的干扰。如果目标应用依然在运行期间崩溃那基本可以判断该应用与Freeform模式不兼容再怎么调参意义也不大。5.3 输入法弹出、窗口焦点和触摸区域问题Freeform窗口下输入法是一个大坑。普通全屏应用弹出键盘时系统会通过adjustResize自动压缩窗口但在Freeform模式里键盘弹出后往往直接覆盖窗口底部输入框被挡住。Android的输入法窗口有自己的Window类型和坐标计算逻辑Freeform容器对它来说是新生事物很多系统没有自动避让。我的做法是先把自由窗口的上边缘拖到屏幕中上方给键盘预留空间然后点输入框让pan或resize策略决定窗口行为。实测大部分应用在窗口上方区域点击输入框时系统会把窗口整体上推输入可见性反而比全屏时更好。还有个容易忽略的细节是触摸区域。Freeform窗口周围会有标题栏和边框边框可拖拽缩放的区域比较窄手指比鼠标粗很多经常拖不动。此时可以借助ADB做精确缩放adb shell am start --windowingMode 5 --bounds 100,100,1500,1200 -n com.some.app/.MainActivity窗口尺寸调整后尽量用边缘中间位置拖动避免用角落。使用触控板或外接鼠标操作平板时这个体验问题会小得多。6. 自由窗口的天然限制和更务实的替代方案6.1 哪些应用永远无法真正自由窗口化有两类应用很难通过普通方式窗口化。一类是系统级安全策略锁定的应用。比如一些金融、支付类应用在Manifest里除了resizeableActivityfalse代码里还会检测窗口模式或屏幕尺寸变化一旦发现不是全屏直接提示“环境不安全”并退出。这是应用自身的安全设计不建议去绕过也不应该绕过。理论上force_resizable_activities可以把这类应用塞进Freeform但运行时它们照样可能拒绝服务。另一类是强依赖特定Camera、DRM、传感器方向的应用。摄像头预览Surface在窗口尺寸变化时如果不重新协商大概率黑屏受版权保护的视频内容在黑屏状态下也未必能正常渲染。这些限制不是系统层面的死路而是应用自身生命周期对多窗口支持不到位导致的。对你来说判断标准其实是应用能否在分屏模式下正常工作。如果它连系统自带的分屏都进不去自由窗口化的成功率也极低。6.2 平板上值得折腾手机上建议理智放弃自由窗口在屏幕足够大的设备上价值很高比如12寸以上的平板、可折叠内屏。大屏空间富余开三四个窗口互不遮挡非常实用。但在6寸出头的手机上自由窗口就是个鸡肋窗口太小点不准窗口大了又和全屏没区别。Android 14原生没有给手机提供好用的自由窗口入口厂商的“小窗模式”在手机上确实更顺手。那种模式只悬浮一个固定尺寸的卡片窗口输入、关闭、移动都非常快不需要考虑窗口堆叠和尺寸重排。所以如果你在手机上搜索这个需求先想想自己是不是真的需要Freeform还是仅仅想要一个“小窗”。6.3 更务实的三条路系统分屏、厂商小窗、柔性质Freeform如果你确认自己就想要类似PC的窗口体验又不想被各种应用兼容性问题折磨有三条更务实的路。第一条使用系统分屏。Android 14的分屏已经比较成熟很多普通应用在分屏里能正常使用稳定性比Freeform高得多。它的缺点是不能随意移动窗口位置但大多数工作流只需要两块区域并排分屏已经够用。第二条用厂商自带的小窗模式。比如一些国产ROM在侧边栏里提供“小窗应用”原理是Overlay窗口稳定性和手势交互都打磨过。虽然它不是Freeform但很多人要的其实就是“把应用变小挂起来”小窗完全满足。第三条使用代码层面的“伪自由窗口”。如果你要在自己开发的应用里嵌入其他应用可以用android:windowIsFloating做一个悬浮Activity或者自己构建一个以固定尺寸启动的外部Activity。这种方式本质是在你的应用进程内模拟窗口形态自由度不如系统Freeform但规避了大量系统配置和权限问题。我现在的做法是把几条ADB命令封装成脚本需要时执行不需要时保持系统默认干净状态。普通使用场景下我依然推荐优先体验系统分屏只有当工作流明确需要“三个以上的窗口自由摆放”时才打开Freeform这条野路子。Android 14本质上支持这套机制只是它被藏得很深知道原理之后一切都变得顺理成章。