iOS 9 更新了他的物理引擎箱,如增加了重力、磁性领域、非矩形碰撞,和一些额外的附属连接行为。本章的主要着眼点在这些新特性上:
打开 playground,添加下面内容:
import UIKit import XCPlayground let view = UIView(frame: CGRect(x: 0, y: 0, width: 600, height: 600)) view.backgroundColor = UIColor.lightTextColor() XCPShowView("Main View", view: view) let whiteSquare = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100)) whiteSquare.backgroundColor = UIColor.whiteColor() view.addSubview(whiteSquare) let orangeSquare = UIView(frame: CGRect(x: 400, y: 100, width: 100, height: 100)) orangeSquare.backgroundColor = UIColor.orangeColor() view.addSubview(orangeSquare)
添加了两个矩形,一白、一橙
接下来创建 UIDynamicAnimator,该类负责管理所有的物理特性。也可以看做是一种媒介用来协调 dynamic items
、 subviews
、 dynamic behaviors
、 iOS 物理引擎
。他提供了一个上下文用来计算将要渲染的动画。
let animator = UIDynamicAnimator(referenceView: view)
Dynamic behaviors封装了某些特定的物理效果,如重力,引力,弹跳效果。Dynamic animators 在动画过程中负责追踪这些 items,你之前传递进去的 referenceView 可以看做是一张施展动画的画布,因此所有要动画的 views 必须是 referenceView 的子类
给橙色矩形加一个自由落体效果
animator.addBehavior(UIGravityBehavior(items: [orangeSquare]))
一运行橙色矩形就掉下去了(出了屏幕),现在来加个底,让他落在底边上
let boundaryCollision = UICollisionBehavior(items: [whiteSquare, orangeSquare]) boundaryCollision.translatesReferenceBoundsIntoBoundary = true animator.addBehavior(boundaryCollision)
默认的所有 dynamic items 都会有一组行为集合来描述他的重量、下落速度、如何应对碰撞和其他一些物理特性。而这些都是由 UIDynamicItemBehavior
来负责描述
我们来设置一下橙色矩形的碰撞效果
let bounce = UIDynamicItemBehavior(items: [orangeSquare]) bounce.elasticity = 0.6 bounce.density = 200 bounce.resistance = 2 animator.addBehavior(bounce)
一个 dynamic item
的密度( density
)和尺寸决定了他的重量,弹力( Elasticity
)决定了碰撞后的弹跳效果(默认为 0 ),阻力( Resistance
)也就是摩擦力,可以让 dynamic item
在线性运动时停下来
为了更好的观察,开启 debug 模式,现在你能在橙色矩形外看到一个蓝色矩形框(表示碰撞时的边界)
animator.setValue(true, forKey: "debugEnabled")
下面来学习一下 UIDynamicBehavior 的七个子类
dynamic items
连接起来或一个 dynamic items
和一个锚点连接在一起 translatesReferenceBoundsIntoBoundary
设为 ture dynamic items
的一些物理特性 dynamic items
上的一种力 dynamic items
移动到指定的位置伴随着弹性效果 最后你可以将上面七种行为混合组合使用,简单来说就是先创建一个 parentBehavior ( UIDynamicBehavior
),然后创建上面七个子类中的几个,在通过父类的 addChildBehavior 方法添加到 parentBehavior 中去
来实际例子中玩一下,先给白色矩形加点物理属性:
let parentBehavior = UIDynamicBehavior() let viewBehavior = UIDynamicItemBehavior(items: [whiteSquare]) viewBehavior.density = 0.01 viewBehavior.resistance = 10 viewBehavior.friction = 0.0 viewBehavior.allowsRotation = false parentBehavior.addChildBehavior(viewBehavior)
再定义一个弹簧场范围,白色矩形刚好位于其中:
let fieldBehavior = UIFieldBehavior.springField() fieldBehavior.addItem(whiteSquare) fieldBehavior.position = CGPoint(x: 150, y: 350) fieldBehavior.region = UIRegion(size: CGSizeMake(500, 500)) parentBehavior.addChildBehavior(fieldBehavior)
运行
animator.addBehavior(parentBehavior)
开启了 debug 模式,就是下面展示的效果:
Spring fields弹簧场可以这么理解,你确定当中某个物体的位置,然后对该物体施加一个力,物体也许会偏离一点点,不过最终会稳定在那一点上,就像被栓了跟弹簧。看得不明显?施加一个向上的力来看看:
let delayTime = dispatch_time(DISPATCH_TIME_NOW, Int64(2 * Double(NSEC_PER_SEC))) dispatch_after(delayTime, dispatch_get_main_queue()) { let pushBehavior = UIPushBehavior(items: [whiteSquare], mode: .Instantaneous) pushBehavior.pushDirection = CGVector(dx: 0, dy: -1) pushBehavior.magnitude = 0.3 animator.addBehavior(pushBehavior) }
之前我们都在 playground 里玩,现在我们应用在一个 Real App 上,下面是一个照片浏览应用,主界面 photos 是简单的 collectionView,点进去是一张大图和关于图片的细节信息
我们首先来将目光焦聚在右边大图照片中显示图片细节信息的部分,即照片拍摄的时间、尺寸、和名称。这些信息都显示在一个灰黑色半透明的圆角矩形中。我们来对这个圆角矩形应用一个自定义的 Sticky behavior ,即使其变成可拖动的,但无论放置在屏幕的哪个位置,该圆角矩形都会自动慢慢停靠在最上边或最下边的边界上。
import UIKit class StickyEdgesBehavior: UIDynamicBehavior { private var edgeInset: CGFloat private let itemBehavior: UIDynamicItemBehavior private let collisionBehavior: UICollisionBehavior private let item: UIDynamicItem private let fieldBehaviors = [ UIFieldBehavior.springField(), UIFieldBehavior.springField() ] init(item: UIDynamicItem, edgeInset: CGFloat) { self.item = item self.edgeInset = edgeInset collisionBehavior = UICollisionBehavior(items: [item]) collisionBehavior.translatesReferenceBoundsIntoBoundary = true itemBehavior = UIDynamicItemBehavior(items: [item]) itemBehavior.density = 0.01 itemBehavior.resistance = 20 itemBehavior.friction = 0.0 itemBehavior.allowsRotation = false super.init() addChildBehavior(collisionBehavior) addChildBehavior(itemBehavior) for fieldBehavior in fieldBehaviors { fieldBehavior.addItem(item) addChildBehavior(fieldBehavior) } } }
StickyEdgesBehavior是 UIDynamicBehavior
的子类,在这里的作用相当于之前提到的 parentBehavior ,我们在该类中设置了 UIDynamicItemBehavior
、 UICollisionBehavior
、 UIDynamicItem
以及两个弹簧场 UIFieldBehavior.springField()
初始化的时候,我们传入了一个 item(通常是遵循 UIDynamicItem
协议的 View)和一个距边界尺寸(edge inset)
func updateFieldsInBounds(bounds: CGRect) { //1 确保 bounds 尺寸非零,且提取了长和宽 guard bounds != CGRect.zero else { return } let h = bounds.height let w = bounds.width let itemHeight = item.bounds.height //2 更新 field 的中心位置和区域(size) func updateRegionForField(field: UIFieldBehavior, _ point: CGPoint) { let size = CGSize(width: w - 2 * edgeInset, height: h - 2 * edgeInset - itemHeight) field.position = point field.region = UIRegion(size: size) } //3 找出 bounds 上半部分和下半部分的中心点 let top = CGPoint(x: w / 2, y: edgeInset + itemHeight / 2) let bottom = CGPoint(x: w / 2, y: h - edgeInset - itemHeight / 2) //4 更新 fieldBehaviors 中的 UIFieldBehavior.springField() updateRegionForField(fieldBehaviors[StickyEdge.Top.rawValue], top) updateRegionForField( fieldBehaviors[StickyEdge.Bottom.rawValue], bottom) } }
上面这个方法传入一个 bounds 作为参数,描述了整个弹簧场(UIFieldBehavior.springField())的范围。在方法内部,又将 bounds 一分为二:划分了上半部分和下半部分两个弹簧场(UIFieldBehavior.springField())
接下来添加一个计算属性 isEnabled
,用来在动画过程中关闭 behavior
var isEnabled = true { didSet { if isEnabled { for fieldBehavior in fieldBehaviors { fieldBehavior.addItem(item) } collisionBehavior.addItem(item) itemBehavior.addItem(item) } else { for fieldBehavior in fieldBehaviors { fieldBehavior.removeItem(item) } collisionBehavior.removeItem(item) itemBehavior.removeItem(item) } } }
最后为 item 增加一个线性速度
func addLinearVelocity(velocity: CGPoint) { itemBehavior.addLinearVelocity(velocity, forItem: item) }
现在 StickyEdgesBehavior 已经定义完毕,我们来使用他。具体方法:回到 FullPhotoViewController.swift ,即展示大图的 VC 为 照片细节部分 tagView (黑色矩形框)添加一个手势
添加下面的一些属性:
private var animator: UIDynamicAnimator! var stickyBehavior: StickyEdgesBehavior! private var offset = CGPoint.zero
在 viewDidload()
中做些初始配置:
let gestureRecognizer = UIPanGestureRecognizer(target: self, action: "pan:") tagView.addGestureRecognizer(gestureRecognizer) animator = UIDynamicAnimator(referenceView: containerView) stickyBehavior = StickyEdgesBehavior(item: tagView, edgeInset: 8) animator.addBehavior(stickyBehavior)
为 tagView 上面添加了一个手势,stickyBehavior 也加在了 tagView 上。接着添加 layoutSubviews
方法,当 main view 的 layout 发生改变时,sticky behavior 会自动调整 bounds(例如旋转发生时)
override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() stickyBehavior.isEnabled = false stickyBehavior.updateFieldsInBounds(containerView.bounds) }
最后来实现 pan:
手势
func pan(pan:UIPanGestureRecognizer) { var location = pan.locationInView(containerView) switch pan.state { case .Began: let center = tagView.center offset.x = location.x - center.x offset.y = location.y - center.y stickyBehavior.isEnabled = false case .Changed: let referenceBounds = containerView.bounds let referenceWidth = referenceBounds.width let referenceHeight = referenceBounds.height let itemBounds = tagView.bounds let itemHalfWidth = itemBounds.width / 2.0 let itemHalfHeight = itemBounds.height / 2.0 location.x -= offset.x location.y -= offset.y location.x = max(itemHalfWidth, location.x) location.x = min(referenceWidth - itemHalfWidth, location.x) location.y = max(itemHalfHeight, location.y) location.y = min(referenceHeight - itemHalfHeight, location.y) tagView.center = location case .Cancelled, .Ended: let velocity = pan.velocityInView(containerView) stickyBehavior.isEnabled = true stickyBehavior.addLinearVelocity(velocity) default: () } }
当 pan gesture 开始时, sticky behavior 将会被关掉,接着他会记录用户手势移动的偏移量 offset,在 .Changed case 中根据 offset 来实时更新 metadata view 的位置,并保证 metadata view 不会超出 container view 的范围。在手势结束或取消时,再开启 sticky behavior,此时你会发现 metadata view (tagView)会先判断在上下半场哪个半场,然后再根据上下弹簧场的各自特性(上面的向上运动,下面的向下运动)自行移动到相应位置。
最后在 viewDidload 里开启 debug 模式运行看一下:
animator.setValue(true, forKey: "debugEnabled")
现在回到 collectionView 上的照片集合界面,点击其中任意一张照片,一张大图从天匀速而降,让我们来加入点自由落体和弹性效果。
实现起来也很简单,在 PhotosCollectionViewController.swift 中增加一个 UIDynamicAnimator
,然后创建一些 UIDynamicBehavior
加进去
创建 UIDynamicAnimator
var animator: UIDynamicAnimator!
随后在 viewDidLoad()
中初始化
animator = UIDynamicAnimator(referenceView: self.view)
创建 UIGravityBehavior、UICollisionBehavior,以及为 item 增加点物理特性(UIDynamicItemBehavior)。最后将这些 behavior 统统添加到 animator 中来
func showFullImageView(index: Int) { //1 将 fullPhotoView 向上移出屏幕 fullPhotoViewController.photoPair = photoData[index] fullPhotoView.center = CGPoint(x: fullPhotoView.center.x, y: fullPhotoView.frame.height / -2) fullPhotoView.hidden = false //2 先清空 animator 再添加 behaviors animator.removeAllBehaviors() let dynamicItemBehavior = UIDynamicItemBehavior(items: [fullPhotoView]) dynamicItemBehavior.elasticity = 0.2 dynamicItemBehavior.density = 400 animator.addBehavior(dynamicItemBehavior) let gravityBehavior = UIGravityBehavior(items: [fullPhotoView]) gravityBehavior.magnitude = 5.0 animator.addBehavior(gravityBehavior) let collisionBehavior = UICollisionBehavior(items: [fullPhotoView]) let left = CGPoint(x: 0, y: fullPhotoView.frame.height + 1.5) let right = CGPoint(x: fullPhotoView.frame.width, y: fullPhotoView.frame.height + 1.5) collisionBehavior.addBoundaryWithIdentifier("bottom", fromPoint: left, toPoint: right) animator.addBehavior(collisionBehavior) //3 执行动画,动画完成时添加 barButton(Done) UIView.animateWithDuration(0.5, animations: { () -> Void in self.fullPhotoView.center = self.view.center }, completion: { (completed: Bool) -> Void in let doneButton = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.Done, target: self, action: "dismissFullPhoto:") self.navigationItem.rightBarButtonItem = doneButton }) }
注意我们在底部从 left 到 right 创建了一条线 collisionBehavior.addBoundaryWithIdentifier("bottom", fromPoint: left, toPoint: right)
这条线稍微比 view 的下边界还要偏下一点