谈谈iOS中的原生物理引擎——UIDynamic的应用 UIDynamic是iOS中UIKit框架提供的接口,其用来为UI元素增加符合物理世界运动规则的动画行为。简单来说,UIDynamic提供的实际上是一个物理引擎,由于它是iOS原生系统支持的(iOS 7以上),因此兼容性和易用性非常好,使用它开发者可以非常方便的创建出物理动画。本篇文章,我们将讨论UIDynamic的设计架构、使用方法以及做一些简单的物理动画示例,希望可以在应用开发中为你带来一些启发。
从一个碰撞动画示例开始 在开始具体完整的介绍UIDynamic相关功能和接口前,我们可以先通过一个示例来体验下UIDynamic强大的功能以及开发流程。
假如我们要实现这样一个动画效果:
模拟一个台球游戏,首先在窗口中显示一个矩形区域作为球桌,其中放置一个台球元素,给其一个初始的速度和方向来模拟发球动作,之后台球将按照预设的物理规律在桌面上进行碰撞运动。
先来看一下效果实例:
实现上面效果的核心代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 var animator: UIDynamicAnimator !func addCollision () { animator = UIDynamicAnimator (referenceView: box) let collision = UICollisionBehavior () collision.translatesReferenceBoundsIntoBoundary = true collision.addItem(ball) let push = UIPushBehavior (items: [ball], mode: .instantaneous) push.addItem(ball) push.pushDirection = .init (dx: 0.5 , dy: 0.5 ) push.magnitude = 5 let item = UIDynamicItemBehavior (items: [ball]) item.resistance = 1 item.elasticity = 0.8 animator.addBehavior(push) animator.addBehavior(collision) animator.addBehavior(item) }
可以看到,实现此物理动画的主要流程包括3个要素:
动画元素View
物理仿真器Animator
物理行为Behavior
上面示例代码中,添加了3种物理行为,UICollisionBehavior用来定义碰撞行为,可以定义要产生碰撞的元素。UIPushBehavior用来定义推动行为,可以给物理元素一个推力。UIDynamicItemBehavior用来定义物理元素本身的性质,例如摩擦力,质量等。下面我们会逐一讨论这些要素。
关于动画元素的定义 定义可动画元素:UIDynamicItem 任何物理行为都需要作用在某一个具体的UI元素上,要支持物理引擎的元素需要实现UIDynamicItem协议,此协议的定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @MainActor public protocol UIDynamicItem : NSObjectProtocol { var center: CGPoint { get set } var bounds: CGRect { get } var transform: CGAffineTransform { get set } @available (iOS 9.0 , *) optional var collisionBoundsType: UIDynamicItemCollisionBoundsType { get } @available (iOS 9.0 , *) optional var collisionBoundingPath: UIBezierPath { get } }
UIKit框架中的UIView类默认实现了UIDynamicItem协议,因此所有UIView的子类都可以直接无缝使用UIDynamic提供的物理能力。通过子类对collisionBoundsType属性,可以对物理元素的边界进行细化的定制:
1 2 3 4 5 6 7 8 9 @available(iOS 9.0 , *) public enum UIDynamicItemCollisionBoundsType : UInt , @unchecked Sendable { case rectangle = 0 case ellipse = 1 case path = 2 }
定义动画元素的属性:UIDynamicItemBehavior UIDynamicItemBehavior本身也是Behavior的那种,和其他物理行为不同的是,UIDynamicItemBehavior侧重于定义动画元素本身的属性。UIDynamicItemBehavior的定义会影响元素本身运动过程中的阻力、弹力、惯性等行为。解析如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 @available (iOS 7.0 , *)@MainActor open class UIDynamicItemBehavior : UIDynamicBehavior { public init (items: [any UIDynamicItem ]) open func addItem (_ item: any UIDynamicItem) open func removeItem (_ item: any UIDynamicItem) open var items: [any UIDynamicItem ] { get } open var elasticity: CGFloat open var friction: CGFloat open var density: CGFloat open var resistance: CGFloat open var angularResistance: CGFloat @available (iOS 9.0 , *) open var charge: CGFloat @available (iOS 9.0 , *) open var isAnchored: Bool open var allowsRotation: Bool open func addLinearVelocity (_ velocity: CGPoint, for item: any UIDynamicItem) open func linearVelocity (for item: any UIDynamicItem) -> CGPoint open func addAngularVelocity (_ velocity: CGFloat, for item: any UIDynamicItem) open func angularVelocity (for item: any UIDynamicItem) -> CGFloat }
物理仿真器 物理仿真器由UIDynamicAnimator类来描述。UIDynamicAnimator的主要作用是将动画元素和物力行为进行结合,驱动出仿真的物理动画。此类定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @available (iOS 7.0 , *)open class UIDynamicAnimator : NSObject { public init (referenceView view: UIView ) open func addBehavior (_ behavior: UIDynamicBehavior) open func removeBehavior (_ behavior: UIDynamicBehavior) open func removeAllBehaviors () open var referenceView: UIView ? { get } open var behaviors: [UIDynamicBehavior ] { get } open func items (in rect: CGRect) -> [any UIDynamicItem ] open func updateItem (usingCurrentState item: any UIDynamicItem) open var isRunning: Bool { get } open var elapsedTime: TimeInterval { get } weak open var delegate: (any UIDynamicAnimatorDelegate )? }
UIDynamicAnimatorDelegate协议可以监听物理动画的状态:
1 2 3 4 5 6 7 8 @MainActor public protocol UIDynamicAnimatorDelegate : NSObjectProtocol { @available (iOS 7.0 , *) optional func dynamicAnimatorWillResume (_ animator: UIDynamicAnimator) @available (iOS 7.0 , *) optional func dynamicAnimatorDidPause (_ animator: UIDynamicAnimator) }
物理行为定义 物理行为可以实现复杂的2D物理动画,我们可以单独使用这些物理行为,也可以将物理行为进行组合使用。
UIDynamicBehavior基类 UIDynamicBehavior是所有物理行为的基类,其中定义了一些公共的方法和属性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @available (iOS 7.0 , *)@MainActor open class UIDynamicBehavior : NSObject { open func addChildBehavior (_ behavior: UIDynamicBehavior) open func removeChildBehavior (_ behavior: UIDynamicBehavior) open var childBehaviors: [UIDynamicBehavior ] { get } open var action: (() -> Void )? open func willMove (to dynamicAnimator: UIDynamicAnimator?) open var dynamicAnimator: UIDynamicAnimator ? { get } }
依附行为:UIAttachmentBehavior 简单理解,依附行为就像将元素与锚点间连接上了一条线,效果如下:
示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func addAttachment () { animator = UIDynamicAnimator (referenceView: box) let gravity = UIGravityBehavior (items: [ball]) gravity.gravityDirection = .init (dx: 0 , dy: 1 ) let attachment = UIAttachmentBehavior (item: ball, attachedToAnchor: CGPoint (x: box.frame.width / 2 + 35 , y: box.frame.height / 2 )) attachment.length = 50 animator.addBehavior(gravity) animator.addBehavior(attachment) }
UIAttachmentBehavior提供了多种初始化的方式,可以使用一个点作为锚点,也可以将另一个视图作为锚点:
1 2 3 4 5 6 7 8 9 public convenience init (item: any UIDynamicItem , attachedToAnchor point: CGPoint )public init (item: any UIDynamicItem , offsetFromCenter offset: UIOffset , attachedToAnchor point: CGPoint )public convenience init (item item1: any UIDynamicItem , attachedTo item2: any UIDynamicItem )public init (item item1: any UIDynamicItem , offsetFromCenter offset1: UIOffset , attachedTo item2: any UIDynamicItem , offsetFromCenter offset2: UIOffset )
UIAttachmentBehavior还有很多可配置的属性,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @available (iOS 7.0 , *)@MainActor open class UIAttachmentBehavior : UIDynamicBehavior { open var items: [any UIDynamicItem ] { get } open var attachedBehaviorType: UIAttachmentBehavior .AttachmentType { get } open var anchorPoint: CGPoint open var length: CGFloat open var damping: CGFloat open var frequency: CGFloat @available (iOS 9.0 , *) open var frictionTorque: CGFloat @available (iOS 9.0 , *) open var attachmentRange: UIFloatRange }
AttachmentType枚举定义了是以点为锚点还是元素为锚点进行依附:
1 2 3 4 5 @available (iOS 7.0 , *)public enum AttachmentType : Int , @unchecked Sendable { case items = 0 case anchor = 1 }
碰撞行为:UICollisionBehavior
碰撞行为比较好理解,在本文也是以一个台球碰撞示例开始。UICollisionBehavior解析如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @available (iOS 7.0 , *)@MainActor open class UICollisionBehavior : UIDynamicBehavior { public init (items: [any UIDynamicItem ]) open func addItem (_ item: any UIDynamicItem) open func removeItem (_ item: any UIDynamicItem) open var items: [any UIDynamicItem ] { get } open var collisionMode: UICollisionBehavior .Mode open var translatesReferenceBoundsIntoBoundary: Bool open func setTranslatesReferenceBoundsIntoBoundary (with insets: UIEdgeInsets) open func addBoundary (withIdentifier identifier: any NSCopying, for bezierPath: UIBezierPath) open func addBoundary (withIdentifier identifier: any NSCopying, from p1: CGPoint, to p2: CGPoint) open func boundary (withIdentifier identifier: any NSCopying) -> UIBezierPath ? open func removeBoundary (withIdentifier identifier: any NSCopying) open var boundaryIdentifiers: [any NSCopying ]? { get } open func removeAllBoundaries () weak open var collisionDelegate: (any UICollisionBehaviorDelegate )? }
collisionMode可以指定碰撞的模式:
1 2 3 4 5 6 7 8 9 10 11 12 @available (iOS 7.0 , *)public struct Mode : OptionSet , @unchecked Sendable { public init (rawValue: UInt ) public static var items: UICollisionBehavior .Mode { get } public static var boundaries: UICollisionBehavior .Mode { get } public static var everything: UICollisionBehavior .Mode { get } }
UICollisionBehaviorDelegate代理中定义了碰撞过程的回调:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @MainActor public protocol UICollisionBehaviorDelegate : NSObjectProtocol { @available (iOS 7.0 , *) optional func collisionBehavior (_ behavior: UICollisionBehavior, beganContactFor item1: any UIDynamicItem, with item2: any UIDynamicItem, at p: CGPoint) @available (iOS 7.0 , *) optional func collisionBehavior (_ behavior: UICollisionBehavior, endedContactFor item1: any UIDynamicItem, with item2: any UIDynamicItem) @available (iOS 7.0 , *) optional func collisionBehavior (_ behavior: UICollisionBehavior, beganContactFor item: any UIDynamicItem, withBoundaryIdentifier identifier: (any NSCopying) ?, at p: CGPoint ) @available (iOS 7.0 , *) optional func collisionBehavior (_ behavior: UICollisionBehavior, endedContactFor item: any UIDynamicItem, withBoundaryIdentifier identifier: (any NSCopying) ?) }
场行为:UIFieldBehavior 场也是物理学中物理运动重要模型,生活中电场、磁场、重力场等场无处不在,iOS 9之后引入了UIFieldBehavior来仿真场行为。场行为本身运动规律复杂,UIFieldBehavior中提供了一些静态方法能方便的创建不同的场模型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 open class func dragField () -> Self // 创建一个弹力场行为(弹簧震荡的效果) open class func springField () -> Self // 加速度场 (场中的物理元素会被叠加上指定方向的加速度) open class func velocityField (direction : CGVector ) -> Self // 创建电场行为 (与物体所携带的电荷量有关) open class func electricField () -> Self // 创建磁场行为 (与物体所携带的电荷量有关) open class func magneticField () -> Self // 在指定的点创建重力场行为 (有质量的物体会被吸引,设置负值则排斥) open class func radialGravityField (position : CGPoint ) -> Self // 创建一个方向上的引力场行为 (与radialGravityField 不同的是此场的力会均匀作用在指定方向) open class func linearGravityField (direction : CGVector ) -> Self // 创建涡流场行为(场中附加的力是沿速度方向的切线) open class func vortexField () -> Self // 噪声场,此场通常与其他场结合使用,用来在纯粹的物理场中增加一些噪声,更好的模拟现实 open class func noiseField (smoothness : CGFloat , animationSpeed speed : CGFloat ) -> Self // 湍流场,也用于增加噪声,此场中的噪声与速度成正比 open class func turbulenceField (smoothness : CGFloat , animationSpeed speed : CGFloat ) -> Self // 完全自定义场行为 open class func field (evaluationBlock block : @escaping (UIFieldBehavior , CGPoint , CGVector , CGFloat , CGFloat , TimeInterval ) -> CGVector ) -> Self
通过上面的静态方法可以直接创建出复杂的场效果,并且场可以叠加进行使用。下面分别演示了拉力场,弹力场的行为运动效果,这些行为本身都是符合具体的物理公式,可以通过参数调整来仿真所需要的场景。
拉力场示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func addDragField () { animator = UIDynamicAnimator (referenceView: box) let gravity = UIGravityBehavior (items: [ball]) gravity.gravityDirection = .init (dx: 0 , dy: 1 ) let field = UIFieldBehavior .dragField() field.addItem(ball) field.position = CGPoint (x: ball.frame.origin.x, y: ball.frame.origin.y + 100 ) field.strength = 3 field.region = .init (size: .init (width: 100 , height: 100 )) animator.addBehavior(field) animator.addBehavior(gravity) }
可以看到,当物理元素位于拉力场范围内时,物体下落速度非常慢,脱离场影响范围后,在重力作用下,快速下落。
弹力场示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func addSpringField () { animator = UIDynamicAnimator (referenceView: box) let gravity = UIGravityBehavior (items: [ball]) gravity.gravityDirection = .init (dx: 0 , dy: 1 ) let field = UIFieldBehavior .springField() field.addItem(ball) field.position = CGPoint (x: ball.frame.origin.x, y: ball.frame.origin.y - 100 ) animator.addBehavior(field) animator.addBehavior(gravity) }
UIFieldBehavior创建的场可以通过设置参数来控制场中的力、方向等属性,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @available (iOS 9.0 , *)@MainActor open class UIFieldBehavior : UIDynamicBehavior { open var position: CGPoint open var region: UIRegion open var strength: CGFloat open var falloff: CGFloat open var minimumRadius: CGFloat open var direction: CGVector open var smoothness: CGFloat open var animationSpeed: CGFloat }
重力行为:UIGravityBehavior UIGravityBehavior与UIFieldBehavior中的重力场功能有重复,这是由于UIGravityBehavior是iOS7之后就已经存在的行为,UIFieldBehavior是iOS9后为了增强对物理场模型的支持新增的,对应也覆盖了重力场的场景。UIGravityBehavior比较简单,解析如下:
1 2 3 4 5 6 7 8 9 10 11 @available (iOS 7.0 , *)@MainActor open class UIGravityBehavior : UIDynamicBehavior { open var gravityDirection: CGVector open var angle: CGFloat open var magnitude: CGFloat open func setAngle (_ angle: CGFloat, magnitude: CGFloat) }
推动行为:UIPushBehavior UIPushBehavior用来仿真推动行为,其可以为物理元素提供一个推力。UIPushBehavior的模式有两种,分别可以添加瞬时推力与持续推力。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 extension UIPushBehavior { @available (iOS 7.0 , *) public enum Mode : Int , @unchecked Sendable { case continuous = 0 case instantaneous = 1 } } @available (iOS 7.0 , *)@MainActor open class UIPushBehavior : UIDynamicBehavior { public init (items: [any UIDynamicItem ], mode: UIPushBehavior .Mode ) open func addItem (_ item: any UIDynamicItem) open func removeItem (_ item: any UIDynamicItem) open var items: [any UIDynamicItem ] { get } open func targetOffsetFromCenter (for item: any UIDynamicItem) -> UIOffset open func setTargetOffsetFromCenter (_ o: UIOffset, for item: any UIDynamicItem) open var mode: UIPushBehavior .Mode { get } open var active: Bool open var angle: CGFloat open func setAngle (_ angle: CGFloat, magnitude: CGFloat) open var magnitude: CGFloat open var pushDirection: CGVector }
捕获行为:UISnapBehavior 捕获行为与弹簧行为类似,其描述运动随时间而衰减,逐渐将物理元素固定到某个点。
1 2 3 4 5 6 7 8 9 @available (iOS 7.0 , *)@MainActor open class UISnapBehavior : UIDynamicBehavior { public init (item: any UIDynamicItem , snapTo point: CGPoint ) @available (iOS 9.0 , *) open var snapPoint: CGPoint open var damping: CGFloat }
写在最后 物理引擎是许多游戏开发中的必备,使用物理引擎也可以为应用增加许多有趣的交互。另外,UIKit提供的物理引擎有着很好的性能,且可以和UIView无缝使用。最后,对本篇文章的任何讨论,都欢迎留言交流。