当WKWebView里的视频进入画中画模式后,用户可以把悬浮小窗拖到iPad屏幕的任意角落。如果此时通过USB-C连接了外部显示器,并且系统允许将画中画窗口拖拽到扩展屏上,那么窗口的坐标就不再属于主屏的坐标空间。如果直接读取frame后就拿去做碰撞检测或边界限制,结果在扩展屏上出现几百点的偏移。要解决这个问题,先要把每个UIScreen的坐标关系弄清楚。

一、多显示器下PiP窗口的坐标归属
UIKit为每块物理屏幕创建独立的UIScreen对象,每个对象的bounds和scale都可能不同。主屏通常从0开始计算,但外接屏幕的坐标系统同样是相对自身左上角,而不是主屏的延伸。WKWebView中的视频画中画窗口由系统媒体播放器维护,并不属于应用自己的视图层级,因此我们在应用内无法直接拿到它的UIView实例。但通过AVPictureInPictureController的delegate回调,可以获知画中画开始和结束的状态,部分系统版本还能通过私有API或category方式获取到画中画容器视图。
常见的误区是把主屏坐标直接当作全局坐标来用,或者以为画中画窗口的frame一定基于主屏。实际上窗口在哪个屏幕,坐标就基于哪个屏幕。假如扩展屏分辨率是1920x1080,逻辑尺寸可能是960x540点,主屏逻辑尺寸是1024x768点,两者完全独立,无法直接比较。解决思路是统一到一个坐标空间,比如都转换到主屏坐标或目标屏幕的窗口坐标。
为了实现转换,需要先确定画中画窗口当前所在的UIScreen。可以通过窗口的window属性或者遍历UIScreen.screens来匹配。如果拿不到窗口引用,就只能依赖UIScreen的didConnectNotification和didDisconnectNotification来跟踪屏幕变化,同时通过用户拖拽行为无法精确获取位置,这时更多是处理布局适配而不是精确坐标映射。
二、用坐标转换API处理跨屏移动
UIKit提供了一系列基于UICoordinateSpace协议的转换方法,最常用的是convert(_:from:)和convert(_:to:)。对于两个不同屏幕的坐标转换,直接调用UIView的convert方法可能返回错误结果,因为源视图和目标视图不在同一个窗口层级。正确做法是把坐标先转换到屏幕坐标,再转换到另一个屏幕的窗口坐标。每个UIScreen对象本身遵循UICoordinateSpace,可以用它来转换。
下面是一段Swift示例,演示如何把画中画窗口在源屏幕上的frame转换到目标屏幕坐标系。这里假设pipView是画中画容器视图,sourceScreen和targetScreen是已知的UIScreen对象。
func convertPiPFrame(_ frame: CGRect,
from sourceScreen: UIScreen,
to targetScreen: UIScreen) -> CGRect {
// 将源屏幕坐标转换成主窗口坐标
let sourceWindow = sourceScreen.windows.first
let targetWindow = targetScreen.windows.first
guard let sourceWin = sourceWindow,
let targetWin = targetWindow else {
return frame
}
// 源屏幕上的点先转成源窗口坐标
let pointInSource = sourceWin.convert(frame.origin, from: nil)
// 再从源窗口坐标转到目标窗口坐标
let pointInTarget = targetWin.convert(pointInSource, from: sourceWin)
return CGRect(origin: pointInTarget, size: frame.size)
}
上面的代码只适合源窗口和目标窗口属于同一窗口层级的情况。画中画窗口的容器并不是普通的UIWindow,所以不能直接用这种方式。更可靠的方案是利用UIScreen的coordinateSpace做屏幕坐标间的转换。你可以把源屏幕坐标转换到目标屏幕的坐标空间,示例:
let sourcePoint = CGPoint(x: frame.midX, y: frame.midY) let targetPoint = targetScreen.coordinateSpace.convert(sourcePoint, from: sourceScreen.coordinateSpace)
这样的转换会正确处理不同scale带来的点与像素差异。需要注意的是,当屏幕正在经历模式切换时,coordinateSpace的映射可能是过渡值,最好在UIScreen.modeDidChangeNotification回调完成后再做计算。
实际开发中还会遇到画中画窗口跨屏后的旋转问题。如果扩展屏支持不同方向,窗口的frame宽高会跟随方向变化。建议在转换后重新基于目标屏幕的bounds进行边界约束,避免窗口超出可视区域。
三、监听屏幕热插拔与窗口位置恢复
外接显示器连接或断开时,系统会发送UIScreen.didConnectNotification和UIScreen.didDisconnectNotification。应用应该监听这些通知,及时更新内部维护的屏幕列表和坐标转换参考。尤其在画中画窗口从扩展屏被拖回主屏或者扩展屏断开时,窗口会自动回到主屏,但应用内的视频布局可能没有同步更新,导致控制条错位。
下面给出一个注册屏幕变化的示例:
NotificationCenter.default.addObserver(
forName: UIScreen.didConnectNotification,
object: nil,
queue: .main
) { notification in
guard let newScreen = notification.object as? UIScreen else {
return
}
// 更新屏幕引用,重新计算画中画坐标
self.handleScreenConnected(newScreen)
}
NotificationCenter.default.addObserver(
forName: UIScreen.didDisconnectNotification,
object: nil,
queue: .main
) { notification in
guard let removedScreen = notification.object as? UIScreen else {
return
}
self.handleScreenDisconnected(removedScreen)
}
在handleScreenConnected方法里,可以记录新屏幕的bounds和scale,并把之前保存的画中画窗口frame从旧坐标系转换到新坐标系。在handleScreenDisconnected方法里,则需要检查画中画窗口是否还在已断开的屏幕上,如果是,系统已经将它移回主屏,应用只需更新UI引用即可。
分辨率变化也需要关注。UIScreen.modeDidChangeNotification在屏幕模式切换时触发,比如用户调整了扩展屏的缩放比例。此时保存的逻辑坐标可能已经失真,应该重新计算。建议在存储坐标时使用归一化的比例,例如窗口中心点相对于屏幕宽高的比例,这样在分辨率变化时还能按比例还原位置。
四、与WKWebView集成的注意事项
WKWebView的画中画由系统媒体框架自动管理,开发者不能直接设置PiP窗口的初始位置。如果需要和网页内容联动,比如自定义控制条上的进度显示,可以通过JavaScript与原生层交换数据。当检测到画中画状态变化时,把转换后的窗口坐标回传给网页,网页端再更新浮层位置。不过这种跨域坐标系传递容易产生延迟,建议只传递状态变化而不是实时坐标。
另外要留意WKWebViewConfiguration中allowsPictureInPictureMediaPlayback属性的设置。如果为false,视频无法进入画中画模式,那么跨屏问题自然也不会出现。但大部分应用需要开启该能力。在多显示器场景下,画中画窗口只能由用户拖动,系统不提供API直接设置跨屏位置,因此我们讨论的坐标转换更多是用在UI适配和状态保存上。
总的来说,解决WKWebView画中画跨显示器坐标转换问题的关键是分清UIScreen的坐标空间,并合理使用convert方法。屏幕热插拔通知和模式切换通知是维护坐标映射的入口。即使无法直接控制画中画窗口的位置,通过这些手段也能避免造成布局错误和手势失效。