谈谈iOS中的原生物理引擎——UIDynamic的应用
谈谈iOS中的原生物理引擎------UIDynamic的应用
UIDynamic是iOS中UIKit框架提供的接口,其用来为UI元素增加符合物理世界运动规则的动画行为。简单来说,UIDynamic提供的实际上是一个物理引擎,由于它是iOS原生系统支持的(iOS 7以上),因此兼容性和易用性非常好,使用它开发者可以非常方便的创建出物理动画。本篇文章,我们将讨论UIDynamic的设计架构、使用方法以及做一些简单的物理动画示例,希望可以在应用开发中为你带来一些启发。
从一个碰撞动画示例开始
在开始具体完整的介绍UIDynamic相关功能和接口前,我们可以先通过一个示例来体验下UIDynamic强大的功能以及开发流程。
假如我们要实现这样一个动画效果:
模拟一个台球游戏,首先在窗口中显示一个矩形区域作为球桌,其中放置一个台球元素,给其一个初始的速度和方向来模拟发球动作,之后台球将按照预设的物理规律在桌面上进行碰撞运动。
先来看一下效果实例:
实现上面效果的核心代码如下:
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协议,此协议的定义如下:
@MainActor public protocol UIDynamicItem : NSObjectProtocol { // 物理上元素的中心 var center: CGPoint { get set } // 物理上元素的bounds var bounds: CGRect { get } // transform var transform: CGAffineTransform { get set } // 边界类型 @available(iOS 9.0, *) optional var collisionBoundsType: UIDynamicItemCollisionBoundsType { get } // 边界Path @available(iOS 9.0, *) optional var collisionBoundingPath: UIBezierPath { get } }
UIKit框架中的UIView类默认实现了UIDynamicItem协议,因此所有UIView的子类都可以直接无缝使用UIDynamic提供的物理能力。通过子类对collisionBoundsType属性,可以对物理元素的边界进行细化的定制:
@available(iOS 9.0, *) public enum UIDynamicItemCollisionBoundsType : UInt, @unchecked Sendable { // 矩形边界(bounds来控制) case rectangle = 0 // 椭圆边界(有width和height来控制椭圆的两个轴) case ellipse = 1 // 路径边界(自定义边界) case path = 2 }
定义动画元素的属性:UIDynamicItemBehavior
UIDynamicItemBehavior本身也是Behavior的那种,和其他物理行为不同的是,UIDynamicItemBehavior侧重于定义动画元素本身的属性。UIDynamicItemBehavior的定义会影响元素本身运动过程中的阻力、弹力、惯性等行为。解析如下:
@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 } // 弹性设置,0到1之间,值越大元素的弹性越大 open var elasticity: CGFloat // 摩擦力设置,0表示无摩擦力,当两个物理元素接触滑动时,此值会有影响 open var friction: CGFloat // 0 being no friction between objects slide along each other // 密度设置 默认为1,密度大的元素加速和减速都能加困难 open var density: CGFloat // 阻尼设置,控制物体运动的阻力 open var resistance: CGFloat // 旋转阻尼设置,控制物理旋转的阻力 open var angularResistance: CGFloat // 0: no angular velocity damping // 电荷设置,决定电磁感应的程度 @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的主要作用是将动画元素和物力行为进行结合,驱动出仿真的物理动画。此类定义如下:
@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协议可以监听物理动画的状态:
@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是所有物理行为的基类,其中定义了一些公共的方法和属性:
@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?) // nil when being removed from an animator // 关联的仿真器 open var dynamicAnimator: UIDynamicAnimator? { get } }
依附行为:UIAttachmentBehavior
简单理解,依附行为就像将元素与锚点间连接上了一条线,效果如下:
示例代码如下:
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提供了多种初始化的方式,可以使用一个点作为锚点,也可以将另一个视图作为锚点:
// 以一个点作为锚点进行依附 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还有很多可配置的属性,如下:
@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 // 1: critical damping // 依附行为的震荡频率(绳子的弹性) open var frequency: CGFloat // in Hertz // 旋转阻力 @available(iOS 9.0, *) open var frictionTorque: CGFloat // default is 0.0 // 运动范围 @available(iOS 9.0, *) open var attachmentRange: UIFloatRange // default is UIFloatRangeInfinite }
AttachmentType枚举定义了是以点为锚点还是元素为锚点进行依附:
@available(iOS 7.0, *) public enum AttachmentType : Int, @unchecked Sendable { case items = 0 case anchor = 1 }
碰撞行为:UICollisionBehavior
碰撞行为比较好理解,在本文也是以一个台球碰撞示例开始。UICollisionBehavior解析如下:
@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可以指定碰撞的模式:
@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代理中定义了碰撞过程的回调:
@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中提供了一些静态方法能方便的创建不同的场模型:
// 创建一个拉力场行为(进入场后减速物理的运动) 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
通过上面的静态方法可以直接创建出复杂的场效果,并且场可以叠加进行使用。下面分别演示了拉力场,弹力场的行为运动效果,这些行为本身都是符合具体的物理公式,可以通过参数调整来仿真所需要的场景。
拉力场示例:
// 拉力场 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) }
可以看到,当物理元素位于拉力场范围内时,物体下落速度非常慢,脱离场影响范围后,在重力作用下,快速下落。
弹力场示例:
// 弹力场 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创建的场可以通过设置参数来控制场中的力、方向等属性,如下:
@available(iOS 9.0, *) @MainActor open class UIFieldBehavior : UIDynamicBehavior { // 场的位置 open var position: CGPoint // 场的作用范围 open var region: UIRegion // 场的力大小,默认为1 open var strength: CGFloat // 定义随着离场中心距离的增加,场强度减弱的速度,默认0 表示场是均匀的,不会减弱 open var falloff: CGFloat // 设置场中心的半径最小值,小于此值时,场作用不会进行衰减 open var minimumRadius: CGFloat // 指定速度场和线性方向的重力场的速度方向 open var direction: CGVector // 设置噪声场合湍流场的噪声强度 0为最强 1为最弱 open var smoothness: CGFloat // 噪声场合湍流场的动画速度 open var animationSpeed: CGFloat }
重力行为:UIGravityBehavior
UIGravityBehavior与UIFieldBehavior中的重力场功能有重复,这是由于UIGravityBehavior是iOS7之后就已经存在的行为,UIFieldBehavior是iOS9后为了增强对物理场模型的支持新增的,对应也覆盖了重力场的场景。UIGravityBehavior比较简单,解析如下:
@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的模式有两种,分别可以添加瞬时推力与持续推力。
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
捕获行为与弹簧行为类似,其描述运动随时间而衰减,逐渐将物理元素固定到某个点。
@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 // 设置震荡幅度 0-1之间 open var damping: CGFloat }
写在最后
物理引擎是许多游戏开发中的必备,使用物理引擎也可以为应用增加许多有趣的交互。另外,UIKit提供的物理引擎有着很好的性能,且可以和UIView无缝使用。最后,对本篇文章的任何讨论,都欢迎留言交流。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
叮!云原生虚拟数仓 PieCloudDB Database 动态包裹已送达
第一部分 PieCloudDB Database 最新动态 支持动态配置查询簇 PieCloudDB 最新内核版本 v2.14.0 新增动态配置查询簇功能。PieCloudDB 动态配置查询簇功能实现可伸缩的并行化查询,可提升单个查询并行使用底层资源的能力,同时加快查询响应速度。 动态配置查询簇的功能主要适用于单个 SELECT 查询的场景,可通过 pdb_parrallel_factor 参数动态配置查询簇的大小。 参考命令:SET pdb_parallel_factor to int_value; 新增定时任务功能 PieCloudDB 可视化云原生平台新增定时任务功能。定时任务支持在预定的时间点或时间间隔内自动执行任务,可应用在定期执行后台任务、生成报告、清理垃圾数据,更新数据和执行自动化操作等场景,为数据库管理员和开发人员提供了进一步的便捷。 本功能目前仅限企业版 新增数据加载功能 PieCloudDB 云原生平台新增数据加载功能。数据加载支持从不同的数据库抽取数据并加载到 PieCloudDB,并基于云原生平台实现流程的可视化管理和监控。用户只需要在平台上配置数据源、数据流和...
- 下一篇
Apache Doris 2.0.12 版本正式发布
亲爱的社区小伙伴们,Apache Doris 2.0.12 版本已于 2024 年 6 月 27 日正式与大家见面,该版本提交了 99 个改进项以及问题修复,欢迎大家下载体验。 官网下载: https://doris.apache.org/download/ GitHub 下载: https://github.com/apache/doris/releases 行为变更 不再将建表的默认注释设置为表的类型,而是改成默认为空,比如 COMMENT 'OLAP' 变成 COMMENT '',这样对于依赖注释的BI软件更加友好。 #35855 将 @@autocommit 变量的类型从 BOOLEAN 改成 BIGINT,以免有些 MySQL 客户端(比如.NET MySQL.Data)报错。 #33282 改进和优化 删除 disable_nested_complex_type 参数,默认允许创建嵌套的 ARRAY MAP STRUCT 类型。#36255 HMS Catalog 支持 SHOW CREATE DATABASE 命令 。 #28145 在 Query Profile 中增加...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS8安装Docker,最新的服务器搭配容器使用
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- Windows10,CentOS7,CentOS8安装Nodejs环境
- SpringBoot2全家桶,快速入门学习开发网站教程
- CentOS8编译安装MySQL8.0.19