Chapter 9 调试视差图层(可选)
本章将向你简单介绍最初是如何测试背景(速度,层级等)的,且这一章是可选的。如果你想知道具体是怎么操作的,那么强烈推荐你一步步看下去。
1. 视差背景
首先,创建一个名叫 ParallaxBackground
的文件,并导入 UIKit
(而不是导入 Foundation
)。并在该文件中,添加一个和文件名相同的类(此类需继承自 CanvasController)。新的文件应该看起来是这样的:
class ParallaxBackground : CanvasController {
}
接下来,在 WorkSpace
中添加一个 background
变量,能用来初始化 ParallaxBackground
的实例:
在继续下去之前,我们先在 WorkSpace
外部建立一些颜色的常量以便整个工程都能使用。你的 WorkSpace
现在看起来应该是这样的:
import UIKit
import C4
let COSMOSprpl = Color(red:0.565, green: 0.075, blue: 0.996, alpha: 1.0)
let COSMOSblue = Color(red: 0.094, green: 0.271, blue: 1.0, alpha: 1.0)
let COSMOSbkgd = Color(red: 0.078, green: 0.118, blue: 0.306, alpha: 1.0)
class WorkSpace: CanvasController {
var background = ParallaxBackground()
override func setup() {
canvas.add(background.canvas)
}
}
和 WorkSpace 类似,背景对象也是 CanvasController 类的子类。因此,背景对象也有 canvas 属性(其实是一个 view),我们才能使用 background.canvas
完成以上这些之后,我们来看看添加具有视差的背景大致要几步:
- 创建速度数组,以标明每个图层移动的快慢
- 基于各自的速度创建并添加所有的图层
- 为创建视差效果而添加一个上下文和观察者
- 为一些标签测试滚动效果
以上这些操作都是在 WorkSpace 中完成的,接下来的操作都将在 ParallaxBackground 文件中实现。
2. 创建速度数组和滚动视图数组
这一步非常简单。为之后的操作创建一个 CGFloat
为元素类型的数组。
在类中添加如下的变量:
let speeds : [CGFloat] = [0.08,0.0,0.10,0.12,0.15,1.0,0.8,1.0]
我们从设计文件中获取到数值,并按顺序添加。最底层的速度为 0.08
,是数组下标为 [0]
的第一个元素;而最上层的速度是最后一个元素。
我们将数组的类型设置为 : [CGFloat]
,因为我们会将这些值直接赋值给继承自 UIScrollView
的视图。事先知道需要的类型是 CGFloat
,而不用在使用 Double
的时候去转型了。
最后,添加一个元素为 InfiniteScrollview
类型的数组。之后的操作中会用到这个变量,我们先放在前面。
lazy var scrollviews = [InfiniteScrollView]()
你的类现在应该是这样子的:
class ParallaxBackground : CanvasController {
let speeds : [CGFloat] = [0.08,0.0,0.10,0.12,0.15,1.0,0.8,1.0]
var scrollviews = [InfiniteScrollView]()
}
3. 创建基于速度的图层
因为我们已经创建了 InfiniteScrollView
类,所以我们可以直接开始了。添加所有图层最简单的方法是一个个创建和添加,但是这样效率太低了,而且我们都已经知道各图层滚动速度是不同的。
上面的这张图片展示了滚动视图是如何跟随速度改变而显示的。与最上层移动整个宽度相比,速度为 0.08
的第一层只需要移动最上层宽度的 8% 作为 contentSize
即可。这就意味着我们不需要为所有的图层都创建全尺寸的内容大小。
3.1. 动态图层的内容大小
我们想要根据各自的速度创建图层,而且,最好是有一个方法只要传入正确的参数就可以返回给我们。
这个方法就是:
func createLayer(speed: CGFloat) -> InfiniteScrollView {
return InfiniteScrollView()
}
这可以作为 setup() 方法里创建动态图层的方法。创建的 setup() 方法如下:
override func setup() {
for i in 0..<speeds.count {
let layer = createLayer(speeds[i])
canvas.add(layer)
scrollviews.append(layer)
}
}
我们在新的图层上添加画布(canvas)和滚动视图数组的原因主要有两个:一个是想在屏幕上显示,另一个是之后会用到。
运行之。
我们什么也没有看到,因为创建了没有内容的对象。因此,我们来修改前面提到的 createLayer
方法。
3.2. 修改 createLayer
首先,我们知道一共会有 12 个标志,因此创建一个 let signCount
。
添加到你的类中:
let signCount : CGFloat = 12.0
同样地,因为是赋值给 UIKit 对象的,所以我们指定的类型是 CGFloat。而且,将全局变量放置在类的顶部。
因为我们只是简单的测试我们写的 createLayer
方法,所以添加一些数字就可以了。
获取视图的框架大小,然后来初始化图层,如下:
func createLayer(speed: CGFloat) -> InfiniteScrollView {
let frame = view.bounds
let layer = InfiniteScrollView(frame: frame)
return layer
}
没有直接返回 InfiniteScrollView
是为了在返回前修改图层内容的大小。
接下来,我们修改一下图层的内容大小,如下:
func createLayer(speed: CGFloat) -> InfiniteScrollView {
let frame = view.bounds
let layer = InfiniteScrollView(frame: frame)
let contentSize = CGSizeMake(frame.size.width * 2.0 * signCount, frame.size.height)
layer.contentSize = contentSize
return layer
}
现在,为了在屏幕上显示,添加一些标签。在我们 return
之前,添加 repeat
来将这些标签充满整个 scrollviews
。
let dx = Double(layer.contentSize.width) / 100.0
var center = Point(dx, canvas.center.y)
repeat {
let label = TextShape(text: "\(scrollviews.count)")
label.center = center
layer.add(label)
center.x += dx
} while center.x < Double(layer.contentSize.width)
这一步创建了一个偏移变量 dx
,会给我们基于图层 contentSize
的 20 个标签。接着,创建了一个 center
变量将用它的 x 位置标识每个添加的标签。最后,用 repeat
循环直到中心位置超出图层的内容。循环内,创建一个基于 scrollviews
数组当前元素数字的新标签(所以第一层的下标是 0
)。
运行你的应用是这样的:
看到所有的数字都叠在一起了吗?这是因为每层的 contentSize
都是相同的。是时候将 speed
变量派上用场了。
修改如下:
let contentSize = CGSizeMake(frame.size.width * 2.0 * signCount * speed, frame.size.height)
现在你整个 ParallaxBackground 文件看起来应该是这样的:
import UIKit
class ParallaxBackground : CanvasController {
let speeds : [CGFloat] = [0.08,0.0,0.10,0.12,0.15,1.0,0.8,1.0]
var scrollviews : [InfiniteScrollView]!
let signCount : CGFloat = 12.0
override func setup() {
scrollviews = [InfiniteScrollView]()
for i in 0..<speeds.count {
let layer = createLayer(speeds[i])
canvas.add(layer)
scrollviews.append(layer)
}
}
func createLayer(speed: CGFloat) -> InfiniteScrollView {
let frame = view.bounds
let layer = InfiniteScrollView(frame: frame)
let contentSize = CGSizeMake(frame.size.width * 2.0 * signCount * speed, frame.size.height)
layer.contentSize = contentSize
let dx = Double(layer.contentSize.width) / 100.0
var center = Point(dx, canvas.center.y)
repeat {
let label = TextShape(text: "\(scrollviews.count)")!
label.center = center
layer.add(label)
center.x += dx
} while center.x < Double(layer.contentSize.width)
return layer
}
}
你的应用应该是这样的:
看起来不爽吧,让我们把它们根据 contentSize 和层数分开来吧:
let dx = Double(layer.contentSize.width) / 100.0
let dy = Double(layer.contentSize.height) / Double(speeds.count+1)
var center = Point(dx, Double(scrollviews.count + 1) * dy)
let fontSize = Double(scrollviews.count + 1) * 6.0
let font = Font(name:"AvenirNext-DemiBold", size: fontSize)!
repeat {
let label = TextShape(text: "\(scrollviews.count)", font: font)!
//...
}
现在看起来好多了:
看到 1 很奇怪的靠在左边了吗?这是因为这一层的速度是 0.0...这一层不会移动,所以它的 contentSize 就是 0。
当你滚动的时候:
4. 连接起来吧
之前做过的事情再让我们做一次!我们会为创建基于图层速度的视差效果而添加上下文和观察者。
首先,为类添加一个新属性:
var scrollviewOffsetContext = 0
这个变量会指出观察我们图层偏移量的上下文。
从敲出 observeValueForKeyPath
开始,当你敲了 b
时,Xcode 会提示 如下:
敲 return
回车就可以了。
现在,添加如下代码:
if context == &scrollviewOffsetContext {
print("top layer is scrolling")
}
在 setup()
方法里创建视图之后添加以下的代码来连接最上层的图层。
if let topLayer = scrollviews.last {
topLayer.addObserver(self, forKeyPath: "contentOffset", options: .New, context: &scrollviewOffsetContext)
}
现在的类应该看上去是这样的:
class ParallaxBackground : CanvasController {
var speeds ...
var scrollviews ...
let signCount ...
var scrollviewOffsetContext = 0
override func setup() {
for i in 0..<speeds.count {
...
}
if let topLayer = scrollviews.last {
topLayer.addObserver(self, forKeyPath: "contentOffset", options: .New, context: &scrollviewOffsetContext)
}
}
func createLayer(speed: CGFloat) -> InfiniteScrollView {
}
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
if context == &scrollviewOffsetContext {
print("top layer is scrolling")
}
}
}
运行之。随着滚动,Xcode 控制台会跳出一些数字。
现在,将观察的方法与其他的图层连接起来。
把
print("top layer is scrolling")
替换为
let sv = object as! InfiniteScrollView
let offset = sv.contentOffset
for i in 0..<scrollviews.count-1 {
let layer = scrollviews[i]
layer.contentOffset = CGPointMake(offset.x * speeds[i], 0.0)
}
代码中获取到观察的对象后,转型成 InfiniteScrollview
(我们可以使用 !
来强制转型,因为我们确定知道它是什么类型的)。接着,获取到偏移量,并用其计算其余图层的偏移量。
最后,用循环遍历所有图层并基于速度修改 contentOffset
的值。
现在是这样子的:
这里逻辑上有个错误。我们一开始说:如果速度是主图层的 80%,就只需要画布(canvas)是宽度的 80% 就够了。这是对的。
但是,当主图层滚动到画布边缘时。视图会停止滚动,因为只有内容在框架内时才能滚动。

按理说,80% 主画布大小的画布只能滚动到 80% - visibleFrameWidth
。因此,我们需要将框架的宽度添加到速度低于 1.0 的画布上。
如果要修复这个 bug,我们就要回到 createLayer()
方法,在创建 contentSize 和赋值之间添加一个判断条件:
let contentSize = ...
contentSize.width += speed < 1.0 ? view.frame.width : 0
layer.contentSize = contentSize
确保你把
let contentSize = ...
改成了var
我们将使用一个技巧,让我们可以根据判断条件只用一行代码为变量赋值。
speed < 1.0 ?
意思就是:「这个 speed 是否小于 1.0?」
还有这个:
view.frame.width : 0
意思是如果判断为 true
就返回 view.frame.width
否则就是 0
。
现在,都照我们预想那样了:
ParallaxBackground.swift 可以从这里下载。
5. 总结
这是我第一次测试以不同的速度产生视差效果。效果还不错:我们知道了每一层的内容是如何滚动的,以及所有图层针对最上层移动是如何响应的。
即使当前方法是有效的,我们也不会在应用使用,之后的章节中会讲到有什么区别。
现在,你可以从你的工程中删除整个类了,因为我们再也不会使用它了。
让我们继续吧!