转载

iOS 9 by Tutorials 笔记(十一)

Chapter 11: UIKit Dynamics

iOS 9 更新了他的物理引擎箱,如增加了重力、磁性领域、非矩形碰撞,和一些额外的附属连接行为。本章的主要着眼点在这些新特性上:

Getting started

打开 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)   

iOS 9 by Tutorials 笔记(十一)

添加了两个矩形,一白、一橙

接下来创建 UIDynamicAnimator,该类负责管理所有的物理特性。也可以看做是一种媒介用来协调 dynamic itemssubviewsdynamic behaviorsiOS 物理引擎 。他提供了一个上下文用来计算将要渲染的动画。

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")   

Behaviors

下面来学习一下 UIDynamicBehavior 的七个子类

  • UIAttachmentBehavior : 两个 dynamic items 连接起来或一个 dynamic items 和一个锚点连接在一起
  • UICollisionBehavior : 描述两个 item 接触时产生的效果,可以用来开启 item 边界: translatesReferenceBoundsIntoBoundary 设为 ture
  • UIDynamicItemBehaviordynamic items 的一些物理特性
  • UIFieldBehavior :这个是 iOS 9 新加的,添加了很多 物理场行为 ,包含电场(electric)、磁场(magnetic)、拖拽(dragging)、漩涡(vortex)、辐射(radial)、线性重力(linear gravity)、速率(velocity)、噪声(noise)、涡流(turbulence)、弹簧场(SpringField)
  • UIGravityBehavior :模拟重力效果
  • UIPushBehavior :应用在 dynamic items 上的一种力
  • UISnapBehavior :将 dynamic items 移动到指定的位置伴随着弹性效果

最后你可以将上面七种行为混合组合使用,简单来说就是先创建一个 parentBehaviorUIDynamicBehavior ),然后创建上面七个子类中的几个,在通过父类的 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 模式,就是下面展示的效果:

iOS 9 by Tutorials 笔记(十一)

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) } 

Applying dynamics to a real app

之前我们都在 playground 里玩,现在我们应用在一个 Real App 上,下面是一个照片浏览应用,主界面 photos 是简单的 collectionView,点进去是一张大图和关于图片的细节信息

iOS 9 by Tutorials 笔记(十一)

我们首先来将目光焦聚在右边大图照片中显示图片细节信息的部分,即照片拍摄的时间、尺寸、和名称。这些信息都显示在一个灰黑色半透明的圆角矩形中。我们来对这个圆角矩形应用一个自定义的 Sticky behavior ,即使其变成可拖动的,但无论放置在屏幕的哪个位置,该圆角矩形都会自动慢慢停靠在最上边或最下边的边界上。

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 ,我们在该类中设置了 UIDynamicItemBehaviorUICollisionBehaviorUIDynamicItem 以及两个弹簧场 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")   

Full photo with a thud

现在回到 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 的下边界还要偏下一点

-EOF-
正文到此结束
Loading...