UIKit Dynamics 置身真实世界
![](https://img.haomeiwen.com/i1396454/67e9d6c71a9489ec.jpeg)
前言:
iOS的设计目标鼓励您创建数字接口(digital interface),对触摸,手势和方向的变化做出反应,就好像它们是物理对象而不仅仅是简单的像素集合。可以使用户可以通过皮肤深层的自身形态与界面更深层次的联系。
工具介绍:
- UIKit Dynamics是整合到UIKit中的完整物理引擎。它允许您通过添加重力,附件(弹簧)和力等行为来创建感觉真实的界面。您定义了您希望您的界面元素采用的物理特征,动力学引擎将照顾其余部分。
- Motion Effects使您可以创建炫酷视差效果。基本上,您可以利用手机加速度计提供的数据,以创建响应手机方向变化的界面。
一、着手
打开ViewController.swift,并将以下代码添加到下面的代码viewDidLoad:
let square = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
square.backgroundColor = UIColor.gray
view.addSubview(square)
二、加重力
animator = UIDynamicAnimator(referenceView: view)
gravity = UIGravityBehavior(items: [square])
animator.addBehavior(gravity)
- UIDynamicAnimator是UIKit物理引擎。该类跟踪您添加到引擎的各种行为,例如重力,并提供整体上下文。创建动画制作实例时,您将传递animator用于定义其坐标系的参考视图。
- UIGravityBehavior模拟重力的行为并在一个或多个项目上施加力,从而允许您建模物理交互。创建行为实例时,将其与一组项目(通常是视图)相关联。这样,您可以选择哪些项目受到行为的影响,在这种情况下,引力影响哪些项目。
大多数行为具有许多配置属性; 例如,重力行为允许您改变其角度和幅度。尝试修改这些属性,使您的对象以不同的加速度下降,侧面或对角线。
注意:单位上的一个简单单词:在物理世界中,重力(g)以米/秒表示,大约等于9.8 m/s2。使用牛顿第二定律,您可以用下列公式计算物体在重力影响下的距离:
distance = 0.5 × g × time2
在UIKit Dynamics中,公式是相同的,但单位是不同的。而不是米,您可以使用每秒成千上万个像素的单位。使用牛顿第二定律,您仍然可以根据您提供的重力组件随时确定您的view在何处。
三、设置边界
即使在屏幕底部消失后,它也会继续下降。为了将其保留在屏幕的边界内,您需要定义边界
var collision: UICollisionBehavior!
collision = UICollisionBehavior(items: [square])
collision.translatesReferenceBoundsIntoBoundary = true
animator.addBehavior(collision)
上述代码创建了一个碰撞行为,该行为定义了一个或多个相关联项目与之相关联的边界。
而不是明确添加边界坐标,上述代码将translatesReferenceBoundsIntoBoundary 属性设置为true。这导致边界提供给UIDynamicAnimator参考视图的边界。
四、处理碰撞
添加一个不可移动的障碍,下降的正方形将与之相冲突。
let barrier = UIView(frame: CGRect(x: 0, y: 300, width: 130, height: 20))
barrier.backgroundColor = UIColor.red
view.addSubview(barrier)
确实提供了一个重要的提醒:dynamics只影响与行为相关联的视图
大多数行为可以与多个项目相关联,并且每个项目可以与多个行为相关联
五、使对象响应碰撞
为了使square
与障碍物相撞,请找到初始化碰撞行为的行,并将其替换为以下内容:
collision = UICollisionBehavior(items: [square, barrier])
碰撞对象需要知道它应该与之相互作用的每个视图; 因此,将项目列表中的障碍添加到允许碰撞对象也可以作用在障碍物上。
效果如下:
![](https://img.haomeiwen.com/i1396454/3774eb1b2bd062d9.gif)
可以看出,square
跟障碍物交互不是很正确,障碍物应该不可移动,更奇怪的是障碍物从屏幕的底部反弹,并不像square
那样沉稳,因为重力行为与障碍物无关
六、隐形边界和碰撞
将碰撞行为初始化更改回最初
collision = UICollisionBehavior(items: [square])
在这一行之后,添加一下内容:(添加跟barrier
相同frame的boundary
)
collision.addBoundary(withIdentifier: "barrier" as NSCopying, for: UIBezierPath(rect: barrier.frame))
红色障碍物对用户仍旧可见,而对动力引擎(dynamics engine)不可见;相反边界(boundary)对动力引擎可见,对用户不可见
随着square
的下降,它似乎与barrier
相互作用,但它实际上是与不可动的boundary
相撞。
![](https://img.haomeiwen.com/i1396454/a94d5263eb78089b.gif)
下面将展示动态引擎如何与应用程序中的对象进行交互的一些细节。
七、在碰撞的背后
每个动态行为(dynamic behavior)都有个一个action属性,你可以在action属性中提供要在动画每一步执行的block,讲下列代码添加到viewDidLoad:
collision.action = {
print("\(NSStringFromCGAffineTransform(square.transform)) \(NSStringFromCGPoint(square.center))")
}
打印日志如下:(动力引擎对square
的frame的影响)
[1, 0, 0, 1, 0, 0] {150, 212}
[1, 0, 0, 1, 0, 0] {150, 218}
[1, 0, 0, 1, 0, 0] {150, 224}
[1, 0, 0, 1, 0, 0] {150, 231}
[1, 0, 0, 1, 0, 0] {150, 237}
[1, 0, 0, 1, 0, 0] {150, 244}
一旦方块击中障碍物,它就开始旋转,这样会产生如下的日志信息:
[0.9999181, 0.01279965, -0.01279965, 0.9999181, 0, 0] {150, 249}
[0.99820054, 0.059964005, -0.059964005, 0.99820054, 0, 0] {152, 249}
[0.9942596, 0.10699479, -0.10699479, 0.9942596, 0, 0] {154, 249}
[0.98810399, 0.15378727, -0.15378727, 0.98810399, 0, 0] {155, 249}
[0.97978747, 0.20004122, -0.20004122, 0.97978747, 0, 0] {157, 250}
[0.96930701, 0.24585338, -0.24585338, 0.96930701, 0, 0] {158, 251}
从打印日志,可以看到动态引擎正在使用变换(transform)和frame偏移(frame offset)的来改变view的position
如果在动画过程中,我们通过代码改变方块的frame和transform属性,物体属性会被我们重写,也就是说,动力学控制过程中,我们不应该通过transform来缩放物体等。
动力行为赋予的对象是term items而不是view。因此,拥有动力行为的对象需要遵守UIDynamicItem协议:
protocol UIDynamicItem : NSObjectProtocol {
var center: CGPoint { get set }
var bounds: CGRect { get }
var transform: CGAffineTransform { get set }
}
该UIDynamicItem协议给dynamics读写访问中心和转换属性(the center and transform properties),允许它基于其内部计算移动items。它还具有对边界的读取访问权限,它用于确定items的size,这样可以在items周边创建碰撞边界,并在施加力时计算物品的质量。
这个协议意味着动态不紧密耦合UIView; 确实有另一个UIKit类不是视图,但仍然采用这个协议:UICollectionViewLayoutAttributes
。这允许dynamics动画在集合视图中对items进行动画。
八、碰撞通知
添加UICollisionBehaviorDelegate
class ViewController: UIViewController, UICollisionBehaviorDelegate {
添加一个协议方法
func collisionBehavior(_ behavior: UICollisionBehavior, beganContactFor item: UIDynamicItem, withBoundaryIdentifier identifier: NSCopying?, at p: CGPoint) {
print("Boundary contact occurred - \(identifier)")
}
打印日志如下:
Boundary contact occurred - Optional(barrier)
Boundary contact occurred - Optional(barrier)
Boundary contact occurred - Optional(barrier)
Boundary contact occurred - Optional(barrier)
Boundary contact occurred - nil
Boundary contact occurred - nil
Boundary contact occurred - nil
Boundary contact occurred - nil
square
与带标识barrier
的边界相撞四次
为了方便看,我们改一下square
的背景颜色,每次撞击边界时,方形将闪烁黄色。在collisionBehavior
方法里面加以下代码
let collidingView = item as! UIView
collidingView.backgroundColor = UIColor.yellow
UIView.animate(withDuration: 0.3) {
collidingView.backgroundColor = UIColor.gray
}
到目前为止,UIKit Dynamics通过根据您的项目的界限进行计算,自动设置物品的物理属性(如质量和弹性)。接下来,您将看到如何通过使用UIDynamicItemBehavior
该类自己来控制这些物理属性。
九、配置item属性
上述代码创建一个item行为,将其与square
相关联,然后将该行为对象添加到动画制作器。弹性属性控制物品的柔软度; 值为1.0表示完全弹性的碰撞; 也就是说,碰撞中没有能量或速度损失。您将您的square
的弹性设置为0.6,这意味着每次弹跳时,平方将失去速度。
我们加以下代码,有线框,方便看
var updateCount = 0
collision.action = {
if (updateCount % 3 == 0) {
let outline = UIView(frame: square.bounds)
outline.transform = square.transform
outline.center = square.center
outline.alpha = 0.5
outline.backgroundColor = UIColor.clear
outline.layer.borderColor = square.layer.presentation()?.backgroundColor
outline.layer.borderWidth = 1.0
self.view.addSubview(outline)
}
updateCount += 1
}
![](https://img.haomeiwen.com/i1396454/e8bbe91237bd3fdc.gif)
在上面的代码中,只改变了项目的弹性; 但是,该项目的行为类具有可以在代码中操作的其他许多属性。它们如下:
- 弹性(elasticity) - 决定弹性的碰撞将如何,即项目在碰撞中的弹性或“橡皮”。
- 摩擦(friction) - 确定沿着表面滑动时的阻力运动量。
- 密度(density) - 当与尺寸结合时,这将给出物品的总体质量。质量越大,加速或减速物体越难。
- 电阻(resistance) - 确定任何线性运动的阻力量。这与仅适用于滑动 运动的摩擦相反。
- angularResistance - 确定任何旋转运动的阻力量。
- allowRotation - 这是一个有趣的,不建模任何现实世界的物理属性。将此属性设置为“否”,无论发生何种旋转力,对象都不会旋转。
十、动态添加行为
下面,介绍如何动态添加和删除行为。
首先添加一下属性
var firstContact = false
将以下代码添加到碰撞委托方法(collisionBehavior
)的末尾
if (!firstContact) {
firstContact = true
let square = UIView(frame: CGRect(x: 30, y: 0, width: 100, height: 100))
square.backgroundColor = UIColor.gray
view.addSubview(square)
collision.addItem(square)
gravity.addItem(square)
let attach = UIAttachmentBehavior(item: collidingView, attachedTo:square)
animator.addBehavior(attach)
}
上述代码检测到barrier
和square
之间的初始接触,创建第二个square
并将其添加到碰撞和重力行为。
效果如下:
![](https://img.haomeiwen.com/i1396454/5fc81abb99c0dd2b.gif)
此外,您还可以设置 attachment 行为,以创建使用虚拟弹簧连接一对对象的效果。
用户交互
添加另一种类型的动态行为——UISnapBehavior,当用户点击时,UISnapBehavior 让对象以弹簧般动画效果跳到一个特定的位置
现在移除firstContact
属性以及在collisionBehavior()
添加它的的代码
在touchesEnded
方法中加入下面代码
override func touchesEnded(touches: NSSet, withEvent event: UIEvent) {
if (snap != nil) {
animator.removeBehavior(snap)
}
let touch = touches.anyObject() as UITouch
snap = UISnapBehavior(item: square, snapToPoint: touch.locationInView(view))
animator.addBehavior(snap)
}
这段代码很简单。首先,它检查是否存在现有的捕捉行为(snap behavior)并将其删除。然后创建一个新的捕捉行为,将square
对齐到用户触摸的位置,并将其添加到动画制作工具(animator)。
现在你可以随便点击屏幕,square
会跳到你点击的位置。
效果如下:
![](https://img.haomeiwen.com/i1396454/ef0420316f538156.gif)
下一篇UIKit Dynamics 的介绍
Dynamics 投掷效果