Chapter 5 无限滚动视图:实现
你需要做下面几件事情:
- 实现 UIScrollView 的子类(可选)
- 重写 layoutSubviews() 方法
- 获取 currentOffset
- 需要时,让视图再次显示开始或者结尾部分
实现
UIScrollview
的子类是可选的,因为这里已经存在一个InfiniteScrollview.swift
文件了,你可以在这里编写代码。如果你选择跳过这部分,那你可以直接跳到增加一些内容这段。然而,对于之前没有做过的同学,我已经把创建子类的步骤写下在下面了。
1. 创建 UIScrollview 的子类
在 Xcode 里创建一个新的文件,可以用这种方法:
File > New > File...
或者敲击快捷键:
CMD + N
选择:
iOS > Source > Swift File
命名文件:
InfiniteScrollView
文件创建后,将下面这行代码删除:
import Foundation
换成这行代码:
import UIKit
接着,输入 class
单词,在你输入完整个单词之前,你会看到类似下图的弹出框:

选择 Swift Subclass 敲击回车。Xcode 会给你刚刚创建的类自动提供空白来让你输入内容。
选择 name 然后输入 InfiniteScrollView。
点击 Tab 键,光标出现在 super class variable 这里,输入 UIScrollView
。
再次点击 Tab 键,光标出现在属性和方法这里。
输入 layoutSubviews
,输入的时候会出现自动补全,点击 回车 即可使用自动补全功能。
接下来,复制下面的代码,粘贴到你刚刚创建的新方法里:
super.layoutSubviews()
完成了。你创建了一个子类叫做 'InfiniteScrollView',这个类和 UIScrollView
的功能一模一样,不过,还没开发什么特别的功能,所以,让我们来开发无限滚动效果。
另外,你的方法应该看起来和下面这个方法一样:
override func layoutSubviews() {
super.layoutSubviews() // 调用父类的方法
}
1.1. 放到屏幕上
是时候来测试你新创建的类了,所以,回到 WorkSpace
文件,然后输入下列代码:
class WorkSpace: CanvasController {
let infiniteScrollView = InfiniteScrollView() // 创建一个新的 InfiniteScrollview 对象
override func setup() {
// 根据 canvas 的 frame 创建 CGRect
// 然后赋值给 infiniteScrollView,让 infiniteScrollView 的大小和 canvas 一样大
infiniteScrollView.frame = CGRect(canvas.frame)
// 把 infiniteScrollView 添加到 canvas 里
canvas.add(infiniteScrollView)
}
}
创建了一个名为 infiniteScrollView
的 InfiniteScrollView
变量,将其 frame 的等同于当前 canvas 的大小,然后添加到 canvas 里。
你需要把 canvas 的 frame 值(
Rect
)转换成CGRect
,因为InfiniteScrollView
是UIKit
的子类,只能处理CGRect
和CGPoint
类型。
继续...
点击:
Build > Run
或者
CMD + R
或者
点击 Xcode 上方的 play 按钮
模拟器出现了,里面没什么东西...实际上这里是有一个 view 的,只是你看不到,背景是透明的,你也不能向下滑动,毕竟里面没有内容,自然无法滑动。
1.2. 增加一些内容
为了测试你刚刚创建的 InfiniteScrollView
,你需要添加一些视觉指示器。
将下面的代码复制粘贴到 WorkSpace
里:
func addVisualIndicators() {
// 指示器的最大数量
let count = 20
// 每个指示器之间的间距
let gap = 150.0
// 初始化指示器的偏移量,因为我们要把指示器放在居中的位置
let dx = 40.0
// 计算 view contentSize 的总宽度
let width = Double(count) * gap + dx
// 创建主要的指示器
for x in 0...count {
// 给新的指示器创建中心点
let point = Point(Double(x) * gap + dx, canvas.center.y)
// 创建 textshape
let ts = TextShape(text: "\(x)")
ts.center = point
// 添加到视图上
infiniteScrollView.add(ts)
}
}
然后,调用 setup()
方法,如下:
override func setup() {
infiniteScrollView.frame = CGRect(canvas.frame)
canvas.add(infiniteScrollView)
addVisualIndicators()
}
运行,现在,模拟器上出现了很多连续的数字。
1.3. 内容 + 滚动
在 infiniteScrolView
里,还有些东西无法滚动,因为 view 实际上还不知道里面有什么内容,你必须告诉 view 你更新了一些内容,通过更新 contentSize 变量即可实现。
在循环后面加上这行代码:
infiniteScrollView.contentSize = CGSizeMake(CGFloat(width), 0)
现在 view 知道它内容的最大尺寸,view 可以滚动了。使用 0
作为内容的高度,因为你不需要 view 垂直滚动。
如果你滚动到最末端,你会注意到最后一个数字有点奇怪...这是因为内容的大小只是一个估计值。
为了修复这个问题,你可以从下方两个解决方法中选一个:
- 记录最新的原始位置
- 想办法知道最后一个对象的结束位置
之后更新这个代码。
把下列删除:
nfiniteScrollView.contentSize = CGSizeMake(CGFloat(width), 0)
替换成:
infiniteScrollView.contentSize = CGSizeMake(CGFloat(width+gap), 0)
从循环体中得到最后一个变量时,你计算出中心点,然后 x 方位增加一个间隙,这样你就能看到最后 20 这个数字了。
接下来,你要开始跟踪 contentOffset
的值。
不要担心,view 还是可以回弹的,过会儿你就会来解决你过会就会看到了...
1.4. 内容的偏移量
UIScrollView
的工作方式基本上是这样的:scrollview 要显示的内容远远大于自身 frame 的大小,在任何时间,用户看到的 content view 实际上滚动视图里的范围。
当用户滑动界面时,你需要找到内容的位置。
回到 InfiniteScrollView.swift 文件,更新 layoutSubviews()
,如下:
override func layoutSubviews() {
// 调用 UIScrollView 的布局方法
super.layoutSubviews()
print(contentOffset.x)
}
UIScrollView
有一个 contentOffset
属性,所以你刚刚做的就是告诉 app ,只要调用 layoutSubviews()
方法,就打印出变量的 x 坐标。
运行应用,当你在模拟器上滑动时,你会看到 Xcode 调试视图中出现了一大堆的数字。
如果你看不到 Xcode 的调试台,敲击快捷键 CMD+SHIFT+C
或者,点击 Xcode 右上角的图片,就像下图一样:
现在能跟踪到 scrollview 的 contentOffset
的 x
坐标了。
你可能会注意到,当你朝向开头滑动时,contentOffset.x 的值变成了负数,之后你会用到这个特点的,给你带来不少好处。
1.5. 无限循环的规则
现在需要实现无限循环的规则了。
更新 layoutSubviews()
方法,如下:
public override func layoutSubviews() {
super.layoutSubviews()
// 获取当前内容的偏移量(左上角)
var curr = contentOffset
// 如果 x 的值小于零
if curr.x < 0 {
// 更新 x 的值,复制 ScrollView 的结束位置
curr.x = contentSize.width - frame.width
// 设置 view 的内容偏移量
contentOffset = curr
}
// 如果 x 值大于 width - frame width
//(比如,当右上角的点超出了当前的 contentSize.width)
else if curr.x >= contentSize.width - frame.width {
// 更新 x 的值,赋值 ScrollView 的开始位置
curr.x = 0
// 设置 view 的内容偏移量
contentOffset = curr
}
}
不管什么时候到了内容的边界位置,从另外一个方向,view 应该迅速跳过,这样才能有持续滚动的效果。
运行,自己看一下效果吧。
1.6. 迅速重叠
还有一件事情需要注意,当 view 重叠时(例如从开头走到了结束,或者反之),里面的内容会发生奇怪的移动...
解决方法就是将开头的内容连接到结束的位置,这样滚动起来就会流畅许多。

然而,这个操作和 scrollview 无关。这个小技巧只依赖于你如何在结尾增加内容。
所以,回到 WorkSpace 文件中。
2. 正确的设置
增加一个新方法 createIndicator()
,用来给滚动视图创建和增加文本。
添加如下代码:
func createIndicator(text: String, at point: Point) {
// 创建一个 TextShape
let ts = TextShape(text: text)
// 居中 TextShape
ts.center = point
// 添加到 canvas 上
infiniteScrollView.add(ts)
}
在这个方法里,我们得到了一个 String
类型和一个 Point
类型,创建了一个新的文本形状的指示器,定位,然后添加到滚动视图里来。
太简单了。
现在,把 addVisualIndicator
的内容换成下放代码:
func addVisualIndicators() {
// 设置指示器的最大数
let count = 20
// 设置每个指示器之间的间隔
let gap = 150.0
//对于每个指示器,设置开始时中心位置的偏移量
let dx = 40.0
// 初始化指示器的偏移量,因为我们要把指示器放在居中的位置
let width = Double(count + 1) * gap + dx
// 创建主要的指示器
for x in 0...count {
//给新的指示器创建中心点
let point = Point(Double(x) * gap + dx, canvas.center.y)
//创建新的指示器
createIndicator("\(x)", at: point)
}
// 创建额外的指示器
var x : Int = 0
// 创建偏移量
var offset = dx
// 总宽度(包括无限滚动视图里最后一个 view,基于 width + screen width)
// 所以,总宽度,和一个有多少个额外的指示器,在一定程度上是随机的
// 这也是为什么我们要使用 while 循环
// 当偏移量小于 view 的宽度时
while offset < Double(infiniteScrollView.frame.size.width) {
// 创建中心点,x 坐标值为 width + the current offset
let point = Point(width + offset, canvas.center.y)
// 创建宽度
createIndicator("\(x)", at: point)
// 对下一个点增加偏移量
offset += gap
// 增加 x 坐标值,作为下一个指示器的数值
x += 1
}
// 更新 infiniteScrollView 的 contentSize
infiniteScrollView.contentSize = CGSizeMake(CGFloat(width) + infiniteScrollView.frame.size.width, 0)
}
现在,运行之后一切都会流畅啦。
当数字滑动到结束时,就会无缝连接继续循环。
3. 打包
你已经将无限滚动视图添加到了 UIKit 的 UIScrollView
类里,这部分比较简单。真正有意思的是实现无限滚动的效果,这有赖于你是如何将内容添加进去的。
你已经学会了如何在滚动视图的结尾部分增加一点额外的内容,这样能让滚动的效果看起来更流畅、无缝连接。
在我们配置无限滚动视图时,大部分的时间都是在让内容显示在正确的位置上。正如这个例子所说明的一样,如果你想用代码让视图排列正确,可能需要使用一些小伎俩。
3.1. 代码
这里有 InfiniteScrollView
类的最终代码: