翻译自raywenderlich网站iOS教程Graphics & Animation系列
准备开始
首先用storyboard布局一个页面(或者你可以用纯代码去设置),效果如下:
在文件中声明和拖拽出如下参数:(blueSquare、redSquare、imgView是storyboard拖出的)
// x:80 y: 420 width: 8 height: 8 bgColor:blue
@IBOutlet weak var blueSquare: UIView!
// x:156 y: 219 width: 8 height: 8 bgColor:red
@IBOutlet weak var redSquare: UIView!
// x:33 y: 137 width: 254 height: 172
@IBOutlet weak var imgView: UIImageView!
private var originalBounds = CGRect.zero
private var originalCenter = CGPoint.zero
private var animator: UIDynamicAnimator!
private var attachmentBehavior: UIAttachmentBehavior!
private var pushBehavior: UIPushBehavior!
private var itemBehaviod: UIDynamicItemBehavior!
红色和蓝色方块表示让图片做动画的UIKit动态物理引擎点:蓝色方块表示触摸开始的位置,红色方块会在手指移动时跟踪。
现在给view添加一个手势识别器:在DynamicsTossingVC.swift
添加如下代码:
@IBAction func handleAttachmentGesture(_ sender: UIPanGestureRecognizer) {
let location = sender.location(in: self.view)
let boxLocation = sender.location(in: self.imgView)
switch sender.state {
case .began:
print("Touch start position is \(location)")
print("Start location in image is \(boxLocation)")
case .ended:
print("Touch end position is \(location)")
print("End location in image is \(boxLocation)")
default:
break
}
}
你可以在storyboard中添加Pan Gesture,也可以用代码创建一个panGesture,并关联这个方法。
现在运行项目,在屏幕上滑动或者拖动,控制台的输出信息应该如下类似:
Touch start position is (234.666656494141, 463.666656494141)
Start location in image is (201.666656494141, 306.666656494141)
Touch end position is (63.0, 482.666656494141)
End location in image is (30.0, 325.666656494141)
现在我们已经设置了基本的界面,下面开始让它动起来。
UIDynamicAnimator和UIAttachmentBehavior
现在我们想要做的第一件事就是让imgView在拖动的时候移动,将要用到一种名为UIAttachmentBehavior的UIKit Dynamics类来执行此操作。
打开DynamicsTossingVC.swift
并将以下代码放在viewDidLoad()
中super.viewDidLoad()
下方。
animator = UIDynamicAnimator(referenceView: view)
originalBounds = imgView.bounds
originalCenter = imgView.center
上面的代码建立了一个UIDynamicAnimator,它是UIKit基于物理动画的引擎。 我们用VC的view作为参考视图,该视图定义了动画制作者的坐标系统。
可以将动画添加到动画制作工具中,这样可以执行诸如附加视图,推动视图,使其受重力影响等等。
从UIAttachmentBehavior开始,使图像视图在制作平移手势时跟踪手指。
为此,请将以下代码添加到handleAttachmentGesture(sender :)
下面case .began
:部分的两个print语句下方:
// 删除可能存在的任何现有动画行为。
animator.removeAllBehaviors()
// 创建一个UIAttachmentBehavior,它将图像视图中的点附加到用户点击一个锚点(碰巧是完全相同的点)。 稍后,更改定位点使图像视图移动。
// 将锚点附加到视图就像安装一个将锚点连接到视图上的固定附件位置的不可见杆。
let centerOffset = UIOffset(horizontal: boxLocation.x - imgView.bounds.midX, vertical: boxLocation.y - imgView.bounds.midY)
attachmentBehavior = UIAttachmentBehavior(item: imgView, offsetFromCenter: centerOffset, attachedToAnchor: location)
// 更新红色方块以指示定位点,并使用蓝色方块来指示图像视图内所附的点。 当手势开始时,这些将是相同的点。
redSquare.center = attachmentBehavior.anchorPoint
blueSquare.center = location
// 将此行为添加到动画器以使其生效。
animator.addBehavior(attachmentBehavior)
接下来,需要让锚点跟随手指。 在handleAttachmentGesture_ :)
中,用下面的代码替换default下的break语句:
attachmentBehavior.anchorPoint = sender.location(in: view)
redSquare.center = attachmentBehavior.anchorPoint
default
下, 这里的代码简单地将锚点和红色方块与手指的当前位置对齐。 当用户的手指移动时,手势识别器调用此方法更新锚点以跟随触摸。 另外,animator 会自动更新视图以跟随定位点。
运行demo,拖动视图会出现如下效果:
注意视图不仅仅是在屏幕上进行旋转; 如果您在图像的某个角落开始手势,则由于锚点的缘故,视图会随着手指移动而旋转。
但是,当完成拖动时,将视图恢复到原始位置会更好。 为了解决这个问题,将这个新方法添加到类中:
fileprivate func resetDemo() {
animator.removeAllBehaviors()
UIView.animate(withDuration: 0.5) {
self.imgView.bounds = self.originalBounds
self.imgView.center = self.originalCenter
self.imgView.transform = CGAffineTransform.identity
}
}
然后在handleAttachmentGesture(_:)
中的case .ended下
面两个print后面调用:
resetDemo()
运行demo。现在拖动图像后,它应该恢复到原始位置。
UIPushBehavior
接下来,我们需要在停止拖动时分离视图,并为其提供动力,以便在运动中释放视图时可以继续其轨迹。 将使用UIPushBehavior完成此操作。
首先,需要两个常量。 将这些添加到文件的顶部:
let ThrowingThreshold: CGFloat = 1000
let ThrowingVelocityPadding: CGFloat = 35
ThrowingThreshhold表示视图必须移动的速度有多快才能使视图继续移动(而不是立即返回到原始位置)。 ThrowingVelocityPadding是一个常数,它会影响投掷应该多快或多慢(这是通过反复试验来选择的)。
最后,在handleAttachmentGesture(_ :)
内部,用下面的代码替换resetDemo()
的调用
animator.removeAllBehaviors()
// 1
let velocity = sender.velocity(in: view)
let magnitude = sqrt(velocity.x * velocity.x + velocity.y * velocity.y)
if magnitude > ThrowingThreshold {
// 2
let pushBehavior = UIPushBehavior(items: [imgView], mode: .instantaneous)
pushBehavior.pushDirection = CGVector(dx: velocity.x / 10, dy: velocity.y / 10)
pushBehavior.magnitude = magnitude / ThrowingVelocityPadding
self.pushBehavior = pushBehavior
animator.addBehavior(pushBehavior)
// 3
let angle = Int(arc4random_uniform(20)) - 10
itemBehavior = UIDynamicItemBehavior(items: [imgView])
itemBehavior.friction = 0.2
itemBehavior.allowsRotation = true
itemBehavior.addAngularVelocity(CGFloat(angle), for: imgView)
animator.addBehavior(itemBehavior)
// 4
let timeOffset = Int(0.4 * Double(NSEC_PER_SEC))
DispatchQueue.main.asyncAfter(deadline: DispatchTime
.now() + DispatchTimeInterval.seconds(timeOffset)) {
self.resetDemo()
}
} else {
resetDemo()
}
对上面的代码一节一节地回顾一下:
1、获取手势的拖动速度。计算速度的大小 - 这是由x方向速度和y方向速度形成的三角
形的斜边。
要理解这个背后的理论,请查看这个Trigonometry for Game Programming教程。
2、假设手势速度超过为动作设置的最小阈值,则设置push行为。 所需的方向由x和y速度组成,并转换为一个给定方向部分的向量。 一旦设置了推送行为,就将其添加到动画序列中。
3、本部分设置了一些旋转以使图像“飞走”。 在这里阅读复杂的计算。 其中一些取决于手指在启动手势时距离手指边缘的距离。
调整这块的value,观察运动如何改变效果。
4、在指定的时间间隔之后,动画通过将图像发送回目的地进行重置,所以它会缩回并返回到屏幕 - 就像球从墙上弹起一样
运行可以看到如下效果:
这里是最终的demo。此demo是raywenderlich下面iOS的Graphics & Animation整个教程系列的集合。