原文:Transitions with CoreImage
作者:Marin Todorov
译者:kmyhy
本教程兼容 Xcode 7/Swift 2。
在“iOS Animations by Tutorials ”的第 3 章 “转换动画” 中,我向你介绍了如何用内置的转换动画来渐入或渐出你的 view。
坦白讲,这种动画有一定限制。你可以选择以内容的位置进行动画,或者交叉溶解,或者反转动画。
CATransition 类中有一个属性 filter,允许你使用 CoreImage 滤镜来加强转换动画。
这个属性也可以用在 OS X——你可以创建任意 CoreImage 转换滤镜,然后赋给转换动画对象的 filter 属性。
在 iOS 中,filter 属性好像无所作为。网上有许多讨论,但这个属性就是不起作用。
幸好创建自己的视图转换动画用一个 CoreImage 转换滤镜来实现也不太难。在 Core Image 编程指南中包含了一个 如何做的 11 个步骤,将本期主题的理论基础。
在本教程的最后,你将实现一个 UIImageView 子类,你可以利用 CoreImage 来转换图片,效果如下:
为了节省你创建 Xcode 项目和配置 UI 和自动布局是时间,我为你准备了开始项目。
下载 CITransitions-Starter.zip,解压缩,打开 CITransitions.xcodeproj 文件。
现在运行项目,什么也没有——你只能看到屏幕中间有一张图片:
除了屏幕上的这个 image view 外,这个项目只是一个普通的 single view 模板。如果你注意看项目导航器,你会发现几张图片——两张是图片本身,有 3 张是遮罩图片,你后面会用到它们:
接下来你将扩展 UIImageView ,为它添加一些功能。
在 Xcode 主菜单中选择 File/New/File… ,然后是 iOS/Source/Cocoa Touch Class。类名命名为 TransitionImageView,继承 UIImageView 并保存。
将文件内容修改为:
import UIKit
import CoreImage
class TransitionImageView: UIImageView {
@IBInspectable var duration: Double = 2.0
private let filter = CIFilter(name: "CICopyMachineTransition")!
private var transitionStartTime: CFTimeInterval = 0.0
private var transitionTimer: NSTimer?
}
这里声明了 4 个新属性:
很好的开头!再唠嗑一下关于滤镜的哪些事吧……
CoreImage 滤镜很强大,能够让你很容易就创建出有趣的图形效果。滤镜通常会有一张原图,然后根据它输出修改后的图。例如,如果你对一张照片应用“褐化滤镜”,会制造出如下效果:
大部分滤镜你都通过可以修改一堆参数来满足你的需要。以上面的褐化滤镜为例,你可以指定原色应该被去掉多少饱和度,以及图片应当加上多少褐色。
除了简单滤镜外,还有一堆被称作“转换”的特殊滤镜。
它们能将一张图片转变成另一张图片。事实上,这些滤镜会产生从原图转换到目标图片的过程中多个帧,构成一个动画。
因此你可以创建一个转换滤镜,给它“指定原图和目标图,然后动画的 60% 处生成动画帧”。(或者动画过程中的任意百分点上)
要创建一个使用滤镜的动画——你需要不停地重复询问滤镜动画的下一帧,然后将这个帧作为 image view 类的显示图片:
听起来不错。
在开始写代码之前,你必须在 IB 中做些修改。打开 Main.storyboard - 你会看到这个 image view:
在做动画之前,你必须将这个 image view 对象的类型设置为你的新类 TransitionImageView。选中 image view,修改 Class 属性:
确认一下它没问题——回到属性检查器,看一下里面是不是能够看到你的 @IBInspectable duration 属性:
打开 ViewController.swift 为 image view 添加一个出口:
@IBOutlet weak var imageView: TransitionImageView!
回到 Main.storyboard ,按住 Ctrl 键从 ViewController 对象拖到 image view。在弹出菜单中选择 imageView,即可连接到该出口。
UI 完成后,你可以编写动画代码了。
在 iOS 创建新的滤镜,需要预先定义它的参数集。这是很有必要的,因为你可以轻松地测试这些滤镜,将滤镜效果调整成你需要的。
对于一个转换滤镜来说,最起码的参数是原图和目标图。打开 TransitionImageView.swift 新增方法:
func transitionToImage(toImage: UIImage?) {
guard let image = image, let toImage = toImage else {
fatalError("You need to have set an image, provide a new image and a mask to fire up a transition")
}
filter.setValue(CIImage(image: image), forKey: kCIInputImageKey)
filter.setValue(CIImage(image: toImage), forKey: kCIInputTargetImageKey)
}
transitionToImage 方法只有一个参数,也就是目标图片。这个方法会对滤镜进行配置,并开始动画。
来看下代码。首先,你要确认 image view 的 image 属性是否已经设置,以及 toImage 参数是否不为空。
然后调用 setValue(_,forKey:) 方法,将 image 作为滤镜的原图, key 参数指定为 kCIInputImageKey。
注意 CoreImage 类使用的是 CIImage(而非 UIImage 或者 CGImageRef)。
最后设置滤镜的目标图,将 toImage 参数赋给(转换成 CIImage)key kCIInputTargetImageKey。
好!这样你就配置好了转换的原图和目标图。现在你需要创建一个定时器,开始获得动画帧并将它们提供给 image view。
在 transitionToImage: 方法中添加:
if let timer = transitionTimer where timer.valid {
timer.invalidate()
}
这个判断语句会在 transitionToImage 方法被调用,也就是已经有一个动画还未完成时重置定时器。这里调用的是 timer.invalidate(),它会重置已有的动画,以便开始一个新的动画。
然后保存当前新动画的开始时间。继续添加代码:
transitionStartTime = CACurrentMediaTime()
CACurrentMediaTime() 会拿到当前绝对时间的秒数(小数部分则是精确到毫秒)。在计算动画进度时需要用到这个动画开始时间。
最后来触发动画定时器:
transitionTimer = NSTimer(timeInterval: 1.0/30.0,
target: self, selector: Selector("timerFired:"),
userInfo: toImage,
repeats: true)
NSRunLoop.currentRunLoop().addTimer(transitionTimer!,
forMode: NSDefaultRunLoopMode)
定时器的重复周期是 1/30 秒,也就是每秒触发 30 次。你可以调高这个帧率,但请注意,CoreImage 会执行你的滤镜,每秒执行的次数越多,对设备的考验就越大。
selector 参数中指定的 timerFired: 方法是 ViewController 类中的方法。你还没有这个方法,但等会就会添加它。
还有一个地方需要注意——每个 NSTimer 都会有一个 userInfo 属性。这个属性的类型是 AnyObject?。你可以赋给它任意类型。你可以用它来保存后面要用到的任何信息。就眼下来说,你需要保存转换的最后的 UIImage——当转换完成,你可以用它作为动画的最后一帧。
接下来你需要添加一个方法,每当定时器触发时获取一个动画帧。添加一个空方法:
func timerFired(timer: NSTimer) {
}
首先需要知道转换动画已经完成了多久。你需要一个 0-1 之间的值,以表示动画完成进度。
在 timerFired: 方法中添加代码:
let progress = (CACurrentMediaTime() - transitionStartTime) / duration
首先算出从动画开始到现在经过了多少时间,然后除以整个动画时长 duration。你会得到一个 0.0-1.0 之间的数,表示转换过程的完成度 progress。
接下来需要让滤镜知道这个进度。添加这句:
filter.setValue(progress, forKey: kCIInputTimeKey)
将 progress 设置到 kCIInputTimeKey 这个 key 中,这样下次获取滤镜的输出时,它会知道你想要的是动画的哪一帧。
这样,你就完成了将滤镜的输出作为当前 image view 的 image 的工作。现在只需要从滤镜的 outputImage 属性中获得滤镜的处理结果,并赋给 image view 即可:
image = UIImage(CIImage: filter.outputImage!,
scale: UIScreen.mainScreen().scale,
orientation: UIImageOrientation.Up)
你基于 CIImage 滤镜的结果、正确的屏幕 scale 和方向,创建了一个新的 UIImage 对象。
这些代码已经给你的 image view 类添加了一个很酷的动画,但我们还是多写几句代码将 timerFired: 方法封装得更好一些。
添加代码:
if CACurrentMediaTime() > transitionStartTime + duration {
image = timer.userInfo as? UIImage
timer.invalidate()
}
这会检查当前时间是否已经超出了动画的时长,如果是的话:
好了,就这样了!让我们来试试效果吧!
没错,也许你已经猜到了,你还没有真正调用 transitionToImage 方法。
当你点击屏幕时,我们将 image view 变成现实另外一张图片。
打开 ViewController.swift ,在 viewDidLoad: 方法中添加:
view.addGestureRecognizer(
UITapGestureRecognizer(target: self, action: Selector("didTap"))
)
在 view controller 的 View 上加一个手势识别器,当用户点击它时调用 didTap 方法。好的——现在来实现 didTap 方法和一个助手属性:
var currentImageName = ""
func didTap() {
currentImageName = (currentImageName == "Photo2.jpg") ? "Photo1.jpg" : "Photo2.jpg"
imageView.transitionToImage(UIImage(named: currentImageName))
}
每当你点击屏幕,都会在 Photo1.jpg 和 Photo2.jpg 之间切换图片。这使得我们可以反复地测试它。
好了!一切准备就绪。
注意最后一点——从现在起你必须在真机上进行测试。模拟器对这种测试不太理想,因此请用真机测试。
运行 app,点击屏幕,你会看到你的第一个 CoreImage 转换:
这个滤镜会产生一种光线在复印机上扫描的效果——是不是很好玩?
接下来我们会创建更复杂的转换动画。现在,让我们看看如何调整当前转换动画的一些参数。
在你设置原图和目标图的代码下面,添加 2 行,设置滤镜的 extent 和颜色:
let extent = CIVector(x: 0.0, y: 0.0,
z: image.size.width * 2.0, w: image.size.height * 2.0)
let color = CIColor(red: 0.6, green: 1.0, blue: 1.0)
filter.setValue(extent, forKey: kCIInputExtentKey)
filter.setValue(color, forKey: kCIInputColorKey)
最后设置了两个滤镜参数,key 分别为 kCIInputExtentKey 和 kCIInputColorKey。
再次运行项目,反复点击屏幕,看一下调整后的动画——你会发现亮蓝色的光束扫过了整个屏幕。
漂亮!
如果你想了解 Copy Machine 滤镜支持的所有参数,你可以看一下在线的 CICopyMachineTransition 文档。
有各种不同的转换滤镜。你可以用它们创建各种动画,然后调整它们的参数。有效的滤镜以及对应的设置请看在线文档。
接下来的内容将快速过一下 CIDisintegrateWithMaskTransition,这个滤镜能创建一种非常炫的转换。
打开 TransitionImageView.swift 添加一个属性:
@IBInspectable var maskImage: UIImage?
CIDisintegrateWithMaskTransition 使用了蒙层图片,因此你添加了一个属性用来保存这个蒙层图片。
然后找到 filter 属性声明,将它修改为:
private let filter = CIFilter(name: "CIDisintegrateWithMaskTransition")!
新的 filter 属性使用了一个不同的 name 参数,因此需要用这个 name 来初始化。
找到 transitionWithImage: 方法。将开头的 if 语句修改为检查 maskImage:
guard let image = image, let toImage = toImage, let maskImage = maskImage else {
然后删除设置 extent 和 color 参数的代码:
let extent = CIVector(x: 0.0, y: 0.0, z: image!.size.width * 2.0, w: image!.size.height * 2.0)
let color = CIColor(red: 0.6, green: 1.0, blue: 1.0)
filter.setValue(extent, forKey: kCIInputExtentKey)
filter.setValue(color, forKey: kCIInputColorKey)
CIDisintegrateWithMaskTransition 滤镜没有这两个参数,如果你不删除它们的话,CoreImage 会导致 app 崩溃。
在删除代码的地方,添加这句,以设置动画的蒙层图片:
filter.setValue(CIImage(image: maskImage), forKey: kCIInputMaskImageKey)
这个类的修改就完成了。在运行项目之前,回到 IB,将视图中 Mask Image 属性设置为 Mask2.png。
运行项目,你会发现动画变得如此的炫和与众不同:
CIDisintegrateWithMaskTransition 所做的不过是使用了这张图片作为蒙版:
然后从蒙层图片的暗部到亮部逐步显示目标图片的局部。下图显示了这个动画的步骤:
你可以看到,一开始只有蒙版中黑色和非常黑的部分开始显示。然后动画逐渐进入到深灰色、灰色和浅灰色的区域。最终,整个蒙层图片由所有灰色阴影部分加上白色部分构成。
这个蒙层图片很好地颜色了这个特效,因为它有一个颜色渐变,你可以清楚地看到这个动画是如何产生的。
你可以试一下另外两张蒙层图片。Mask1.jp 是这个样子:
它会产生这样的效果:
Mask3.jpg 是这个样子:
它产生一种极炫的下落的碎片动画效果——尝试一下吧!
如你所见——仅仅 CIDisintegrateWithMaskTransition 一个滤镜就能产生数不清的转换动画!而 CoreImage 还有很多这样的滤镜呢。
别忘了看一眼转换滤镜列表。
用 CoreImage 转换滤镜来创建 View Controller 转换是一个不错的想法。例如,你可以扩展第 23 章“交互式 UINavigationController 转换动画”中的项目,实现一个 CoreImage 转换动画。
这个任务并不难,因为这个项目中的手势识别器提供了平移手势的进度 0-1 之间的值。
你可以为新、老 view controller 截图,然后在 CoreImage 中使用这两张图,构成呈现动画。
如果你准备基于本教程的内容编写一些好玩的东东,请回复这封邮件或者 twitter 给我 @icanzilb。