原文链接
作者:C4 开源项目
译者:Crystal Sun
全部章节请关注此文集C4教程翻译
校对后的内容请看这里
需要你做下面几件事情:
- UIScrollview 的子类
- 重写 layoutSubviews() 方法
- 抓住 currentOffset
- 如果需要,跳过结尾或者开始
UIScrollview
的子类是可选的,这里已经存在一个InfiniteScrollview.swift
文件了,你可以在这里编写代码。如果你选择跳过这部分,那你可以直接跳到 增加一些内容 这段。然而,对于之前没有做过的同学,我已经把创建子类的步骤下在下面了。
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 键,光标出现在 properties and methods variable 这里。
输入 layoutSubviews
,输入的时候会出现自动补全,点击 回车 即可使用自动补全功能。
接下来,复制下面的代码,粘贴到你刚刚创建的新方法里:
super.layoutSubviews()
完成了(查了半天,finito是西班牙语 我完成了 的意思)。你创建了一个子类叫做 'InfiniteScrollView',这个类和 UIScrollView
的功能一模一样,不过,还没开发什么特别的功能,所以,让我们来开发无限效果。
另外,你的方法应该看起来和下面这个方法一样:
override func layoutSubviews() {
super.layoutSubviews() //调用父类的方法
}
放到屏幕上
是时候来测试你新创建的类了,所以,回到 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 的,只是你看不到,背景是透明的,你也不能向下滑动,毕竟里面没有内容,自然无法滑动。
增加一些内容
为了测试你刚刚创建的 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)
//创建 textshap
let ts = TextShape(text: "\(x)")
ts.center = point
//添加到 canvas 上
infiniteScrollView.add(ts)
}
}
然后,调用 setup()
方法,如下:
override func setup() {
infiniteScrollView.frame = CGRect(canvas.frame)
canvas.add(infiniteScrollView)
addVisualIndicators()
}
运行,现在,模拟器上出现了很多数组。
内容 + 滚动
在 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 还是可以回弹的,你过会就会看到了...
内容的 Offset
UIScrollView
的工作方式基本上是这样的:scrollview 要显示的内容远远大于自身 frame 的大小,在审核时间,用户看到的 content view 实际上滚动视图里的范围。
当用户滑动界面时,你需要找到内容的位置。
回到 InfiniteScrollView.swift 文件,更新 layoutSubviews()
,如下:
override func layoutSubviews() {
//调用 UIScrollView 的 layout 方法
super.layoutSubviews()
print(contentOffset.x)
}
UIScrollView
有一个 contentOffset
属性,所以你刚刚做的就是告诉 app ,只要调用 layoutSubviews()
方法,就打印出变量的 x 坐标。
运行应用,当你在模拟器上滑动时,你会看到 Xcode 调试视图中出现了一大堆的数字。
如果你看不到 Xcode 的调试台,敲击快捷键 CMD+SHIFT+C
或者,点击 Xcode 右上角的图片,就是下图这个图标:
现在能跟踪到 scrollview contentOffset
的 x 坐标了。
你可能会注意到,当你朝向开头滑动时,contentOffset.x 的值变成了负数,之后你会用到这个特点的,给你带来不少好处。
无限循环的规则
现在需要实现无限循环的规则了。
更新 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 应该迅速跳过,这样才能有持续滚动的效果。
运行,自己看一下效果吧。
迅速重叠
还有一件事情需要注意,当 view 重叠时(例如从开头走到了结束,或者反之),里面的内容会尴尬地跳动...
解决方法就是将开头的内容连接到结束的位置,这样滚动起来就会流畅许多。
然而,这和 scrollview 自己没有任何关系,依赖于你如何在结尾增加内容。
所以,回到 WorkSpace 文件。
正确的设置
增加一个新方法 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
//initial offset because we're positioning from the center of each indicator's view
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)
}
现在,运行之后一切都会流畅啦。
当数字滑动到结束时,就会无缝连接继续循环。
打包
你已经将无限滚动视图添加到了 UIKit 的 UIScrollView
类里,这部分比较简单。真正有意思的是实现无限滚动的效果,这有赖于你是如何添加内容的。
你已经学会了在结尾如何增加一点额外的内容,这样让滚动的效果看起来更流畅,哄到到结尾时,视图直接能够无缝连接
大部分的工作都是在让额外的内容定位准确,设置无限滚动视图。正如这个例子中一样,如果你想纯用代码达到目的,你可能需要写一大推的代码,才能得到自己想要的效果。
代码
这里有 InfiniteScrollView
类的最终代码:
InfiniteScrollView 代码
本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权。