Unity 安卓项目适配 Freeform 小窗
一、确定你要的是哪种"小窗"
Android 上实现小窗有三种机制,容易混淆:
| 机制 | 触发者 | 关键配置 | API | 场景 |
|---|---|---|---|---|
| Freeform 多窗口 ✅ | 系统手势 | resizeableActivity="true" |
24+ | 微信飞书小窗、多任务 |
| PiP(画中画) ❌ | App 代码 | supportsPictureInPicture="true" |
26+ | 视频播放、地图导航 |
| Overlay 悬浮窗 ❌ | App 代码 | SYSTEM_ALERT_WINDOW 权限 |
23+ | 悬浮球、工具栏 |
我们要的是 Freeform —— 用户手势触发,系统自动缩放窗口,应用不需要写一行代码。
二、配置(只需两步)
2.1 AndroidManifest.xml
Assets/Plugins/Android/AndroidManifest.xml:
<activity android:name="com.unity3d.player.UnityPlayerActivity"
android:resizeableActivity="true"
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|uiMode|screenSize|smallestScreenSize|fontScale|layoutDirection|density" />
属性说明:
resizeableActivity="true"— 允许系统把应用缩放为 Freeform 小窗configChanges中必须包含screenLayout— 阻止切换小窗时 Activity 重建,避免 Unity 引擎重启- 禁止添加
supportsPictureInPicture="true"— 会与 Freeform 手势冲突,导致系统优先走 PiP
2.2 Unity ProjectSettings
在 Unity Editor 中:Edit → Project Settings → Player → Android → Resolution and Presentation → Resizable Window 勾选。
或直接编辑 ProjectSettings/ProjectSettings.asset:
androidResizableWindow: 1
这是 Unity 引擎层的独立开关。默认值为 0(关闭),即使 manifest 写对了,Unity 渲染层也会拒绝窗口缩放。这是此前手势不生效的根因。
三、Unity 渲染行为
3.1 3D 场景
Unity 使用 OpenGL ES / Vulkan 渲染到 Native SurfaceView。窗口缩放时:
Screen.width/Screen.height自动更新- Camera 按新的宽高比重新计算视口
- 3D 场景不会拉伸变形,只会改变可视范围
3.2 Canvas UI
UI Canvas 的适配取决于 Scaler 模式:
| Scaler 模式 | 小窗下的表现 |
|---|---|
| Scale With Screen Size ✅ | 自适应,推荐使用 |
| Constant Pixel Size | 小窗下 UI 可能过大溢出 |
| Constant Physical Size | 小窗下 UI 可能过大溢出 |
建议在 Canvas Scaler 中设置 Reference Resolution(如 1920×1080),Match 设为 0.5。
3.3 画面拉伸的常见原因
以下操作会导致小窗时画面变形或出黑边:
- ❌ 代码中调用
Screen.SetResolution()锁死分辨率 - ❌ 代码中硬编码
Camera.aspect为固定值 - ❌ Canvas Scaler 使用固定像素模式
正确做法: 不锁分辨率,不锁宽高比,Unity 会自动跟随窗口大小。
四、状态检测(可选)
4.1 为什么可选
Freeform 小窗由系统手势触发,不需要应用主动操作。状态检测的作用是锦上添花:
- 在小窗时暂停某些全屏特效
- 在小窗时调整 UI 字体大小
- 记录小窗状态用于数据分析
没有检测代码,手势照常生效。
4.2 Unity 中如何检测
#if UNITY_ANDROID && !UNITY_EDITOR
using (var player = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
using (var activity = player.GetStatic<AndroidJavaObject>("currentActivity"))
{
bool isMultiWindow = activity.Call<bool>("isInMultiWindowMode");
}
#endif
也可监控 Screen.width / Screen.height 变化作为兜底方案(所有品牌通用)。
4.3 isInMultiWindowMode() 各品牌差异
| 品牌 | 返回值 | 生命周期回调 |
|---|---|---|
| 华为 | ✅ true | onResume(不走 onConfigurationChanged) |
| 小米 | ✅ true | onConfigurationChanged |
| OPPO | ❌ 小窗返回 false | onConfigurationChanged |
| vivo | ✅ true | onConfigurationChanged |
| 三星 | ✅ true | onConfigurationChanged |
OPPO 的 bug 只能用窗口尺寸 vs 屏幕尺寸对比来判断。Unity 中直接监听 Screen 分辨率变化即可覆盖。
五、按品牌适配总结
| 适配点 | 需按品牌处理? | 说明 |
|---|---|---|
| 触发手势 | ❌ | 系统 ROM 负责,应用不参与 |
| manifest 配置 | ❌ | resizeableActivity="true" 全厂商通用 |
| Unity ProjectSettings | ❌ | androidResizableWindow: 1 一次配置 |
| 3D 渲染 | ❌ | Unity 自动处理 |
| Canvas UI | ❌ | 选对 Scaler 模式即可 |
| 多窗口状态检测 | ⚠️ | OPPO 的 isInMultiWindowMode() 小窗下返回 false,用分辨率兜底 |
| Android 原生 UI | ❌ | Unity 用 SurfaceView 渲染,不受 Dialog/PopupWindow/StatusBar 等 OEM 魔改影响 |
简单说:完全不需要按品牌分别写代码或配置。
六、避坑清单
| 坑 | 后果 | 正确做法 |
|---|---|---|
加了 supportsPictureInPicture="true" |
系统手势走 PiP 而非 Freeform | 不加此属性 |
androidResizableWindow 未开启 |
即使 manifest 正确,手势也不生效 | 设为 1 |
configChanges 缺少 screenLayout |
切小窗时 Activity 重建,Unity 引擎重启 | 补上此属性 |
代码里调用 Screen.SetResolution() |
小窗时画面变形或黑边 | 不锁定分辨率 |
| Canvas Scaler 用固定像素模式 | 小窗下 UI 溢出或太小 | 改用 Scale With Screen Size |

浙公网安备 33010602011771号