在iOS端WKWebView视频画中画的自定义拖拽场景中,边界弹性碰撞的恢复系数并不是一个可以简单设成固定值的参数。它直接决定了悬浮窗被甩向屏幕边缘后回弹的幅度,也影响高速碰撞与低速碰撞之间明显的能量损失差异。要理解并控制这种差异,需要先回到碰撞恢复系数的物理定义。

一、恢复系数与碰撞速度的物理关系
恢复系数 e 定义为碰撞后沿接触面法线方向的分离速度与碰撞前接近速度之比。完全弹性碰撞的恢复系数为1,动能没有损失;完全非弹性碰撞的恢复系数为0,两个物体会粘在一起。对于可拖拽的悬浮窗,边界碰撞更接近介于两者之间的非完全弹性碰撞,恢复系数通常小于1。能量损失可以由如下关系表示:ΔE = (1/2) μ v_n² (1 - e²),其中 v_n 是碰撞前法向相对速度,μ 是约化质量。
从这个公式可以看出,即使恢复系数保持固定,能量损失的绝对值也会随碰撞速度的平方增长。低速碰撞时损失的动能相对温和,而高速碰撞时损失会急剧放大。更接近真实物理的情况是,恢复系数本身也会随速度升高而下降。低速下视图与边界的接触变形基本处于弹性区间,局部阻尼和塑性变形很少,恢复系数可以维持在0.7甚至更高;高速下接触时间更短,材料内部的粘性阻尼和局部永久变形占比上升,更多动能被转化成不可恢复的内能,恢复系数可能降到0.3以下。实际工程测量中,金属、橡胶、复合材料的恢复系数都表现出类似的速度相关性。对iOS悬浮窗而言,虽然没有真实材料塑性,但物理引擎的连续碰撞计算同样可以通过动态恢复系数来模拟这种速度相关趋势。
因此,如果只是把UIDynamicItemBehavior的elasticity设为一个固定值,无论碰撞速度多高,能量损失比例都保持不变,高速和低速碰撞只会表现出相同的回弹比例。但用户在实际拖拽画中画窗口时,能明显感受到高速甩出后的“吸边”感和低速推动时的柔和回弹,这正是固定恢复系数无法还原的细节。
二、在UIKit Dynamics中捕获碰撞速度并调整恢复系数
系统提供的WKWebView视频画中画使用AVPictureInPictureController管理,窗口拖拽和边界回弹完全由系统控制,开发者无法修改恢复系数。要获得可控的弹性碰撞效果,通常需要关闭系统画中画,自行创建可拖拽的悬浮容器,将WKWebView的视频画面通过AVPlayerLayer或截图方式放入容器,再用UIKit Dynamics管理拖拽与边界碰撞。UIDynamicAnimator负责物理动画,UICollisionBehavior定义边界,UIDynamicItemBehavior的elasticity属性即为恢复系数。
UIKit Dynamics没有直接提供“碰撞前速度”回调,但可以通过UIDynamicItemBehavior的action闭包在每一帧读取item的线性速度。具体思路是:记录最近若干帧的速度;当一个视图的边界与参照边界距离小于一定阈值,并且速度方向指向边界时,判定为即将碰撞,此时根据速度大小更新elasticity;碰撞后可以恢复默认值。这样就能实现速度相关的恢复系数。
import UIKit
class PiPBounceView: UIView {
private var animator: UIDynamicAnimator!
private var collision: UICollisionBehavior!
private var itemBehavior: UIDynamicItemBehavior!
private let targetView = UIView()
override func layoutSubviews() {
super.layoutSubviews()
if animator == nil {
setupDynamics()
}
}
private func setupDynamics() {
animator = UIDynamicAnimator(referenceView: self)
targetView.frame = CGRect(x: 100, y: 100, width: 160, height: 90)
targetView.backgroundColor = .systemBlue
addSubview(targetView)
collision = UICollisionBehavior(items: [targetView])
collision.translatesReferenceBoundsIntoBoundary = true
animator.addBehavior(collision)
itemBehavior = UIDynamicItemBehavior(items: [targetView])
itemBehavior.elasticity = 0.75
itemBehavior.resistance = 0.0
itemBehavior.friction = 0.0
itemBehavior.allowsRotation = false
animator.addBehavior(itemBehavior)
itemBehavior.action = { [weak self] in
self?.adjustElasticityForVelocity()
}
}
private func adjustElasticityForVelocity() {
let velocity = itemBehavior.linearVelocity(for: targetView)
let location = targetView.center
let bounds = self.bounds.insetBy(dx: targetView.bounds.width / 2,
dy: targetView.bounds.height / 2)
let margin: CGFloat = 12.0
let nearLeft = location.x - bounds.minX
let nearRight = bounds.maxX - location.x
let nearTop = location.y - bounds.minY
let nearBottom = bounds.maxY - location.y
let willHitLeft = nearLeft < margin && velocity.x < 0
let willHitRight = nearRight < margin && velocity.x > 0
let willHitTop = nearTop < margin && velocity.y < 0
let willHitBottom = nearBottom < margin && velocity.y > 0
if willHitLeft || willHitRight || willHitTop || willHitBottom {
let speed = sqrt(velocity.x * velocity.x + velocity.y * velocity.y)
if speed > 1200 {
itemBehavior.elasticity = 0.35
} else if speed > 600 {
itemBehavior.elasticity = 0.55
} else {
itemBehavior.elasticity = 0.75
}
} else {
itemBehavior.elasticity = 0.75
}
}
}
这段代码在边界附近检测视图速度方向,并根据合速度大小分三段设置elasticity。高速碰撞用较低恢复系数模拟更大能量损失,低速碰撞保持较高恢复系数,让回弹更明显。需要注意的是,action闭包在每一帧物理步进时都会执行,频繁修改elasticity可能导致接触阶段数值抖动,因此最好配合速度阈值和边界距离余量使用。
三、高速与低速碰撞能量损失差异及分段策略
根据能量损失公式,如果恢复系数按上述分段设置,低速(例如速度小于600pt/s)弹性为0.75,能量损失比例是1减去0.75的平方,约为43.75%;中速弹性为0.55,损失比例约69.75%;高速弹性为0.35,损失比例高达87.75%。可以看到高速时不仅速度项变大,恢复系数下降进一步放大了损失。绝对能量损失在高速下会是低速的数十倍,这也是悬浮窗高速甩出碰撞边界后几乎没有回弹、而缓慢拖动时能有柔和弹性的原因。
| 碰撞速度区间 | 恢复系数取值 | 能量损失比例 | 手感描述 |
|---|---|---|---|
| 低速(小于600pt/s) | 0.75 | 约43.75% | 回弹明显,边界柔和 |
| 中速(600-1200pt/s) | 0.55 | 约69.75% | 回弹减弱,有缓冲感 |
| 高速(大于1200pt/s) | 0.35 | 约87.75% | 接近吸边,几乎不回弹 |
实现动态恢复系数时,如果目标视图带有手势拖拽,需要区分手势当前手指速度与物理引擎速度。通常使用UIPanGestureRecognizer的velocity(in:)映射到UIDynamicItemBehavior的addLinearVelocity,手势结束后由物理引擎接管,此时action中读到的速度才用于碰撞前恢复系数调整。如果在手势拖拽过程中也修改elasticity,会让手指拖动和物理回弹混在一起,边界表现不稳定。建议在pan gesture结束后短暂延迟几帧再启用动态elasticity逻辑,或者用状态标记区分拖拽中与碰撞前。
回到WKWebView集成场景,若使用系统画中画,用户把画中画窗口拖到屏幕边缘时,系统已经有内置回弹行为,且该回弹恢复系数无法通过公开API修改。此时自定义碰撞策略只能用在自建悬浮窗上:可以监听WKWebView的视频进入画中画事件,但更好的做法是在网页端通过JavaScript与原生协商,把视频元素隐藏,原生用AVPlayerViewController或AVPlayerLayer承接播放,再对承载视图施加上面的动力学。这样既保留画中画浮层拖拽体验,又能精细控制高速与低速碰撞的不同能量损失。
iOS WKWebView 画中画恢复系数碰撞能量损失修改时间:2026-10-02 21:28:12