CoreImage 变换

原文: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 文件。

现在运行项目,什么也没有——你只能看到屏幕中间有一张图片:

CoreImage 变换_第1张图片

除了屏幕上的这个 image view 外,这个项目只是一个普通的 single view 模板。如果你注意看项目导航器,你会发现几张图片——两张是图片本身,有 3 张是遮罩图片,你后面会用到它们:

接下来你将扩展 UIImageView ,为它添加一些功能。

TransitionImageView

在 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 个新属性:

  • duration 属性:动画时长,单位秒,它用 @IBInspectable 进行修饰,这样便于你在 IB 中修改它的值。
  • filter 属性:CoreImage 滤镜,你将在每次转换时应用的滤镜。所谓的 copy machine 转换是一种经典的有趣的动画,没有它的教程,我们只好将就一下。你会发现这种滤镜很好玩。
  • transitionStartTime 属性:用于保存当前动画什么时候开始的。
  • transitionTimer 属性:为了制作自定义动画,我们必须使用定时器,通过定时器触发每一个动画帧的渲染。

很好的开头!再唠嗑一下关于滤镜的哪些事吧……

CoreImage 滤镜速成

CoreImage 滤镜很强大,能够让你很容易就创建出有趣的图形效果。滤镜通常会有一张原图,然后根据它输出修改后的图。例如,如果你对一张照片应用“褐化滤镜”,会制造出如下效果:

CoreImage 变换_第2张图片

大部分滤镜你都通过可以修改一堆参数来满足你的需要。以上面的褐化滤镜为例,你可以指定原色应该被去掉多少饱和度,以及图片应当加上多少褐色。

除了简单滤镜外,还有一堆被称作“转换”的特殊滤镜。

它们能将一张图片转变成另一张图片。事实上,这些滤镜会产生从原图转换到目标图片的过程中多个帧,构成一个动画。

因此你可以创建一个转换滤镜,给它“指定原图和目标图,然后动画的 60% 处生成动画帧”。(或者动画过程中的任意百分点上)

要创建一个使用滤镜的动画——你需要不停地重复询问滤镜动画的下一帧,然后将这个帧作为 image view 类的显示图片:

CoreImage 变换_第3张图片

听起来不错。

在开始写代码之前,你必须在 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。

创建一个 NSTimer 动画

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

这会检查当前时间是否已经超出了动画的时长,如果是的话:

  • 将转换动画的最后图片设置为定时器的 userInfo 属性(你之前已经为此实现保存过它)。
  • 销毁定时器,这会停止向 image view 提供新的帧。

好了,就这样了!让我们来试试效果吧!

你的第一个 CoreImage 动画

没错,也许你已经猜到了,你还没有真正调用 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 转换:

CoreImage 变换_第4张图片

这个滤镜会产生一种光线在复印机上扫描的效果——是不是很好玩?

接下来我们会创建更复杂的转换动画。现在,让我们看看如何调整当前转换动画的一些参数。

在你设置原图和目标图的代码下面,添加 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)
  • extent 定义一个向量,让滤镜效果覆盖图片的整个区域。例如,复印机的激光将扫过到达右边的整个路径,而不是默认的一半路径。在创建这个向量时,你需要将宽度和高度乘以 2,因为图片是 @2x 图。
  • color 是亮蓝色,复印机激光是一种蓝色的冷色调。

最后设置了两个滤镜参数,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。

运行项目,你会发现动画变得如此的炫和与众不同:

CoreImage 变换_第5张图片

CIDisintegrateWithMaskTransition 所做的不过是使用了这张图片作为蒙版:

然后从蒙层图片的暗部到亮部逐步显示目标图片的局部。下图显示了这个动画的步骤:

CoreImage 变换_第6张图片

你可以看到,一开始只有蒙版中黑色和非常黑的部分开始显示。然后动画逐渐进入到深灰色、灰色和浅灰色的区域。最终,整个蒙层图片由所有灰色阴影部分加上白色部分构成。

这个蒙层图片很好地颜色了这个特效,因为它有一个颜色渐变,你可以清楚地看到这个动画是如何产生的。

你可以试一下另外两张蒙层图片。Mask1.jp 是这个样子:

它会产生这样的效果:

CoreImage 变换_第7张图片

Mask3.jpg 是这个样子:

CoreImage 变换_第8张图片

它产生一种极炫的下落的碎片动画效果——尝试一下吧!

如你所见——仅仅 CIDisintegrateWithMaskTransition 一个滤镜就能产生数不清的转换动画!而 CoreImage 还有很多这样的滤镜呢。

别忘了看一眼转换滤镜列表。

接下来做什么?

用 CoreImage 转换滤镜来创建 View Controller 转换是一个不错的想法。例如,你可以扩展第 23 章“交互式 UINavigationController 转换动画”中的项目,实现一个 CoreImage 转换动画。

这个任务并不难,因为这个项目中的手势识别器提供了平移手势的进度 0-1 之间的值。

你可以为新、老 view controller 截图,然后在 CoreImage 中使用这两张图,构成呈现动画。

如果你准备基于本教程的内容编写一些好玩的东东,请回复这封邮件或者 twitter 给我 @icanzilb。

你可能感兴趣的:(iPhone开发,iOS动画专栏,ios,动画,CoreImage,swift2)